From 4267c40f3b764336463cc936e824b157c7d3eeea Mon Sep 17 00:00:00 2001 From: christianalfoni Date: Fri, 17 Apr 2015 19:36:28 +0200 Subject: [PATCH] Fixed validation errors using callback --- bower.json | 2 +- package.json | 2 +- release/formsy-react.js | 23 ++++++++---- release/formsy-react.min.js | 2 +- specs/Validation-spec.js | 73 +++++++++++++++++++++++++++++++++++++ src/Mixin.js | 5 ++- src/main.js | 18 ++++++--- 7 files changed, 106 insertions(+), 19 deletions(-) diff --git a/bower.json b/bower.json index 29e9db1..12a1bef 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "formsy-react", - "version": "0.12.4", + "version": "0.12.5", "main": "src/main.js", "dependencies": { "react": "^0.13.1" diff --git a/package.json b/package.json index 14f3777..6ccf9b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "formsy-react", - "version": "0.12.4", + "version": "0.12.5", "description": "A form input builder and validator for React JS", "repository": { "type": "git", diff --git a/release/formsy-react.js b/release/formsy-react.js index 28ff527..8193293 100644 --- a/release/formsy-react.js +++ b/release/formsy-react.js @@ -78,7 +78,8 @@ Formsy.Form = React.createClass({displayName: "Form", // Update model, submit to url prop and send the model submit: function (event) { - event.preventDefault(); + + event && event.preventDefault(); // Trigger form as not pristine. // If any inputs have not been touched yet this will make them dirty @@ -117,7 +118,7 @@ Formsy.Form = React.createClass({displayName: "Form", var component = this.inputs[name]; var args = [{ _isValid: !(name in errors), - _serverError: errors[name] + _validationError: errors[name] }]; component.setState.apply(component, args); }.bind(this)); @@ -136,7 +137,7 @@ Formsy.Form = React.createClass({displayName: "Form", var args = [{ _isValid: false, - _validationError: errors[name] + _externalError: errors[name] }]; component.setState.apply(component, args); }.bind(this)); @@ -217,7 +218,8 @@ Formsy.Form = React.createClass({displayName: "Form", component.setState({ _isValid: validation.isValid, _isRequired: validation.isRequired, - _validationError: validation.error + _validationError: validation.error, + _externalError: null }, this.validateForm); }, @@ -225,7 +227,6 @@ Formsy.Form = React.createClass({displayName: "Form", // Checks validation on current value or a passed value runValidation: function (component, value) { - var currentValues = this.getCurrentValues(); var validationErrors = component.props.validationErrors; var validationError = component.props.validationError; @@ -241,6 +242,7 @@ Formsy.Form = React.createClass({displayName: "Form", var isRequired = Object.keys(component._requiredValidations).length ? !!requiredResults.success.length : false; var isValid = !validationResults.failed.length && !(this.props.validationErrors && this.props.validationErrors[component.props.name]); + return { isRequired: isRequired, isValid: isRequired ? false : isValid, @@ -360,10 +362,14 @@ Formsy.Form = React.createClass({displayName: "Form", inputKeys.forEach(function (name, index) { var component = inputs[name]; var validation = this.runValidation(component); + if (validation.isValid && component.state._externalError) { + validation.isValid = false; + } component.setState({ _isValid: validation.isValid, _isRequired: validation.isRequired, - _validationError: validation.error + _validationError: validation.error, + _externalError: !validation.isValid && component.state._externalError ? component.state._externalError : null }, index === inputKeys.length - 1 ? onValidationComplete : null); }.bind(this)); @@ -447,7 +453,8 @@ module.exports = { _isValid: true, _isPristine: true, _pristineValue: this.props.value, - _validationError: '' + _validationError: '', + _externalError: null }; }, getDefaultProps: function () { @@ -538,7 +545,7 @@ module.exports = { return this.state._value !== ''; }, getErrorMessage: function () { - return !this.isValid() || this.showRequired() ? this.state._validationError : null; + return !this.isValid() || this.showRequired() ? (this.state._externalError || this.state._validationError) : null; }, isFormDisabled: function () { return this.props._isFormDisabled(); diff --git a/release/formsy-react.min.js b/release/formsy-react.min.js index 39f3eb2..9e868dc 100644 --- a/release/formsy-react.min.js +++ b/release/formsy-react.min.js @@ -1 +1 @@ -!function t(i,n,e){function r(o,u){if(!n[o]){if(!i[o]){var a="function"==typeof require&&require;if(!u&&a)return a(o,!0);if(s)return s(o,!0);var l=new Error("Cannot find module '"+o+"'");throw l.code="MODULE_NOT_FOUND",l}var d=n[o]={exports:{}};i[o][0].call(d.exports,function(t){var n=i[o][1][t];return r(n?n:t)},d,d.exports,t,i,n,e)}return n[o].exports}for(var s="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[e]=n[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){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);i()&&this.setValue(this.props.value)},componentWillUnmount:function(){this.props._detachFromForm(this)},setValidations:function(t,i){this._validations=n(t)||{},this._requiredValidations=i===!0?{isDefaultRequiredValue:!0}:n(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.props.required},showRequired:function(){return this.state._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 n=!1;return t.length!==i.length?n=!0:t.forEach(function(t,e){t!==i[e]&&(n=!0)}),n}}},{}],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,n){return void 0!==i&&!!i.match(n)},isUndefined:function(t,i){return void 0===i},isEmptyString:function(t,i){return""===i},isEmail:function(t,i){return!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 n=void 0!==i&&i.match(/[-+]?(\d*[.])?\d+/);return n?n[0]==i:!1},isAlpha:function(t,i){return!i||i.match(/^[a-zA-Z]+$/)},isWords:function(t,i){return!i||i.match(/^[a-zA-Z\s]+$/)},isSpecialWords:function(t,i){return!i||i.match(/^[a-zA-Z\s\u00C0-\u017F]+$/)},isLength:function(t,i,n){return void 0!==i&&i.length===n},equals:function(t,i,n){return i==n},equalsField:function(t,i,n){return i==t[n]},maxLength:function(t,i,n){return void 0!==i&&i.length<=n},minLength:function(t,i,n){return void 0!==i&&i.length>=n}}},{}]},{},[1]); \ No newline at end of file +!function t(i,n,e){function r(o,u){if(!n[o]){if(!i[o]){var a="function"==typeof require&&require;if(!u&&a)return a(o,!0);if(s)return s(o,!0);var l=new Error("Cannot find module '"+o+"'");throw l.code="MODULE_NOT_FOUND",l}var d=n[o]={exports:{}};i[o][0].call(d.exports,function(t){var n=i[o][1][t];return r(n?n:t)},d,d.exports,t,i,n,e)}return n[o].exports}for(var s="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[e]=n[0]||!0,t},{}):t||{}};i.exports={getInitialState:function(){return{_value:this.props.value,_isRequired:!1,_isValid:!0,_isPristine:!0,_pristineValue:this.props.value,_validationError:"",_externalError:null}},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){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);i()&&this.setValue(this.props.value)},componentWillUnmount:function(){this.props._detachFromForm(this)},setValidations:function(t,i){this._validations=n(t)||{},this._requiredValidations=i===!0?{isDefaultRequiredValue:!0}:n(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._externalError||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.props.required},showRequired:function(){return this.state._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 n=!1;return t.length!==i.length?n=!0:t.forEach(function(t,e){t!==i[e]&&(n=!0)}),n}}},{}],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,n){return void 0!==i&&!!i.match(n)},isUndefined:function(t,i){return void 0===i},isEmptyString:function(t,i){return""===i},isEmail:function(t,i){return!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 n=void 0!==i&&i.match(/[-+]?(\d*[.])?\d+/);return n?n[0]==i:!1},isAlpha:function(t,i){return!i||i.match(/^[a-zA-Z]+$/)},isWords:function(t,i){return!i||i.match(/^[a-zA-Z\s]+$/)},isSpecialWords:function(t,i){return!i||i.match(/^[a-zA-Z\s\u00C0-\u017F]+$/)},isLength:function(t,i,n){return void 0!==i&&i.length===n},equals:function(t,i,n){return i==n},equalsField:function(t,i,n){return i==t[n]},maxLength:function(t,i,n){return void 0!==i&&i.length<=n},minLength:function(t,i,n){return void 0!==i&&i.length>=n}}},{}]},{},[1]); \ No newline at end of file diff --git a/specs/Validation-spec.js b/specs/Validation-spec.js index a6b957b..aefb9b2 100644 --- a/specs/Validation-spec.js +++ b/specs/Validation-spec.js @@ -2,6 +2,79 @@ var Formsy = require('./../src/main.js'); describe('Validation', function() { + it('should reset only changed form element when external error is passed', function (done) { + + var onSubmit = function (model, reset, invalidate) { + invalidate({ + foo: 'bar', + bar: 'foo' + }); + } + var TestInput = React.createClass({ + mixins: [Formsy.Mixin], + updateValue: function (event) { + this.setValue(event.target.value); + }, + render: function () { + return + } + }); + var form = TestUtils.renderIntoDocument( + + + + + ); + + var input = TestUtils.scryRenderedDOMComponentsWithTag(form, 'INPUT')[0]; + var inputComponents = TestUtils.scryRenderedComponentsWithType(form, TestInput); + + form.submit(); + expect(inputComponents[0].isValid()).toBe(false); + expect(inputComponents[1].isValid()).toBe(false); + TestUtils.Simulate.change(input, {target: {value: 'bar'}}); + setTimeout(function () { + expect(inputComponents[0].isValid()).toBe(true); + expect(inputComponents[1].isValid()).toBe(false); + done(); + }, 0); + }); + + it('should let normal validation take over when component with external error is changed', function (done) { + + var onSubmit = function (model, reset, invalidate) { + invalidate({ + foo: 'bar' + }); + } + var TestInput = React.createClass({ + mixins: [Formsy.Mixin], + updateValue: function (event) { + this.setValue(event.target.value); + }, + render: function () { + return + } + }); + var form = TestUtils.renderIntoDocument( + + + + ); + + var input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); + var inputComponent = TestUtils.findRenderedComponentWithType(form, TestInput); + + form.submit(); + expect(inputComponent.isValid()).toBe(false); + TestUtils.Simulate.change(input, {target: {value: 'bar'}}); + setTimeout(function () { + expect(inputComponent.getValue()).toBe('bar'); + expect(inputComponent.isValid()).toBe(false); + done(); + }, 0); + }); + it('should trigger an onValid handler, if passed, when form is valid', function () { var onValid = jasmine.createSpy('valid'); diff --git a/src/Mixin.js b/src/Mixin.js index 1dc6334..ff8a059 100644 --- a/src/Mixin.js +++ b/src/Mixin.js @@ -33,7 +33,8 @@ module.exports = { _isValid: true, _isPristine: true, _pristineValue: this.props.value, - _validationError: '' + _validationError: '', + _externalError: null }; }, getDefaultProps: function () { @@ -124,7 +125,7 @@ module.exports = { return this.state._value !== ''; }, getErrorMessage: function () { - return !this.isValid() || this.showRequired() ? this.state._validationError : null; + return !this.isValid() || this.showRequired() ? (this.state._externalError || this.state._validationError) : null; }, isFormDisabled: function () { return this.props._isFormDisabled(); diff --git a/src/main.js b/src/main.js index 6dd9f19..cd95804 100644 --- a/src/main.js +++ b/src/main.js @@ -76,7 +76,8 @@ Formsy.Form = React.createClass({ // Update model, submit to url prop and send the model submit: function (event) { - event.preventDefault(); + + event && event.preventDefault(); // Trigger form as not pristine. // If any inputs have not been touched yet this will make them dirty @@ -115,7 +116,7 @@ Formsy.Form = React.createClass({ var component = this.inputs[name]; var args = [{ _isValid: !(name in errors), - _serverError: errors[name] + _validationError: errors[name] }]; component.setState.apply(component, args); }.bind(this)); @@ -134,7 +135,7 @@ Formsy.Form = React.createClass({ var args = [{ _isValid: false, - _validationError: errors[name] + _externalError: errors[name] }]; component.setState.apply(component, args); }.bind(this)); @@ -215,7 +216,8 @@ Formsy.Form = React.createClass({ component.setState({ _isValid: validation.isValid, _isRequired: validation.isRequired, - _validationError: validation.error + _validationError: validation.error, + _externalError: null }, this.validateForm); }, @@ -223,7 +225,6 @@ Formsy.Form = React.createClass({ // Checks validation on current value or a passed value runValidation: function (component, value) { - var currentValues = this.getCurrentValues(); var validationErrors = component.props.validationErrors; var validationError = component.props.validationError; @@ -239,6 +240,7 @@ Formsy.Form = React.createClass({ var isRequired = Object.keys(component._requiredValidations).length ? !!requiredResults.success.length : false; var isValid = !validationResults.failed.length && !(this.props.validationErrors && this.props.validationErrors[component.props.name]); + return { isRequired: isRequired, isValid: isRequired ? false : isValid, @@ -358,10 +360,14 @@ Formsy.Form = React.createClass({ inputKeys.forEach(function (name, index) { var component = inputs[name]; var validation = this.runValidation(component); + if (validation.isValid && component.state._externalError) { + validation.isValid = false; + } component.setState({ _isValid: validation.isValid, _isRequired: validation.isRequired, - _validationError: validation.error + _validationError: validation.error, + _externalError: !validation.isValid && component.state._externalError ? component.state._externalError : null }, index === inputKeys.length - 1 ? onValidationComplete : null); }.bind(this));