diff --git a/bower.json b/bower.json index 29e9db1..039aceb 100644 --- a/bower.json +++ b/bower.json @@ -1,8 +1,25 @@ { "name": "formsy-react", - "version": "0.12.4", + "version": "0.12.5", + "description": "A form input builder and validator for React JS", + "repository": { + "type": "git", + "url": "https://github.com/christianalfoni/formsy-react.git" + }, "main": "src/main.js", + "license": "MIT", + "ignore": [ + "build/", + "Gulpfile.js" + ], "dependencies": { "react": "^0.13.1" - } + }, + "keywords": [ + "react", + "form", + "forms", + "validation", + "react-component" + ] } diff --git a/package.json b/package.json index 14f3777..619681d 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", @@ -13,6 +13,13 @@ }, "author": "Christian Alfoni", "license": "MIT", + "keywords": [ + "react", + "form", + "forms", + "validation", + "react-component" + ], "devDependencies": { "browserify": "^6.2.0", "glob": "^4.0.6", 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/Rules-equals-spec.js b/specs/Rules-equals-spec.js new file mode 100644 index 0000000..b4b72db --- /dev/null +++ b/specs/Rules-equals-spec.js @@ -0,0 +1,60 @@ +var Formsy = require('./../src/main.js'); + +describe('Rules: equals', function() { + var TestInput, isValid, form, input; + + beforeEach(function() { + isValid = jasmine.createSpy('valid'); + + TestInput = React.createClass({ + mixins: [Formsy.Mixin], + updateValue: function (event) { + this.setValue(event.target.value); + }, + render: function () { + if (this.isValid()) { + isValid(); + } + return + } + }); + + form = TestUtils.renderIntoDocument( + + + + ); + + input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); + + }); + + afterEach(function() { + TestInput = isValid = isInvalid = form = null; + }); + + it('should fail with undefined', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: undefined}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail with null', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: null}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail when the value is not equal', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 'foo'}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should pass when the value is equal', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 'myValue'}}); + expect(isValid).toHaveBeenCalled(); + }); + +}); \ No newline at end of file diff --git a/specs/Rules-hasValue-spec.js b/specs/Rules-hasValue-spec.js new file mode 100644 index 0000000..7cd71c9 --- /dev/null +++ b/specs/Rules-hasValue-spec.js @@ -0,0 +1,54 @@ +var Formsy = require('./../src/main.js'); + +describe('Rules: hasValue', function() { + var TestInput, isValid, form, input; + + beforeEach(function() { + isValid = jasmine.createSpy('valid'); + + TestInput = React.createClass({ + mixins: [Formsy.Mixin], + updateValue: function (event) { + this.setValue(event.target.value); + }, + render: function () { + if (this.isValid()) { + isValid(); + } + return + } + }); + + form = TestUtils.renderIntoDocument( + + + + ); + + input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); + + }); + + afterEach(function() { + TestInput = isValid = isInvalid = form = null; + }); + + it('should fail with undefined', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: undefined}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail with null', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: null}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should pass with a string', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 'myValue'}}); + expect(isValid).toHaveBeenCalled(); + }); + +}); \ No newline at end of file diff --git a/specs/Rules-isAlpha-spec.js b/specs/Rules-isAlpha-spec.js new file mode 100644 index 0000000..7b71173 --- /dev/null +++ b/specs/Rules-isAlpha-spec.js @@ -0,0 +1,60 @@ +var Formsy = require('./../src/main.js'); + +describe('Rules: isAlpha', function() { + var TestInput, isValid, form, input; + + beforeEach(function() { + isValid = jasmine.createSpy('valid'); + + TestInput = React.createClass({ + mixins: [Formsy.Mixin], + updateValue: function (event) { + this.setValue(event.target.value); + }, + render: function () { + if (this.isValid()) { + isValid(); + } + return + } + }); + + form = TestUtils.renderIntoDocument( + + + + ); + + input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); + + }); + + afterEach(function() { + TestInput = isValid = isInvalid = form = null; + }); + + it('should fail with undefined', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: undefined}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail with null', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: null}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail with a number', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 123}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should pass with a string', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 'myValue'}}); + expect(isValid).toHaveBeenCalled(); + }); + +}); \ No newline at end of file diff --git a/specs/Rules-isEmail-spec.js b/specs/Rules-isEmail-spec.js new file mode 100644 index 0000000..4c75cd0 --- /dev/null +++ b/specs/Rules-isEmail-spec.js @@ -0,0 +1,48 @@ +var Formsy = require('./../src/main.js'); + +describe('Rules: isEmail', function() { + var TestInput, isValid, form, input; + + beforeEach(function() { + isValid = jasmine.createSpy('valid'); + + TestInput = React.createClass({ + mixins: [Formsy.Mixin], + updateValue: function (event) { + this.setValue(event.target.value); + }, + render: function () { + if (this.isValid()) { + isValid(); + } + return + } + }); + + form = TestUtils.renderIntoDocument( + + + + ); + + input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); + + }); + + afterEach(function() { + TestInput = isValid = isInvalid = form = null; + }); + + it('should fail with "foo"', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 'foo'}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should pass with "foo@foo.com"', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 'foo@foo.com'}}); + expect(isValid).toHaveBeenCalled(); + }); + +}); \ No newline at end of file diff --git a/specs/Rules-isLength-spec.js b/specs/Rules-isLength-spec.js new file mode 100644 index 0000000..e456908 --- /dev/null +++ b/specs/Rules-isLength-spec.js @@ -0,0 +1,72 @@ +var Formsy = require('./../src/main.js'); + +describe('Rules: isLength', function() { + var TestInput, isValid, form, input; + + beforeEach(function() { + isValid = jasmine.createSpy('valid'); + + TestInput = React.createClass({ + mixins: [Formsy.Mixin], + updateValue: function (event) { + this.setValue(event.target.value); + }, + render: function () { + if (this.isValid()) { + isValid(); + } + return + } + }); + + form = TestUtils.renderIntoDocument( + + + + ); + + input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); + + }); + + afterEach(function() { + TestInput = isValid = isInvalid = form = null; + }); + + it('should fail with undefined', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: undefined}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail with null', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: null}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail with a number', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 123}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail with a string too small', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: "hi"}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail with a string too long', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: "foo bar"}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should pass with the right length', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 'sup'}}); + expect(isValid).toHaveBeenCalled(); + }); + +}); \ No newline at end of file diff --git a/specs/Rules-isNumeric-spec.js b/specs/Rules-isNumeric-spec.js new file mode 100644 index 0000000..8048792 --- /dev/null +++ b/specs/Rules-isNumeric-spec.js @@ -0,0 +1,72 @@ +var Formsy = require('./../src/main.js'); + +describe('Rules: isNumeric', function() { + var TestInput, isValid, form, input; + + beforeEach(function() { + isValid = jasmine.createSpy('valid'); + + TestInput = React.createClass({ + mixins: [Formsy.Mixin], + updateValue: function (event) { + this.setValue(event.target.value); + }, + render: function () { + if (this.isValid()) { + isValid(); + } + return + } + }); + + form = TestUtils.renderIntoDocument( + + + + ); + + input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); + + }); + + afterEach(function() { + TestInput = isValid = isInvalid = form = null; + }); + + it('should fail with undefined', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: undefined}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail with null', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: null}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail with a string', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 'myValue'}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should pass with a number as string', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: '123'}}); + expect(isValid).toHaveBeenCalled(); + }); + + it('should pass with an int', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 123}}); + expect(isValid).toHaveBeenCalled(); + }); + + it('should pass with a float', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 1.23}}); + expect(isValid).toHaveBeenCalled(); + }); + +}); \ No newline at end of file diff --git a/specs/Rules-isWords-spec.js b/specs/Rules-isWords-spec.js new file mode 100644 index 0000000..bec480d --- /dev/null +++ b/specs/Rules-isWords-spec.js @@ -0,0 +1,66 @@ +var Formsy = require('./../src/main.js'); + +describe('Rules: isWords', function() { + var TestInput, isValid, form, input; + + beforeEach(function() { + isValid = jasmine.createSpy('valid'); + + TestInput = React.createClass({ + mixins: [Formsy.Mixin], + updateValue: function (event) { + this.setValue(event.target.value); + }, + render: function () { + if (this.isValid()) { + isValid(); + } + return + } + }); + + form = TestUtils.renderIntoDocument( + + + + ); + + input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); + + }); + + afterEach(function() { + TestInput = isValid = isInvalid = form = null; + }); + + it('should fail with undefined', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: undefined}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail with null', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: null}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail with a number', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 123}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should pass with a 1 word', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 'sup'}}); + expect(isValid).toHaveBeenCalled(); + }); + + it('should pass with 2 words', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 'sup dude'}}); + expect(isValid).toHaveBeenCalled(); + }); + +}); \ No newline at end of file diff --git a/specs/Rules-maxLength-spec.js b/specs/Rules-maxLength-spec.js new file mode 100644 index 0000000..690a90d --- /dev/null +++ b/specs/Rules-maxLength-spec.js @@ -0,0 +1,72 @@ +var Formsy = require('./../src/main.js'); + +describe('Rules: maxLength', function() { + var TestInput, isValid, form, input; + + beforeEach(function() { + isValid = jasmine.createSpy('valid'); + + TestInput = React.createClass({ + mixins: [Formsy.Mixin], + updateValue: function (event) { + this.setValue(event.target.value); + }, + render: function () { + if (this.isValid()) { + isValid(); + } + return + } + }); + + form = TestUtils.renderIntoDocument( + + + + ); + + input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); + + }); + + afterEach(function() { + TestInput = isValid = isInvalid = form = null; + }); + + it('should fail with undefined', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: undefined}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail with null', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: null}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail with a number', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 123}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should pass when a string\'s length is smaller', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 'hi'}}); + expect(isValid).toHaveBeenCalled(); + }); + + it('should pass when a string\'s length is equal', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 'bar'}}); + expect(isValid).toHaveBeenCalled(); + }); + + it('should fail when a string\'s length is bigger', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 'myValue'}}); + expect(isValid).not.toHaveBeenCalled(); + }); + +}); \ No newline at end of file diff --git a/specs/Rules-minLength-spec.js b/specs/Rules-minLength-spec.js new file mode 100644 index 0000000..f442d55 --- /dev/null +++ b/specs/Rules-minLength-spec.js @@ -0,0 +1,72 @@ +var Formsy = require('./../src/main.js'); + +describe('Rules: minLength', function() { + var TestInput, isValid, form, input; + + beforeEach(function() { + isValid = jasmine.createSpy('valid'); + + TestInput = React.createClass({ + mixins: [Formsy.Mixin], + updateValue: function (event) { + this.setValue(event.target.value); + }, + render: function () { + if (this.isValid()) { + isValid(); + } + return + } + }); + + form = TestUtils.renderIntoDocument( + + + + ); + + input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); + + }); + + afterEach(function() { + TestInput = isValid = isInvalid = form = null; + }); + + it('should fail with undefined', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: undefined}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail with null', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: null}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail with a number', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 123}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should fail when a string\'s length is smaller', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 'hi'}}); + expect(isValid).not.toHaveBeenCalled(); + }); + + it('should pass when a string\'s length is equal', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 'bar'}}); + expect(isValid).toHaveBeenCalled(); + }); + + it('should pass when a string\'s length is bigger', function () { + expect(isValid).not.toHaveBeenCalled(); + TestUtils.Simulate.change(input, {target: {value: 'myValue'}}); + expect(isValid).toHaveBeenCalled(); + }); + +}); \ No newline at end of file diff --git a/specs/Validation-spec.js b/specs/Validation-spec.js index a6b957b..971d0d4 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'); @@ -142,166 +215,4 @@ describe('Validation', function() { }); - it('RULE: isEmail', function () { - - var isValid = jasmine.createSpy('valid'); - var TestInput = React.createClass({ - mixins: [Formsy.Mixin], - updateValue: function (event) { - this.setValue(event.target.value); - }, - render: function () { - if (this.isValid()) { - isValid(); - } - return - } - }); - var form = TestUtils.renderIntoDocument( - - - - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); - expect(isValid).not.toHaveBeenCalled(); - TestUtils.Simulate.change(input, {target: {value: 'foo@foo.com'}}); - expect(isValid).toHaveBeenCalled(); - - }); - - it('RULE: isNumeric', function () { - - var isValid = jasmine.createSpy('valid'); - var TestInput = React.createClass({ - mixins: [Formsy.Mixin], - updateValue: function (event) { - this.setValue(event.target.value); - }, - render: function () { - if (this.isValid()) { - isValid(); - } - return - } - }); - var form = TestUtils.renderIntoDocument( - - - - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); - expect(isValid).not.toHaveBeenCalled(); - TestUtils.Simulate.change(input, {target: {value: '123'}}); - expect(isValid).toHaveBeenCalled(); - - }); - - it('RULE: isNumeric (actual number)', function () { - - var isValid = jasmine.createSpy('valid'); - var TestInput = React.createClass({ - mixins: [Formsy.Mixin], - updateValue: function (event) { - this.setValue(Number(event.target.value)); - }, - render: function () { - if (this.isValid()) { - isValid(); - } - return - } - }); - var form = TestUtils.renderIntoDocument( - - - - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); - expect(isValid).not.toHaveBeenCalled(); - TestUtils.Simulate.change(input, {target: {value: '123'}}); - expect(isValid).toHaveBeenCalled(); - - }); - - it('RULE: isNumeric (string representation of a float)', function () { - - var isValid = jasmine.createSpy('valid'); - var TestInput = React.createClass({ - mixins: [Formsy.Mixin], - updateValue: function (event) { - this.setValue(event.target.value); - }, - render: function () { - if (this.isValid()) { - isValid(); - } - return - } - }); - var form = TestUtils.renderIntoDocument( - - - - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); - expect(isValid).not.toHaveBeenCalled(); - TestUtils.Simulate.change(input, {target: {value: '1.5'}}); - expect(isValid).toHaveBeenCalled(); - - }); - - it('RULE: isNumeric is false (string representation of an invalid float)', function () { - - var isValid = jasmine.createSpy('valid'); - var TestInput = React.createClass({ - mixins: [Formsy.Mixin], - updateValue: function (event) { - this.setValue(event.target.value); - }, - render: function () { - if (this.isValid()) { - isValid(); - } - return - } - }); - var form = TestUtils.renderIntoDocument( - - - - ); - - var input = TestUtils.findRenderedDOMComponentWithTag(form, 'INPUT'); - expect(isValid).not.toHaveBeenCalled(); - TestUtils.Simulate.change(input, {target: {value: '1.'}}); - expect(isValid).not.toHaveBeenCalled(); - - }); - - it('RULE: equalsField', function () { - - var TestInput = React.createClass({ - mixins: [Formsy.Mixin], - render: function () { - return - } - }); - var form = TestUtils.renderIntoDocument( - - - - - - ); - - var input = TestUtils.scryRenderedComponentsWithType(form, TestInput); - expect(input[0].isValid()).toBe(true); - expect(input[1].isValid()).toBe(false); - - }); - }); diff --git a/src/Mixin.js b/src/Mixin.js index 4b4e730..2c8ab9f 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 fa4d0f9..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 @@ -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)); diff --git a/src/validationRules.js b/src/validationRules.js index 05fa3b2..0d30179 100644 --- a/src/validationRules.js +++ b/src/validationRules.js @@ -1,33 +1,36 @@ module.exports = { - 'isDefaultRequiredValue': function (values, value) { + isDefaultRequiredValue: function (values, value) { return value === undefined || value === ''; }, - 'hasValue': function (values, value) { - return value !== undefined; + hasValue: function (values, value) { + return !!value; }, - 'matchRegexp': function (values, value, regexp) { - return value !== undefined && !!value.match(regexp); + matchRegexp: function (values, value, regexp) { + return !!value && !!value.match(regexp); }, - 'isUndefined': function (values, value) { + isUndefined: function (values, value) { return value === undefined; }, - 'isEmptyString': function (values, value) { + isEmptyString: function (values, value) { return value === ''; }, - 'isEmail': function (values, value) { + isEmail: function (values, value) { return !value || 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) { + isTrue: function (values, value) { return value === true; }, - 'isFalse': function (values, value) { + isFalse: function (values, value) { return value === false; }, - 'isNumeric': function (values, value) { + isNumeric: function (values, value) { + if (!value) { + return false; + } if (typeof value === 'number') { return true; } else { - var matchResults = value !== undefined && value.match(/[-+]?(\d*[.])?\d+/); + var matchResults = value.match(/[-+]?(\d*[.])?\d+/); if (!!matchResults) { return matchResults[0] == value; } else { @@ -35,17 +38,17 @@ module.exports = { } } }, - 'isAlpha': function (values, value) { - return !value || value.match(/^[a-zA-Z]+$/); + isAlpha: function (values, value) { + return value && /^[a-zA-Z]+$/.test(value); }, - 'isWords': function (values, value) { - return !value || value.match(/^[a-zA-Z\s]+$/); + isWords: function (values, value) { + return value && /^[a-zA-Z\s]+$/.test(value); }, - 'isSpecialWords': function (values, value) { + isSpecialWords: function (values, value) { return !value || value.match(/^[a-zA-Z\s\u00C0-\u017F]+$/); }, isLength: function (values, value, length) { - return value !== undefined && value.length === length; + return value && value.length === length; }, equals: function (values, value, eql) { return value == eql; @@ -54,9 +57,9 @@ module.exports = { return value == values[field]; }, maxLength: function (values, value, length) { - return value !== undefined && value.length <= length; + return value && value.length && value.length <= length; }, minLength: function (values, value, length) { - return value !== undefined && value.length >= length; + return value && value.length && value.length >= length; } };