UNPKG

react-formian

Version:

A React framework for easily creating and handling controlled forms

285 lines (246 loc) 7.28 kB
import injectCSS from './injectCSS'; import React, {Component, Children} from 'react'; import PropTypes from 'prop-types'; import validators from './validators'; import formatters from './formatters'; import Reset from './Buttons/Reset'; import Submit from './Buttons/Submit'; import Fieldset from './common/Fieldset'; import formElements from './Inputs'; const Recaptcha = formElements[formElements.length - 1]; class Form extends Component { constructor(props) { super(props); this.addHandlersToChild = this.addHandlersToChild.bind(this); this.renderChildren = this.renderChildren.bind(this); this.autoSubmit = this.autoSubmit.bind(this); this.onChange = this.onChange.bind(this); this.onBlur = this.onBlur.bind(this); this.onFocus = this.onFocus.bind(this); this.recaptcha = this.recaptcha.bind(this); this.flagAllErrors = this.flagAllErrors.bind(this); this.resetForm = this.resetForm.bind(this); this.isDefaultState = this.isDefaultState.bind(this); this.onSubmit = this.onSubmit.bind(this); this.formatters = formatters; this.validators = validators; if (props.customStyles !== false) injectCSS(); this.initialState = { disabled: !this.props.noValidate, formData: {} }; this.mapInputsToState(props.children); this.formDataKeys = Object.keys(this.initialState.formData); this.state = Object.assign({}, this.initialState); } mapInputsToState(children) { Children.map(children, child => { if (!child) return; else if (child.type === Recaptcha) { this.initialState.formData.recaptcha = false; } else if (child.type === Fieldset || child.type === 'fieldset') { this.mapInputsToState(child.props.children); } else if (formElements.includes(child.type)) { // discover the dataset object key const key = child.props.name; // set prevalidated for marked inputs; otherwise set an appropriate validator function if (!child.props.required) { this.validators[key] = this.validators.prevalidated; } else { this.setCheckerForChild(child, 'validators'); } this.setCheckerForChild(child, 'formatters'); this.setInitialStateForChild(child); } }); } setCheckerForChild(child, set, setOff) { const {name, type, defaultValue} = child.props; // tinyInt rule if (child.props.tinyInt && set === 'formatters' && (type === 'onoff' || type === 'checkbox')) { this[set][name] = this[set].tinyInt; } else { this[set][name] = child.props[set] // custom || this[set][name.toLowerCase()] // by field name || this[set][child.props.type.toLowerCase()] // by type ; } } setInitialStateForChild(child) { const {name, options, defaultValue} = child.props; const target = {value: defaultValue, checked: defaultValue}; if (options) target.value = options[defaultValue]; this.initialState.formData[name] = this.formatters[name](target); } addHandlersToChild(child, tabIndex) { if (formElements.includes(child.type)) { return React.cloneElement(child, { tabIndex: tabIndex, onChange: child.type === Recaptcha ? this.recaptcha : this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, dataset: this.state.formData, required: child.required || true }); } if (child.type === Submit) { return React.cloneElement(child, { tabIndex: tabIndex, disabled: this.state.disabled, flagAllErrors: this.flagAllErrors }); } if (child.type === Reset) { return React.cloneElement(child, { tabIndex: tabIndex, disabled: this.isDefaultState(), resetForm: this.resetForm }); } return child; } renderChildren() { let tabIndex = 1; return Children.map(this.props.children, child => { if (!child) return; if (child.type === Fieldset || child.type === 'fieldset') { return React.cloneElement(child, { children: Children.map(child.props.children, child => { return this.addHandlersToChild(child, tabIndex++); }) }); } if (formElements.includes(child.type)) { return this.addHandlersToChild(child, tabIndex++); } return child; }); } isDefaultState() { for (let i = 0; i < this.formDataKeys.length; i++) { if (this.initialState.formData[this.formDataKeys[i]] !== this.state.formData[this.formDataKeys[i]]) { return false; } } return true; } onChange(evt) { let key = evt.target.id; if (evt.target.type === 'radio') key = key.split('@@')[1]; const formData = Object.assign({}, this.state.formData, {[key]: this.formatters[key] ? this.formatters[key](evt.target) : evt.target.value} ); if (!this.props.noValidate) this.checkForm(formData); if (this.props.submitOnChange) this.autoSubmit(); } // if left field with an error, flag it onBlur(evt) { const {formData} = this.state; const key = evt.target.id; const data = formData[key]; if (this.validators[key] && !this.validators[key](formData[key])) { evt.target.classList.add('error'); evt.target.nextSibling.classList.add('error'); } } // if focusing on an err'd field, clear the ErrorPage onFocus(evt) { evt.target.classList.remove('error'); evt.target.nextSibling.classList.remove('error'); } onSubmit(evt) { evt.preventDefault(); if (this.state.disabled) this.flagAllErrors(); else this.props.onSubmit.call(this, this.state.formData); } recaptcha(value) { const formData = Object.assign({}, this.state.formData, {recaptcha: value} ); if (!this.props.noValidate) this.checkForm(formData); } checkForm(formData) { let disabled = false; let key; for (let i = 0; i < this.formDataKeys.length; i++) { key = this.formDataKeys[i]; if (this.validators[key] && !this.validators[key](formData[key])) { disabled = true; break; } } this.setState({ disabled, formData }); } resetForm(evt) { evt.preventDefault(); this.setState(Object.assign({}, this.initialState)); for (let i = 0 ; i < this.formDataKeys.length; i++) { document.getElementById(this.formDataKeys[i]) .dispatchEvent(new Event('blur')) ; } } flagAllErrors() { if (!this.state.disabled) return; let target; for (let i = 0 ; i < this.formDataKeys.length; i++) { target = document.getElementById(this.formDataKeys[i]); target.classList.remove('error'); target.nextSibling.classList.remove('error'); } } autoSubmit() { clearTimeout(this.submitTimeout); this.submitTimeout = setTimeout(() => { this.onSubmit(new Event('synthetic', {bubbles: false, cancelable: true} )); }, 2000); } componentWillUnmount() { if (this.submitTimeout) { clearTimeout(this.submitTimeout); this.onSubmit(new Event('synthetic', {bubbles: false, cancelable: true} )); } } render() { return ( <form id={this.props.id} className={'formian-form '+this.props.className} onSubmit={this.onSubmit} style={this.props.style} disabled={this.state.disabled} > {this.renderChildren()} </form> ); } } Form.defaultProps = { id: "", className: "", autoComplete: "on", noValidate: false, submitOnChange: false }; const Formian = formElements.reduce((formian, input) => { formian[input.name] = input; return formian; }, { Form, Submit, Reset, Fieldset }); export default Formian;