(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= min && value.length <= max; } return value.length >= min; }, equals: function (value, eql) { return value == eql; }, equalsField: function (value, field) { return value === this[field]; } }; var toURLEncoded = function (element, key, list) { var list = list || []; if (typeof (element) == 'object') { for (var idx in element) toURLEncoded(element[idx], key ? key + '[' + idx + ']' : idx, list); } else { list.push(key + '=' + encodeURIComponent(element)); } return list.join('&'); }; var request = function (method, url, data, contentType, headers) { var contentType = contentType === 'urlencoded' ? 'application/' + contentType.replace('urlencoded', 'x-www-form-urlencoded') : 'application/json'; data = contentType === 'application/json' ? JSON.stringify(data) : toURLEncoded(data); return new Promise(function (resolve, reject) { try { var xhr = new XMLHttpRequest(); xhr.open(method, url, true); xhr.setRequestHeader('Accept', 'application/json'); xhr.setRequestHeader('Content-Type', contentType); // Add passed headers Object.keys(headers).forEach(function (header) { xhr.setRequestHeader(header, headers[header]); }); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { try { var response = xhr.responseText ? JSON.parse(xhr.responseText) : null; if (xhr.status >= 200 && xhr.status < 300) { resolve(response); } else { reject(response); } } catch (e) { reject(e); } } }; xhr.send(data); } catch (e) { reject(e); } }); }; var arraysDiffer = function (arrayA, arrayB) { var isDifferent = false; if (arrayA.length !== arrayB.length) { isDifferent = true; } else { arrayA.forEach(function (item, index) { if (item !== arrayB[index]) { isDifferent = true; } }); } return isDifferent; }; var ajax = { post: request.bind(null, 'POST'), put: request.bind(null, 'PUT') }; var options = {}; Formsy.defaults = function (passedOptions) { options = passedOptions; }; Formsy.Mixin = { getInitialState: function () { return { _value: this.props.value ? this.props.value : '', _isValid: true, _isPristine: true }; }, componentWillMount: function () { var configure = function () { // Add validations to the store itself as the props object can not be modified this._validations = this.props.validations || ''; if (this.props.required) { this._validations = this.props.validations ? this.props.validations + ',' : ''; this._validations += 'isValue'; } this.props._attachToForm(this); }.bind(this); if (!this.props.name) { throw new Error('Form Input requires a name property when used'); } if (!this.props._attachToForm) { return setTimeout(function () { if (!this.props._attachToForm) { throw new Error('Form Mixin requires component to be nested in a Form'); } configure(); }.bind(this), 0); } configure(); }, // We have to make the validate method is kept when new props are added componentWillReceiveProps: function (nextProps) { nextProps._attachToForm = this.props._attachToForm; nextProps._detachFromForm = this.props._detachFromForm; nextProps._validate = this.props._validate; }, componentDidUpdate: function(prevProps, prevState) { // If the input is untouched and something outside changes the value // update the FORM model by re-attaching to the form if (this.state._isPristine) { if (this.props.value !== prevProps.value && this.state._value === prevProps.value) { this.state._value = this.props.value || ''; this.props._attachToForm(this); } } }, // Detach it when component unmounts componentWillUnmount: function () { this.props._detachFromForm(this); }, // We validate after the value has been set setValue: function (value) { this.setState({ _value: value, _isPristine: false }, function () { this.props._validate(this); }.bind(this)); }, resetValue: function () { this.setState({ _value: '', _isPristine: true }, function () { this.props._validate(this); }); }, getValue: function () { return this.state._value; }, hasValue: function () { return this.state._value !== ''; }, getErrorMessage: function () { return this.isValid() || this.showRequired() ? null : this.state._serverError || this.props.validationError; }, isValid: function () { return this.state._isValid; }, isPristine: function () { return this.state._isPristine; }, isRequired: function () { return !!this.props.required; }, showRequired: function () { return this.isRequired() && this.state._value === ''; }, showError: function () { return !this.showRequired() && !this.state._isValid; } }; Formsy.addValidationRule = function (name, func) { validationRules[name] = func; }; Formsy.Form = React.createClass({displayName: "Form", getInitialState: function () { return { isValid: true, isSubmitting: false, canChange: false }; }, getDefaultProps: function () { return { headers: {}, onSuccess: function () {}, onError: function () {}, onSubmit: function () {}, onSubmitted: function () {}, onValid: function () {}, onInvalid: function () {}, onChange: function () {} }; }, // 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 = {}; this.registerInputs(this.props.children); }, componentDidMount: function () { this.validateForm(); }, componentWillUpdate: function () { var inputKeys = Object.keys(this.inputs); // The updated children array is not available here for some reason, // we need to wait for next event loop setTimeout(function () { this.registerInputs(this.props.children); var newInputKeys = Object.keys(this.inputs); if (arraysDiffer(inputKeys, newInputKeys)) { this.validateForm(); } }.bind(this), 0); }, // Update model, submit to url prop and send the model submit: function (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); // To support use cases where no async or request operation is needed. // The "onSubmit" callback is called with the model e.g. {fieldName: "myValue"}, // if wanting to reset the entire form to original state, the second param is a callback for this. if (!this.props.url) { this.updateModel(); this.props.onSubmit(this.mapModel(), this.resetModel, this.updateInputsWithError); return; } this.updateModel(); this.setState({ isSubmitting: true }); this.props.onSubmit(this.mapModel(), this.resetModel, this.updateInputsWithError); var headers = (Object.keys(this.props.headers).length && this.props.headers) || options.headers || {}; var method = this.props.method && ajax[this.props.method.toLowerCase()] ? this.props.method.toLowerCase() : 'post'; ajax[method](this.props.url, this.mapModel(), this.props.contentType || options.contentType || 'json', headers) .then(function (response) { this.props.onSuccess(response); this.props.onSubmitted(); }.bind(this)) .catch(this.failSubmit); }, mapModel: function () { return this.props.mapping ? this.props.mapping(this.model) : this.model; }, // 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 value resetModel: function () { Object.keys(this.inputs).forEach(function (name) { this.inputs[name].resetValue(); }.bind(this)); this.validateForm(); }, // 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 exists. Verify errors object with input names. ' + JSON.stringify(errors)); } var args = [{ _isValid: false, _serverError: errors[name] }]; component.setState.apply(component, args); }.bind(this)); }, failSubmit: function (errors) { this.updateInputsWithError(errors); this.setState({ isSubmitting: false }); this.props.onError(errors); this.props.onSubmitted(); }, // Traverse the children and children of children to find // all inputs by checking the name prop. Maybe do a better // check here registerInputs: function (children) { React.Children.forEach(children, function (child) { if (child && child.props && child.props.name) { child.props._attachToForm = this.attachToForm; child.props._detachFromForm = this.detachFromForm; child.props._validate = this.validate; } if (child && child.props && child.props.children) { this.registerInputs(child.props.children); } }.bind(this)); }, 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); // Iterate through each component and set it as pristine // or "dirty". inputKeys.forEach(function (name, index) { var component = inputs[name]; component.setState({ _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 this.state.canChange && this.props.onChange && this.props.onChange(this.getCurrentValues()); if (!component.props.required && !component._validations) { return; } // Run through the validations, split them up and call // the validator IF there is a value or it is required var isValid = this.runValidation(component); component.setState({ _isValid: isValid, _serverError: null }, this.validateForm); }, runValidation: function (component) { var isValid = true; if (component._validations.length && (component.props.required || component.state._value !== '')) { component._validations.split(',').forEach(function (validation) { var args = validation.split(':'); var validateMethod = args.shift(); args = args.map(function (arg) { try { return JSON.parse(arg); } catch (e) { return arg; // It is a string if it can not parse it } }); args = [component.state._value].concat(args); if (!validationRules[validateMethod]) { throw new Error('Formsy does not have the validation rule: ' + validateMethod); } if (!validationRules[validateMethod].apply(this.getCurrentValues(), args)) { isValid = false; } }.bind(this)); } return isValid; }, // 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 }); allIsValid && this.props.onValid(); !allIsValid && 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 isValid = this.runValidation(component); component.setState({ _isValid: isValid, _serverError: null }, index === inputKeys.length - 1 ? onValidationComplete : null); }.bind(this)); // If there are no inputs, it is ready to trigger change events if (!inputKeys.length) { 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 React.DOM.form({ onSubmit: this.submit, className: this.props.className }, this.props.children ); } }); if (!global.exports && !global.module && (!global.define || !global.define.amd)) { global.Formsy = Formsy; } module.exports = Formsy; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"react":"react"}]},{},[1]);