From b0738a503269cd17284a8a7332a6b1440266eddd Mon Sep 17 00:00:00 2001 From: christianalfoni Date: Mon, 13 Apr 2015 18:29:18 +0200 Subject: [PATCH] Added validation objects and required validation --- README.md | 38 ++++- release/formsy-react.js | 312 +++++++++++++++++++----------------- release/formsy-react.min.js | 2 +- specs/Element-spec.js | 282 ++++++++++++++++++++++---------- specs/Formsy-spec.js | 21 +-- specs/Submit-spec.js | 178 -------------------- src/Mixin.js | 70 +++++--- src/main.js | 138 ++++++++++------ src/utils.js | 61 ------- src/validationRules.js | 60 ++++--- 10 files changed, 587 insertions(+), 575 deletions(-) delete mode 100644 specs/Submit-spec.js diff --git a/README.md b/README.md index 6fbbe53..3ca087b 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ In development you will get a warning about Formsy overriding `props`. This is d - [value](#value) - [validations](#validations) - [validationError](#validationerror) + - [validationErrors](#elementvalidationerrors) - [required](#required) - [getValue()](#getvalue) - [setValue()](#setvalue) @@ -331,7 +332,18 @@ You should always use the [**getValue()**](#getvalue) method inside your formsy #### validations ```html - + + + ``` An comma seperated list with validation rules. Take a look at [**Validators**](#validators) to see default rules. Use ":" to separate arguments passed to the validator. The arguments will go through a **JSON.parse** converting them into correct JavaScript types. Meaning: @@ -347,11 +359,33 @@ Works just fine. ``` The message that will show when the form input component is invalid. +#### validationErrors +```html + +``` +The message that will show when the form input component is invalid. You can combine this with `validationError`. Keys not found in `validationErrors` defaults to the general error message. + #### required ```html ``` -A property that tells the form that the form input component value is required. + +A property that tells the form that the form input component value is required. By default it uses `isEmptyString`, but you can define your own definition of what defined a required state. + +```html + +``` +Would be typical for a checkbox type of form element. #### getValue() ```javascript diff --git a/release/formsy-react.js b/release/formsy-react.js index 40243af..c165736 100644 --- a/release/formsy-react.js +++ b/release/formsy-react.js @@ -27,7 +27,6 @@ Formsy.Form = React.createClass({displayName: "Form", }, getDefaultProps: function () { return { - headers: {}, onSuccess: function () {}, onError: function () {}, onSubmit: function () {}, @@ -36,7 +35,8 @@ Formsy.Form = React.createClass({displayName: "Form", onSubmitted: function () {}, onValid: function () {}, onInvalid: function () {}, - onChange: function () {} + onChange: function () {}, + validationErrors: null }; }, @@ -188,7 +188,9 @@ Formsy.Form = React.createClass({displayName: "Form", child.props._detachFromForm = this.detachFromForm; child.props._validate = this.validate; child.props._isFormDisabled = this.isFormDisabled; - child.props._isValidValue = this.runValidation; + child.props._isValidValue = function (component, value) { + return this.runValidation(component, value).isValid; + }.bind(this); } if (child && child.props && child.props.children) { @@ -234,18 +236,13 @@ Formsy.Form = React.createClass({displayName: "Form", this.props.onChange(this.getCurrentValues()); } - var isValid = true; - if (component.validate && typeof component.validate === 'function') { - isValid = component.validate(); - } else if (component.props.required || component._validations) { - isValid = this.runValidation(component); - } - + 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: isValid, - _serverError: null + _isValid: validation.isValid, + _isRequired: validation.isRequired, + _validationError: validation.error }, this.validateForm); }, @@ -253,33 +250,78 @@ Formsy.Form = React.createClass({displayName: "Form", // Checks validation on current value or a passed value runValidation: function (component, value) { - var isValid = true; + + var currentValues = this.getCurrentValues(); + var validationErrors = component.props.validationErrors; + var validationError = component.props.validationError; value = arguments.length === 2 ? value : component.state._value; - if (component._validations.length) { - component._validations.split(/\,(?![^{\[]*[}\]])/g).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 = [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)); - } + + 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") { - // the component defines an explicit validate function - isValid = component.validate() + validationResults.failed = component.validate() ? [] : ['failed']; } - return isValid; + + 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: isValid, + error: (function () { + + if (isValid && !isRequired) { + return ''; + } + + if (this.props.validationErrors && this.props.validationErrors[component.props.name]) { + return this.props.validationErrors[component.props.name]; + } + + if (isRequired) { + return validationErrors[requiredResults.success[0]] || null; + } + + if (!isValid) { + return validationErrors[validationResults.failed[0]] || validationError; + } + + }.call(this)) + }; + + }, + + runRules: function (value, currentValues, validations) { + + var results = { + 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' && !validations[validationMethod](currentValues, value)) { + return results.failed.push(validationMethod); + } else if (typeof validations[validationMethod] !== 'function' && !validationRules[validationMethod](currentValues, value, validations[validationMethod])) { + return results.failed.push(validationMethod); + } + + return results.success.push(validationMethod); + + }); + } + + return results; + }, // Validate the form by going through all child input components @@ -319,10 +361,11 @@ Formsy.Form = React.createClass({displayName: "Form", // last component validated will run the onValidationComplete callback inputKeys.forEach(function (name, index) { var component = inputs[name]; - var isValid = this.runValidation(component); + var validation = this.runValidation(component); component.setState({ - _isValid: isValid, - _serverError: null + _isValid: validation.isValid, + _isRequired: validation.isRequired, + _validationError: validation.error }, index === inputKeys.length - 1 ? onValidationComplete : null); }.bind(this)); @@ -371,14 +414,48 @@ module.exports = Formsy; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"./Mixin.js":2,"./utils.js":3,"./validationRules.js":4,"react":"react"}],2:[function(require,module,exports){ +var convertValidationsToObject = function (validations) { + + if (typeof validations === 'string') { + + return validations.split(/\,(?![^{\[]*[}\]])/g).reduce(function (validations, 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 + } + }); + + if (args.length > 1) { + throw new Error('Formsy does not support multiple args on string validations. Use object format of validations instead.'); + } + validations[validateMethod] = args[0] || true; + return validations; + }, {}); + + } + + return validations || {}; + +}; module.exports = { getInitialState: function () { - var value = 'value' in this.props ? this.props.value : ''; return { - _value: value, + _value: this.props.value, + _isRequired: false, _isValid: true, _isPristine: true, - _pristineValue: value + _pristineValue: this.props.value, + _validationError: '' + }; + }, + getDefaultProps: function () { + return { + validationError: '', + validationErrors: {} }; }, componentWillMount: function () { @@ -419,25 +496,15 @@ module.exports = { var isValueChanged = function () { - return ( - this.props.value !== prevProps.value && ( - this.state._value === prevProps.value || - - // Since undefined is converted to empty string we have to - // check that specifically - (this.state._value === '' && prevProps.value === undefined) - ) - ); + return this.props.value !== prevProps.value && this.state._value === prevProps.value; }.bind(this); // If validations has changed or something outside changes // the value, set the value again running a validation - if (prevProps.validations !== this.props.validations || isValueChanged()) { - var value = 'value' in this.props ? this.props.value : ''; - this.setValue(value); + this.setValue(this.props.value); } }, @@ -449,12 +516,8 @@ module.exports = { setValidations: function (validations, required) { // Add validations to the store itself as the props object can not be modified - this._validations = validations || ''; - - if (required) { - this._validations = validations ? validations + ',' : ''; - this._validations += 'isValue'; - } + this._validations = convertValidationsToObject(validations) || {}; + this._requiredValidations = required === true ? {isDefaultRequiredValue: true} : convertValidationsToObject(required); }, @@ -482,7 +545,7 @@ module.exports = { return this.state._value !== ''; }, getErrorMessage: function () { - return this.isValid() || this.showRequired() ? null : this.state._serverError || this.props.validationError; + return !this.isValid() || this.showRequired() ? this.state._validationError : null; }, isFormDisabled: function () { return this.props._isFormDisabled(); @@ -494,13 +557,13 @@ module.exports = { return this.state._isPristine; }, isRequired: function () { - return !!this.props.required; + return this.state._isRequired; }, showRequired: function () { - return this.isRequired() && this.state._value === ''; + return this.isRequired(); }, showError: function () { - return !this.showRequired() && !this.state._isValid; + return !this.showRequired() && !this.isValid(); }, isValidValue: function (value) { return this.props._isValidValue.call(null, this, value); @@ -510,63 +573,6 @@ module.exports = { },{}],3:[function(require,module,exports){ -var csrfTokenSelector = typeof document != 'undefined' ? document.querySelector('meta[name="csrf-token"]') : null; - -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); - - if (!!csrfTokenSelector && !!csrfTokenSelector.content) { - xhr.setRequestHeader('X-CSRF-Token', csrfTokenSelector.content); - } - - // 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); - } - }); -}; - module.exports = { arraysDiffer: function (arrayA, arrayB) { var isDifferent = false; @@ -580,10 +586,6 @@ module.exports = { }); } return isDifferent; - }, - ajax: { - post: request.bind(null, 'POST'), - put: request.bind(null, 'PUT') } }; @@ -591,47 +593,65 @@ module.exports = { },{}],4:[function(require,module,exports){ module.exports = { - 'isValue': function (value) { - return value !== ''; + 'isDefaultRequiredValue': function (values, value) { + return value === undefined || value === ''; }, - 'isEmail': function (value) { - return value.match(/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i); + 'hasValue': function (values, value) { + return value !== undefined; }, - 'isTrue': function (value) { + 'matchRegexp': function (values, value, regexp) { + return value !== undefined && !!value.match(regexp); + }, + 'isUndefined': function (values, value) { + return value === undefined; + }, + 'isEmptyString': function (values, value) { + return value === ''; + }, + 'isEmail': function (values, value) { + return value !== undefined && value.match(/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i); + }, + 'isTrue': function (values, value) { return value === true; }, - 'isNumeric': function (value) { + 'isFalse': function (values, value) { + return value === false; + }, + 'isNumeric': function (values, value) { if (typeof value === 'number') { return true; } else { - var matchResults = value.match(/[-+]?(\d*[.])?\d+/); - if (!! matchResults) { + var matchResults = value !== undefined && value.match(/[-+]?(\d*[.])?\d+/); + if (!!matchResults) { return matchResults[0] == value; } else { return false; } } }, - 'isAlpha': function (value) { - return value.match(/^[a-zA-Z]+$/); + 'isAlpha': function (values, value) { + return value !== undefined && value.match(/^[a-zA-Z]+$/); }, - 'isWords': function (value) { - return value.match(/^[a-zA-Z\s]+$/); + 'isWords': function (values, value) { + return value !== undefined && value.match(/^[a-zA-Z\s]+$/); }, - 'isSpecialWords': function (value) { - return value.match(/^[a-zA-Z\s\u00C0-\u017F]+$/); + 'isSpecialWords': function (values, value) { + return value !== undefined && value.match(/^[a-zA-Z\s\u00C0-\u017F]+$/); }, - isLength: function (value, min, max) { - if (max !== undefined) { - return value.length >= min && value.length <= max; - } - return value.length >= min; + isLength: function (values, value, length) { + return value !== undefined && value.length === length; }, - equals: function (value, eql) { + equals: function (values, value, eql) { return value == eql; }, - equalsField: function (value, field) { + equalsField: function (values, value, field) { return value == this[field]; + }, + maxLength: function (values, value, length) { + return value !== undefined && value.length <= length; + }, + minLength: function (values, value, length) { + return value !== undefined && value.length >= length; } }; diff --git a/release/formsy-react.min.js b/release/formsy-react.min.js index 42e3df6..e49db8d 100644 --- a/release/formsy-react.min.js +++ b/release/formsy-react.min.js @@ -1 +1 @@ -!function t(i,e,n){function s(o,a){if(!e[o]){if(!i[o]){var u="function"==typeof require&&require;if(!a&&u)return u(o,!0);if(r)return r(o,!0);var h=new Error("Cannot find module '"+o+"'");throw h.code="MODULE_NOT_FOUND",h}var p=e[o]={exports:{}};i[o][0].call(p.exports,function(t){var e=i[o][1][t];return s(e?e:t)},p,p.exports,t,i,e,n)}return e[o].exports}for(var r="function"==typeof require&&require,o=0;o=200&&u.status<300?n(t):a(t)}catch(i){a(i)}},u.send(s)}catch(h){a(h)}})};i.exports={arraysDiffer:function(t,i){var e=!1;return t.length!==i.length?e=!0:t.forEach(function(t,n){t!==i[n]&&(e=!0)}),e},ajax:{post:s.bind(null,"POST"),put:s.bind(null,"PUT")}}},{}],4:[function(t,i){i.exports={isValue:function(t){return""!==t},isEmail:function(t){return t.match(/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i)},isTrue:function(t){return t===!0},isNumeric:function(t){if("number"==typeof t)return!0;var i=t.match(/[-+]?(\d*[.])?\d+/);return i?i[0]==t:!1},isAlpha:function(t){return t.match(/^[a-zA-Z]+$/)},isWords:function(t){return t.match(/^[a-zA-Z\s]+$/)},isSpecialWords:function(t){return t.match(/^[a-zA-Z\s\u00C0-\u017F]+$/)},isLength:function(t,i,e){return void 0!==e?t.length>=i&&t.length<=e:t.length>=i},equals:function(t,i){return t==i},equalsField:function(t,i){return t==this[i]}}},{}]},{},[1]); \ No newline at end of file +!function t(i,s,n){function e(o,u){if(!s[o]){if(!i[o]){var a="function"==typeof require&&require;if(!u&&a)return a(o,!0);if(r)return r(o,!0);var h=new Error("Cannot find module '"+o+"'");throw h.code="MODULE_NOT_FOUND",h}var d=s[o]={exports:{}};i[o][0].call(d.exports,function(t){var s=i[o][1][t];return e(s?s:t)},d,d.exports,t,i,s,n)}return s[o].exports}for(var r="function"==typeof require&&require,o=0;o1)throw new Error("Formsy does not support multiple args on string validations. Use object format of validations instead.");return t[n]=s[0]||!0,t},{}):t||{}};i.exports={getInitialState:function(){return{_value:this.props.value,_isRequired:!1,_isValid:!0,_isPristine:!0,_pristineValue:this.props.value,_validationError:""}},getDefaultProps:function(){return{validationError:"",validationErrors:{}}},componentWillMount:function(){var t=function(){this.setValidations(this.props.validations,this.props.required),this.props._attachToForm(this)}.bind(this);if(!this.props.name)throw new Error("Form Input requires a name property when used");return this.props._attachToForm?(t(),void 0):setTimeout(function(){if(this.isMounted()){if(!this.props._attachToForm)throw new Error("Form Mixin requires component to be nested in a Form");t()}}.bind(this),0)},componentWillReceiveProps:function(t){t._attachToForm=this.props._attachToForm,t._detachFromForm=this.props._detachFromForm,t._validate=this.props._validate,t._isValidValue=this.props._isValidValue,t._isFormDisabled=this.props._isFormDisabled,this.setValidations(t.validations,t.required)},componentDidUpdate:function(t){var i=function(){return this.props.value!==t.value&&this.state._value===t.value}.bind(this);(t.validations!==this.props.validations||i())&&this.setValue(this.props.value)},componentWillUnmount:function(){this.props._detachFromForm(this)},setValidations:function(t,i){this._validations=s(t)||{},this._requiredValidations=i===!0?{isDefaultRequiredValue:!0}:s(i)},setValue:function(t){this.setState({_value:t,_isPristine:!1},function(){this.props._validate(this)}.bind(this))},resetValue:function(){this.setState({_value:this.state._pristineValue,_isPristine:!0},function(){this.props._validate(this)})},getValue:function(){return this.state._value},hasValue:function(){return""!==this.state._value},getErrorMessage:function(){return!this.isValid()||this.showRequired()?this.state._validationError:null},isFormDisabled:function(){return this.props._isFormDisabled()},isValid:function(){return this.state._isValid},isPristine:function(){return this.state._isPristine},isRequired:function(){return this.state._isRequired},showRequired:function(){return this.isRequired()},showError:function(){return!this.showRequired()&&!this.isValid()},isValidValue:function(t){return this.props._isValidValue.call(null,this,t)}}},{}],3:[function(t,i){i.exports={arraysDiffer:function(t,i){var s=!1;return t.length!==i.length?s=!0:t.forEach(function(t,n){t!==i[n]&&(s=!0)}),s}}},{}],4:[function(t,i){i.exports={isDefaultRequiredValue:function(t,i){return void 0===i||""===i},hasValue:function(t,i){return void 0!==i},matchRegexp:function(t,i,s){return void 0!==i&&!!i.match(s)},isUndefined:function(t,i){return void 0===i},isEmptyString:function(t,i){return""===i},isEmail:function(t,i){return void 0!==i&&i.match(/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i)},isTrue:function(t,i){return i===!0},isFalse:function(t,i){return i===!1},isNumeric:function(t,i){if("number"==typeof i)return!0;var s=void 0!==i&&i.match(/[-+]?(\d*[.])?\d+/);return s?s[0]==i:!1},isAlpha:function(t,i){return void 0!==i&&i.match(/^[a-zA-Z]+$/)},isWords:function(t,i){return void 0!==i&&i.match(/^[a-zA-Z\s]+$/)},isSpecialWords:function(t,i){return void 0!==i&&i.match(/^[a-zA-Z\s\u00C0-\u017F]+$/)},isLength:function(t,i,s){return void 0!==i&&i.length===s},equals:function(t,i,s){return i==s},equalsField:function(t,i,s){return i==this[s]},maxLength:function(t,i,s){return void 0!==i&&i.length<=s},minLength:function(t,i,s){return void 0!==i&&i.length>=s}}},{}]},{},[1]); \ No newline at end of file diff --git a/specs/Element-spec.js b/specs/Element-spec.js index 77ecb6a..5575b69 100644 --- a/specs/Element-spec.js +++ b/specs/Element-spec.js @@ -79,45 +79,6 @@ describe('Element', function() { }); - it('should return server error message when calling getErrorMessage()', function (done) { - - jasmine.Ajax.install(); - - var getErrorMessage = null; - var TestInput = React.createClass({ - mixins: [Formsy.Mixin], - componentDidMount: function () { - getErrorMessage = this.getErrorMessage; - }, - updateValue: function (event) { - this.setValue(event.target.value); - }, - render: function () { - return - } - }); - var form = TestUtils.renderIntoDocument( - - - - ); - - var form = TestUtils.Simulate.submit(form.getDOMNode()); - - jasmine.Ajax.requests.mostRecent().respondWith({ - status: 500, - contentType: 'application/json', - responseText: '{"foo": "bar"}' - }) - - setTimeout(function () { - expect(getErrorMessage()).toBe('bar'); - jasmine.Ajax.uninstall(); - done(); - }, 0); - - }); - it('should return true or false when calling isValid() depending on valid state', function () { var isValid = null; @@ -163,13 +124,15 @@ describe('Element', function() { }); var form = TestUtils.renderIntoDocument( - - + + + ); expect(isRequireds[0]()).toBe(false); expect(isRequireds[1]()).toBe(true); + expect(isRequireds[2]()).toBe(true); }); @@ -202,47 +165,6 @@ describe('Element', function() { }); - it('should return true or false when calling showError() depending on value is invalid or a server error has arrived, or not', function (done) { - - var showError = null; - var TestInput = React.createClass({ - mixins: [Formsy.Mixin], - componentDidMount: function () { - showError = this.showError; - }, - updateValue: function (event) { - this.setValue(event.target.value); - }, - render: function () { - return - } - }); - var form = TestUtils.renderIntoDocument( - - - - ); - - expect(showError()).toBe(true); - - var input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); - TestUtils.Simulate.change(input, {target: {value: 'foo@foo.com'}}); - expect(showError()).toBe(false); - - jasmine.Ajax.install(); - TestUtils.Simulate.submit(form.getDOMNode()); - jasmine.Ajax.requests.mostRecent().respondWith({ - status: 500, - responseType: 'application/json', - responseText: '{"foo": "Email already exists"}' - }); - setTimeout(function () { - expect(showError()).toBe(true); - jasmine.Ajax.uninstall(); - done(); - }, 0); - }); - it('should return true or false when calling isPristine() depending on input has been "touched" or not', function () { var isPristine = null; @@ -375,4 +297,200 @@ it('should allow an undefined value to be updated to a value', function (done) { }); + it('should be able to use an object as validations property', function () { + + var TestInput = React.createClass({ + mixins: [Formsy.Mixin], + render: function () { + return + } + }); + var TestForm = React.createClass({ + render: function () { + return ( + + + + ); + } + }); + var form = TestUtils.renderIntoDocument( + + ); + + var input = TestUtils.findRenderedComponentWithType(form, TestInput); + expect(input.isValidValue('foo@bar.com')).toBe(true); + expect(input.isValidValue('foo@bar')).toBe(false); + }); + + it('should be able to pass complex values to a validation rule', function () { + + var TestInput = React.createClass({ + mixins: [Formsy.Mixin], + changeValue: function (event) { + this.setValue(event.target.value); + }, + render: function () { + return + } + }); + var TestForm = React.createClass({ + render: function () { + return ( + + + + ); + } + }); + var form = TestUtils.renderIntoDocument( + + ); + + var inputComponent = TestUtils.findRenderedComponentWithType(form, TestInput); + expect(inputComponent.isValid()).toBe(true); + var input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); + TestUtils.Simulate.change(input, {target: {value: 'bar'}}); + expect(inputComponent.isValid()).toBe(false); + }); + + it('should be able to run a function to validate', function () { + + var TestInput = React.createClass({ + mixins: [Formsy.Mixin], + changeValue: function (event) { + this.setValue(event.target.value); + }, + render: function () { + return + } + }); + var TestForm = React.createClass({ + customValidationA: function (values, value) { + return value === 'foo'; + }, + customValidationB: function (values, value) { + return value === 'foo' && values.A === 'foo'; + }, + render: function () { + return ( + + + + + ); + } + }); + var form = TestUtils.renderIntoDocument( + + ); + + var inputComponent = TestUtils.scryRenderedComponentsWithType(form, TestInput); + expect(inputComponent[0].isValid()).toBe(true); + expect(inputComponent[1].isValid()).toBe(true); + var input = TestUtils.scryRenderedDOMComponentsWithTag(form, 'INPUT'); + TestUtils.Simulate.change(input[0], {target: {value: 'bar'}}); + expect(inputComponent[0].isValid()).toBe(false); + expect(inputComponent[1].isValid()).toBe(false); + }); + + it('should override all error messages with error messages passed by form', function () { + var TestInput = React.createClass({ + mixins: [Formsy.Mixin], + render: function () { + return + } + }); + var TestForm = React.createClass({ + render: function () { + return ( + + + + ); + } + }); + var form = TestUtils.renderIntoDocument( + + ); + + var inputComponent = TestUtils.findRenderedComponentWithType(form, TestInput); + expect(inputComponent.getErrorMessage()).toBe('bar'); + }); + + it('should override validation rules with required rules', function () { + var TestInput = React.createClass({ + mixins: [Formsy.Mixin], + render: function () { + return + } + }); + var TestForm = React.createClass({ + render: function () { + return ( + + + + ); + } + }); + var form = TestUtils.renderIntoDocument( + + ); + + var inputComponent = TestUtils.findRenderedComponentWithType(form, TestInput); + expect(inputComponent.getErrorMessage()).toBe('bar3'); + }); + + it('should fall back to default error message when non exist in validationErrors map', function () { + var TestInput = React.createClass({ + mixins: [Formsy.Mixin], + render: function () { + return + } + }); + var TestForm = React.createClass({ + render: function () { + return ( + + + + ); + } + }); + var form = TestUtils.renderIntoDocument( + + ); + + var inputComponent = TestUtils.findRenderedComponentWithType(form, TestInput); + expect(inputComponent.getErrorMessage()).toBe('bar'); + }); + }); diff --git a/specs/Formsy-spec.js b/specs/Formsy-spec.js index c0aa327..550c7f9 100755 --- a/specs/Formsy-spec.js +++ b/specs/Formsy-spec.js @@ -82,12 +82,9 @@ describe('Formsy', function () { // Wait before adding the input setTimeout(function () { - inputs.push(TestInput({ - name: 'test' - })); + inputs.push(); forceUpdate(function () { - // Wait for next event loop, as that does the form setTimeout(function () { TestUtils.Simulate.submit(form.getDOMNode()); @@ -136,9 +133,7 @@ describe('Formsy', function () { // Wait before adding the input setTimeout(function () { - inputs.push(TestInput({ - name: 'test' - })); + inputs.push(); forceUpdate(function () { @@ -257,7 +252,7 @@ describe('Formsy', function () { var form = TestUtils.renderIntoDocument(); var input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); TestUtils.Simulate.change(input.getDOMNode(), {target: {value: 'bar'}}); - expect(CheckValid).toHaveBeenCalledWith('bar'); + expect(CheckValid).toHaveBeenCalledWith({one: 'bar'}, 'bar', true); expect(OtherCheckValid).not.toHaveBeenCalled(); }); @@ -266,7 +261,7 @@ describe('Formsy', function () { form.setProps({inputs: [{name: 'one', validations: 'OtherCheckValid', value: 'foo'}] }); var input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); TestUtils.Simulate.change(input.getDOMNode(), {target: {value: 'bar'}}); - expect(OtherCheckValid).toHaveBeenCalledWith('bar'); + expect(OtherCheckValid).toHaveBeenCalledWith({one: 'bar'}, 'bar', true); }); it('should invalidate a form if dynamically inserted input is invalid', function(done) { @@ -301,8 +296,8 @@ describe('Formsy', function () { var form = TestUtils.renderIntoDocument(); var input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); TestUtils.Simulate.change(input.getDOMNode(), {target: {value: 'bar'}}); - expect(CheckValid).toHaveBeenCalledWith('bar'); - expect(OtherCheckValid).toHaveBeenCalledWith('bar'); + expect(CheckValid).toHaveBeenCalledWith({one: 'bar'}, 'bar', true); + expect(OtherCheckValid).toHaveBeenCalledWith({one: 'bar'}, 'bar', true); }); }); @@ -380,9 +375,7 @@ describe('Formsy', function () { ); // Wait before adding the input - inputs.push(TestInput({ - name: 'test' - })); + inputs.push(); forceUpdate(function () { diff --git a/specs/Submit-spec.js b/specs/Submit-spec.js deleted file mode 100644 index 6820972..0000000 --- a/specs/Submit-spec.js +++ /dev/null @@ -1,178 +0,0 @@ -var Formsy = require('./../src/main.js'); - -describe('Ajax', function() { - - beforeEach(function () { - jasmine.Ajax.install(); - }); - - afterEach(function () { - jasmine.Ajax.uninstall(); - }); - - it('should post to a given url if passed', function () { - - var form = TestUtils.renderIntoDocument( - - - ); - - TestUtils.Simulate.submit(form.getDOMNode()); - expect(jasmine.Ajax.requests.mostRecent().url).toBe('/users'); - expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST'); - - }); - - it('should put to a given url if passed a method attribute', function () { - - var form = TestUtils.renderIntoDocument( - - - ); - - TestUtils.Simulate.submit(form.getDOMNode()); - expect(jasmine.Ajax.requests.mostRecent().url).toBe('/users'); - expect(jasmine.Ajax.requests.mostRecent().method).toBe('PUT'); - - }); - - it('should pass x-www-form-urlencoded as contentType when urlencoded is set as contentType', function () { - - var form = TestUtils.renderIntoDocument( - - - ); - - TestUtils.Simulate.submit(form.getDOMNode()); - expect(jasmine.Ajax.requests.mostRecent().contentType()).toBe('application/x-www-form-urlencoded'); - - }); - - it('should run an onSuccess handler, if passed and ajax is successfull. First argument is data from server', function (done) { - - var onSuccess = jasmine.createSpy("success"); - var form = TestUtils.renderIntoDocument( - - - ); - - jasmine.Ajax.stubRequest('/users').andReturn({ - status: 200, - contentType: 'application/json', - responseText: '{}' - }); - - TestUtils.Simulate.submit(form.getDOMNode()); - - // Since ajax is returned as a promise (async), move assertion - // to end of event loop - setTimeout(function () { - expect(onSuccess).toHaveBeenCalledWith({}); - done(); - }, 0); - - }); - - it('should not do ajax request if onSubmit handler is passed, but pass the model as first argument to onSubmit handler', function () { - - var TestInput = React.createClass({ - mixins: [Formsy.Mixin], - render: function () { - return - } - }); - var form = TestUtils.renderIntoDocument( - - - - ); - - TestUtils.Simulate.submit(form.getDOMNode()); - - expect(jasmine.Ajax.requests.count()).toBe(0); - - function onSubmit (data) { - expect(data).toEqual({ - foo: 'bar' - }); - } - - }); - - it('should trigger an onSubmitted handler, if passed and the submit has responded with SUCCESS', function (done) { - - var onSubmitted = jasmine.createSpy("submitted"); - var form = TestUtils.renderIntoDocument( - - - ); - - jasmine.Ajax.stubRequest('/users').andReturn({ - status: 200, - contentType: 'application/json', - responseText: '{}' - }); - - TestUtils.Simulate.submit(form.getDOMNode()); - - // Since ajax is returned as a promise (async), move assertion - // to end of event loop - setTimeout(function () { - expect(onSubmitted).toHaveBeenCalled(); - done(); - }, 0); - - }); - - it('should trigger an onSubmitted handler, if passed and the submit has responded with ERROR', function (done) { - - var onSubmitted = jasmine.createSpy("submitted"); - var form = TestUtils.renderIntoDocument( - - - ); - - jasmine.Ajax.stubRequest('/users').andReturn({ - status: 500, - contentType: 'application/json', - responseText: '{}' - }); - - TestUtils.Simulate.submit(form.getDOMNode()); - - // Since ajax is returned as a promise (async), move assertion - // to end of event loop - setTimeout(function () { - expect(onSubmitted).toHaveBeenCalled(); - done(); - }, 0); - - }); - - it('should trigger an onError handler, if passed and the submit has responded with ERROR', function (done) { - - var onError = jasmine.createSpy("error"); - var form = TestUtils.renderIntoDocument( - - - ); - - // Do not return any error because there are no inputs - jasmine.Ajax.stubRequest('/users').andReturn({ - status: 500, - contentType: 'application/json', - responseText: '{}' - }); - - TestUtils.Simulate.submit(form.getDOMNode()); - - // Since ajax is returned as a promise (async), move assertion - // to end of event loop - setTimeout(function () { - expect(onError).toHaveBeenCalledWith({}); - done(); - }, 0); - - }); - -}); diff --git a/src/Mixin.js b/src/Mixin.js index 1ad66e6..5641ee2 100644 --- a/src/Mixin.js +++ b/src/Mixin.js @@ -1,11 +1,45 @@ +var convertValidationsToObject = function (validations) { + + if (typeof validations === 'string') { + + return validations.split(/\,(?![^{\[]*[}\]])/g).reduce(function (validations, 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 + } + }); + + if (args.length > 1) { + throw new Error('Formsy does not support multiple args on string validations. Use object format of validations instead.'); + } + validations[validateMethod] = args[0] || true; + return validations; + }, {}); + + } + + return validations || {}; + +}; module.exports = { getInitialState: function () { - var value = 'value' in this.props ? this.props.value : ''; return { - _value: value, + _value: this.props.value, + _isRequired: false, _isValid: true, _isPristine: true, - _pristineValue: value + _pristineValue: this.props.value, + _validationError: '' + }; + }, + getDefaultProps: function () { + return { + validationError: '', + validationErrors: {} }; }, componentWillMount: function () { @@ -46,25 +80,15 @@ module.exports = { var isValueChanged = function () { - return ( - this.props.value !== prevProps.value && ( - this.state._value === prevProps.value || - - // Since undefined is converted to empty string we have to - // check that specifically - (this.state._value === '' && prevProps.value === undefined) - ) - ); + return this.props.value !== prevProps.value && this.state._value === prevProps.value; }.bind(this); // If validations has changed or something outside changes // the value, set the value again running a validation - if (prevProps.validations !== this.props.validations || isValueChanged()) { - var value = 'value' in this.props ? this.props.value : ''; - this.setValue(value); + this.setValue(this.props.value); } }, @@ -76,12 +100,8 @@ module.exports = { setValidations: function (validations, required) { // Add validations to the store itself as the props object can not be modified - this._validations = validations || ''; - - if (required) { - this._validations = validations ? validations + ',' : ''; - this._validations += 'isValue'; - } + this._validations = convertValidationsToObject(validations) || {}; + this._requiredValidations = required === true ? {isDefaultRequiredValue: true} : convertValidationsToObject(required); }, @@ -109,7 +129,7 @@ module.exports = { return this.state._value !== ''; }, getErrorMessage: function () { - return this.isValid() || this.showRequired() ? null : this.state._serverError || this.props.validationError; + return !this.isValid() || this.showRequired() ? this.state._validationError : null; }, isFormDisabled: function () { return this.props._isFormDisabled(); @@ -121,13 +141,13 @@ module.exports = { return this.state._isPristine; }, isRequired: function () { - return !!this.props.required; + return this.state._isRequired; }, showRequired: function () { - return this.isRequired() && this.state._value === ''; + return this.isRequired(); }, showError: function () { - return !this.showRequired() && !this.state._isValid; + return !this.showRequired() && !this.isValid(); }, isValidValue: function (value) { return this.props._isValidValue.call(null, this, value); diff --git a/src/main.js b/src/main.js index fa1f98a..325aee9 100644 --- a/src/main.js +++ b/src/main.js @@ -25,7 +25,6 @@ Formsy.Form = React.createClass({ }, getDefaultProps: function () { return { - headers: {}, onSuccess: function () {}, onError: function () {}, onSubmit: function () {}, @@ -34,7 +33,8 @@ Formsy.Form = React.createClass({ onSubmitted: function () {}, onValid: function () {}, onInvalid: function () {}, - onChange: function () {} + onChange: function () {}, + validationErrors: null }; }, @@ -86,19 +86,24 @@ Formsy.Form = React.createClass({ // 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); - + // 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) { - return; + this.updateModel(); + var model = this.mapModel(); + this.props.onSubmit(model, this.resetModel, this.updateInputsWithError); + this.state.isValid ? this.props.onValidSubmit(model, this.resetModel) : this.props.onInvalidSubmit(model, this.resetModel); + 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 && utils.ajax[this.props.method.toLowerCase()] ? this.props.method.toLowerCase() : 'post'; @@ -181,7 +186,9 @@ Formsy.Form = React.createClass({ child.props._detachFromForm = this.detachFromForm; child.props._validate = this.validate; child.props._isFormDisabled = this.isFormDisabled; - child.props._isValidValue = this.runValidation; + child.props._isValidValue = function (component, value) { + return this.runValidation(component, value).isValid; + }.bind(this); } if (child && child.props && child.props.children) { @@ -227,18 +234,13 @@ Formsy.Form = React.createClass({ this.props.onChange(this.getCurrentValues()); } - var isValid = true; - if (component.validate && typeof component.validate === 'function') { - isValid = component.validate(); - } else if (component.props.required || component._validations) { - isValid = this.runValidation(component); - } - + 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: isValid, - _serverError: null + _isValid: validation.isValid, + _isRequired: validation.isRequired, + _validationError: validation.error }, this.validateForm); }, @@ -246,33 +248,78 @@ Formsy.Form = React.createClass({ // Checks validation on current value or a passed value runValidation: function (component, value) { - var isValid = true; + + var currentValues = this.getCurrentValues(); + var validationErrors = component.props.validationErrors; + var validationError = component.props.validationError; value = arguments.length === 2 ? value : component.state._value; - if (component._validations.length) { - component._validations.split(/\,(?![^{\[]*[}\]])/g).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 = [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)); - } + + 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") { - // the component defines an explicit validate function - isValid = component.validate() + validationResults.failed = component.validate() ? [] : ['failed']; } - return isValid; + + 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: isValid, + error: (function () { + + if (isValid && !isRequired) { + return ''; + } + + if (this.props.validationErrors && this.props.validationErrors[component.props.name]) { + return this.props.validationErrors[component.props.name]; + } + + if (isRequired) { + return validationErrors[requiredResults.success[0]] || null; + } + + if (!isValid) { + return validationErrors[validationResults.failed[0]] || validationError; + } + + }.call(this)) + }; + + }, + + runRules: function (value, currentValues, validations) { + + var results = { + 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' && !validations[validationMethod](currentValues, value)) { + return results.failed.push(validationMethod); + } else if (typeof validations[validationMethod] !== 'function' && !validationRules[validationMethod](currentValues, value, validations[validationMethod])) { + return results.failed.push(validationMethod); + } + + return results.success.push(validationMethod); + + }); + } + + return results; + }, // Validate the form by going through all child input components @@ -312,10 +359,11 @@ Formsy.Form = React.createClass({ // last component validated will run the onValidationComplete callback inputKeys.forEach(function (name, index) { var component = inputs[name]; - var isValid = this.runValidation(component); + var validation = this.runValidation(component); component.setState({ - _isValid: isValid, - _serverError: null + _isValid: validation.isValid, + _isRequired: validation.isRequired, + _validationError: validation.error }, index === inputKeys.length - 1 ? onValidationComplete : null); }.bind(this)); diff --git a/src/utils.js b/src/utils.js index cfd2db0..1b26f1b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,60 +1,3 @@ -var csrfTokenSelector = typeof document != 'undefined' ? document.querySelector('meta[name="csrf-token"]') : null; - -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); - - if (!!csrfTokenSelector && !!csrfTokenSelector.content) { - xhr.setRequestHeader('X-CSRF-Token', csrfTokenSelector.content); - } - - // 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); - } - }); -}; - module.exports = { arraysDiffer: function (arrayA, arrayB) { var isDifferent = false; @@ -68,9 +11,5 @@ module.exports = { }); } return isDifferent; - }, - ajax: { - post: request.bind(null, 'POST'), - put: request.bind(null, 'PUT') } }; diff --git a/src/validationRules.js b/src/validationRules.js index c0b62a5..c5bad87 100644 --- a/src/validationRules.js +++ b/src/validationRules.js @@ -1,44 +1,62 @@ module.exports = { - 'isValue': function (value) { - return value !== ''; + 'isDefaultRequiredValue': function (values, value) { + return value === undefined || value === ''; }, - 'isEmail': function (value) { - return value.match(/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i); + 'hasValue': function (values, value) { + return value !== undefined; }, - 'isTrue': function (value) { + 'matchRegexp': function (values, value, regexp) { + return value !== undefined && !!value.match(regexp); + }, + 'isUndefined': function (values, value) { + return value === undefined; + }, + 'isEmptyString': function (values, value) { + return value === ''; + }, + 'isEmail': function (values, value) { + return value !== undefined && value.match(/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i); + }, + 'isTrue': function (values, value) { return value === true; }, - 'isNumeric': function (value) { + 'isFalse': function (values, value) { + return value === false; + }, + 'isNumeric': function (values, value) { if (typeof value === 'number') { return true; } else { - var matchResults = value.match(/[-+]?(\d*[.])?\d+/); - if (!! matchResults) { + var matchResults = value !== undefined && value.match(/[-+]?(\d*[.])?\d+/); + if (!!matchResults) { return matchResults[0] == value; } else { return false; } } }, - 'isAlpha': function (value) { - return value.match(/^[a-zA-Z]+$/); + 'isAlpha': function (values, value) { + return value !== undefined && value.match(/^[a-zA-Z]+$/); }, - 'isWords': function (value) { - return value.match(/^[a-zA-Z\s]+$/); + 'isWords': function (values, value) { + return value !== undefined && value.match(/^[a-zA-Z\s]+$/); }, - 'isSpecialWords': function (value) { - return value.match(/^[a-zA-Z\s\u00C0-\u017F]+$/); + 'isSpecialWords': function (values, value) { + return value !== undefined && value.match(/^[a-zA-Z\s\u00C0-\u017F]+$/); }, - isLength: function (value, min, max) { - if (max !== undefined) { - return value.length >= min && value.length <= max; - } - return value.length >= min; + isLength: function (values, value, length) { + return value !== undefined && value.length === length; }, - equals: function (value, eql) { + equals: function (values, value, eql) { return value == eql; }, - equalsField: function (value, field) { + equalsField: function (values, value, field) { return value == this[field]; + }, + maxLength: function (values, value, length) { + return value !== undefined && value.length <= length; + }, + minLength: function (values, value, length) { + return value !== undefined && value.length >= length; } };