var React = global.React || require('react'); var Formsy = {}; var validationRules = require('./validationRules.js'); var formDataToObject = require('form-data-to-object'); var utils = require('./utils.js'); var Mixin = require('./Mixin.js'); var options = {}; Formsy.Mixin = Mixin; Formsy.defaults = function (passedOptions) { options = passedOptions; }; Formsy.addValidationRule = function (name, func) { validationRules[name] = func; }; Formsy.Form = React.createClass({ getInitialState: function () { return { isValid: true, isSubmitting: false, canChange: false }; }, getDefaultProps: function () { return { onSuccess: function () {}, onError: function () {}, onSubmit: function () {}, onValidSubmit: function () {}, onInvalidSubmit: function () {}, onSubmitted: function () {}, onValid: function () {}, onInvalid: function () {}, onChange: function () {}, validationErrors: null, preventExternalInvalidation: false }; }, childContextTypes: { formsy: React.PropTypes.object }, getChildContext: function () { return { formsy: { attachToForm: this.attachToForm, detachFromForm: this.detachFromForm, validate: this.validate, isFormDisabled: this.isFormDisabled, isValidValue: function (component, value) { return this.runValidation(component, value).isValid; }.bind(this) } } }, // Add a map to store the inputs of the form, a model to store // the values of the form and register child inputs componentWillMount: function () { this.inputs = {}; this.model = {}; }, componentDidMount: function () { this.validateForm(); }, componentWillUpdate: function () { // Keep a reference to input keys before form updates, // to check if inputs has changed after render this.prevInputKeys = Object.keys(this.inputs); }, componentDidUpdate: function () { if (this.props.validationErrors) { this.setInputValidationErrors(this.props.validationErrors); } var newInputKeys = Object.keys(this.inputs); if (utils.arraysDiffer(this.prevInputKeys, newInputKeys)) { this.validateForm(); } }, // Allow resetting to specified data reset: function (data) { this.setFormPristine(true); this.resetModel(data); }, // Update model, submit to url prop and send the model submit: function (event) { event && event.preventDefault(); // Trigger form as not pristine. // If any inputs have not been touched yet this will make them dirty // so validation becomes visible (if based on isPristine) this.setFormPristine(false); this.updateModel(); var model = this.mapModel(); this.props.onSubmit(model, this.resetModel, this.updateInputsWithError); this.state.isValid ? this.props.onValidSubmit(model, this.resetModel, this.updateInputsWithError) : this.props.onInvalidSubmit(model, this.resetModel, this.updateInputsWithError); }, mapModel: function () { if (this.props.mapping) { return this.props.mapping(this.model) } else { return formDataToObject(Object.keys(this.model).reduce(function (mappedModel, key) { var keyArray = key.split('.'); var base = mappedModel; while (keyArray.length) { var currentKey = keyArray.shift(); base = (base[currentKey] = keyArray.length ? base[currentKey] || {} : this.model[key]); } return mappedModel; }.bind(this), {})); } }, // Goes through all registered components and // updates the model values updateModel: function () { Object.keys(this.inputs).forEach(function (name) { var component = this.inputs[name]; this.model[name] = component.state._value; }.bind(this)); }, // Reset each key in the model to the original / initial / specified value resetModel: function (data) { Object.keys(this.inputs).forEach(function (name) { if (data && data[name]) { this.inputs[name].setValue(data[name]); } else { this.inputs[name].resetValue(); } }.bind(this)); this.validateForm(); }, setInputValidationErrors: function (errors) { Object.keys(this.inputs).forEach(function (name, index) { var component = this.inputs[name]; var args = [{ _isValid: !(name in errors), _validationError: typeof errors[name] === 'string' ? [errors[name]] : errors[name] }]; component.setState.apply(component, args); }.bind(this)); }, // Checks if the values have changed from their initial value isChanged: function() { return !utils.isSame(this.getPristineValues(), this.getCurrentValues()); }, getPristineValues: function() { var inputs = this.inputs; return Object.keys(inputs).reduce(function (data, name) { var component = inputs[name]; data[name] = component.props.value; return data; }, {}); }, // Go through errors from server and grab the components // stored in the inputs map. Change their state to invalid // and set the serverError message updateInputsWithError: function (errors) { Object.keys(errors).forEach(function (name, index) { var component = this.inputs[name]; if (!component) { throw new Error('You are trying to update an input that does not exist. Verify errors object with input names. ' + JSON.stringify(errors)); } var args = [{ _isValid: this.props.preventExternalInvalidation || false, _externalError: typeof errors[name] === 'string' ? [errors[name]] : errors[name] }]; component.setState.apply(component, args); }.bind(this)); }, // Traverse the children and children of children to find // all inputs by checking the name prop. Maybe do a better // check here traverseChildrenAndRegisterInputs: function (children) { if (typeof children !== 'object' || children === null) { return children; } return React.Children.map(children, function (child) { if (typeof child !== 'object' || child === null) { return child; } if (child.props && child.props.name) { return React.cloneElement(child, { _attachToForm: this.attachToForm, _detachFromForm: this.detachFromForm, _validate: this.validate, _isFormDisabled: this.isFormDisabled, _isValidValue: function (component, value) { return this.runValidation(component, value).isValid; }.bind(this) }, child.props && child.props.children); } else { return React.cloneElement(child, {}, this.traverseChildrenAndRegisterInputs(child.props && child.props.children)); } }, this); }, isFormDisabled: function () { return this.props.disabled; }, getCurrentValues: function () { return Object.keys(this.inputs).reduce(function (data, name) { var component = this.inputs[name]; data[name] = component.state._value; return data; }.bind(this), {}); }, setFormPristine: function (isPristine) { var inputs = this.inputs; var inputKeys = Object.keys(inputs); this.setState({ _formSubmitted: !isPristine }) // Iterate through each component and set it as pristine // or "dirty". inputKeys.forEach(function (name, index) { var component = inputs[name]; component.setState({ _formSubmitted: !isPristine, _isPristine: isPristine }); }.bind(this)); }, // Use the binded values and the actual input value to // validate the input and set its state. Then check the // state of the form itself validate: function (component) { // Trigger onChange if (this.state.canChange) { this.props.onChange(this.getCurrentValues(), this.isChanged()); } var validation = this.runValidation(component); // Run through the validations, split them up and call // the validator IF there is a value or it is required component.setState({ _isValid: validation.isValid, _isRequired: validation.isRequired, _validationError: validation.error, _externalError: null }, this.validateForm); }, // Checks validation on current value or a passed value runValidation: function (component, value) { var currentValues = this.getCurrentValues(); var validationErrors = component.props.validationErrors; var validationError = component.props.validationError; value = arguments.length === 2 ? value : component.state._value; var validationResults = this.runRules(value, currentValues, component._validations); var requiredResults = this.runRules(value, currentValues, component._requiredValidations); // the component defines an explicit validate function if (typeof component.validate === "function") { validationResults.failed = component.validate() ? [] : ['failed']; } var isRequired = Object.keys(component._requiredValidations).length ? !!requiredResults.success.length : false; var isValid = !validationResults.failed.length && !(this.props.validationErrors && this.props.validationErrors[component.props.name]); return { isRequired: isRequired, isValid: isRequired ? false : isValid, error: (function () { if (isValid && !isRequired) { return []; } if (validationResults.errors.length) { return validationResults.errors; } if (this.props.validationErrors && this.props.validationErrors[component.props.name]) { return typeof this.props.validationErrors[component.props.name] === 'string' ? [this.props.validationErrors[component.props.name]] : this.props.validationErrors[component.props.name]; } if (isRequired) { return [validationErrors[requiredResults.success[0]]] || null; } if (validationResults.failed.length) { return validationResults.failed.map(function(failed) { return validationErrors[failed] ? validationErrors[failed] : validationError; }).filter(function(x, pos, arr) { // Remove duplicates return arr.indexOf(x) === pos; }); } }.call(this)) }; }, runRules: function (value, currentValues, validations) { var results = { errors: [], failed: [], success: [] }; if (Object.keys(validations).length) { Object.keys(validations).forEach(function (validationMethod) { if (validationRules[validationMethod] && typeof validations[validationMethod] === 'function') { throw new Error('Formsy does not allow you to override default validations: ' + validationMethod); } if (!validationRules[validationMethod] && typeof validations[validationMethod] !== 'function') { throw new Error('Formsy does not have the validation rule: ' + validationMethod); } if (typeof validations[validationMethod] === 'function') { var validation = validations[validationMethod](currentValues, value); if (typeof validation === 'string') { results.errors.push(validation); results.failed.push(validationMethod); } else if (!validation) { results.failed.push(validationMethod); } return; } else if (typeof validations[validationMethod] !== 'function') { var validation = validationRules[validationMethod](currentValues, value, validations[validationMethod]); if (typeof validation === 'string') { results.errors.push(validation); results.failed.push(validationMethod); } else if (!validation) { results.failed.push(validationMethod); } else { results.success.push(validationMethod); } return; } return results.success.push(validationMethod); }); } return results; }, // Validate the form by going through all child input components // and check their state validateForm: function () { var allIsValid = true; var inputs = this.inputs; var inputKeys = Object.keys(inputs); // We need a callback as we are validating all inputs again. This will // run when the last component has set its state var onValidationComplete = function () { inputKeys.forEach(function (name) { if (!inputs[name].state._isValid) { allIsValid = false; } }.bind(this)); this.setState({ isValid: allIsValid }); if (allIsValid) { this.props.onValid(); } else { this.props.onInvalid(); } // Tell the form that it can start to trigger change events this.setState({ canChange: true }); }.bind(this); // Run validation again in case affected by other inputs. The // last component validated will run the onValidationComplete callback inputKeys.forEach(function (name, index) { var component = inputs[name]; var validation = this.runValidation(component); if (validation.isValid && component.state._externalError) { validation.isValid = false; } component.setState({ _isValid: validation.isValid, _isRequired: validation.isRequired, _validationError: validation.error, _externalError: !validation.isValid && component.state._externalError ? component.state._externalError : null }, index === inputKeys.length - 1 ? onValidationComplete : null); }.bind(this)); // If there are no inputs, set state where form is ready to trigger // change event. New inputs might be added later if (!inputKeys.length && this.isMounted()) { this.setState({ canChange: true }); } }, // Method put on each input component to register // itself to the form attachToForm: function (component) { this.inputs[component.props.name] = component; this.model[component.props.name] = component.state._value; this.validate(component); }, // Method put on each input component to unregister // itself from the form detachFromForm: function (component) { delete this.inputs[component.props.name]; delete this.model[component.props.name]; }, render: function () { return (
{this.props.children} {/*this.traverseChildrenAndRegisterInputs(this.props.children)*/}
); } }); if (!global.exports && !global.module && (!global.define || !global.define.amd)) { global.Formsy = Formsy; } module.exports = Formsy;