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;
}
};