diff --git a/README.md b/README.md index 2bc1ee1..eb97de8 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,10 @@ The main concept is that forms, inputs and validation is done very differently a ## Changes +**0.2.3**: + + - Fixed bug where child does not have props property + **0.2.2**: - Fixed bug with updating the props diff --git a/bower.json b/bower.json index 889c1b9..fcd170f 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "formsy-react", - "version": "0.2.2", + "version": "0.2.3", "main": "src/main.js", "dependencies": { "react": "^0.11.2" diff --git a/package.json b/package.json index 9dd3c89..75e480c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "formsy-react", - "version": "0.2.2", + "version": "0.2.3", "description": "A form input builder and validator for React JS", "main": "src/main.js", "scripts": { diff --git a/releases/0.2.3/formsy-react-0.2.3.js b/releases/0.2.3/formsy-react-0.2.3.js new file mode 100755 index 0000000..79feb3f --- /dev/null +++ b/releases/0.2.3/formsy-react-0.2.3.js @@ -0,0 +1,358 @@ +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define(["react"],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.Formsy=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= min && value.length <= max; + } + return value.length >= min; + }, + equals: function (value, eql) { + return value == eql; + } +}; +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) { + + 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); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + + if (xhr.status >= 200 && xhr.status < 300) { + resolve(xhr.responseText ? JSON.parse(xhr.responseText) : null); + } else { + reject(xhr.responseText ? JSON.parse(xhr.responseText) : null); + } + + } + }; + xhr.send(data); + } catch (e) { + reject(e); + } + }); + +}; +var ajax = { + post: request.bind(null, 'POST'), + put: request.bind(null, 'PUT') +}; +var options = {}; + +Formsy.defaults = function (passedOptions) { + options = passedOptions; +}; + +Formsy.Mixin = { + getInitialState: function () { + return { + _value: this.props.value ? this.props.value : '', + _isValid: true + }; + }, + componentWillMount: function () { + + if (!this.props.name) { + throw new Error('Form Input requires a name property when used'); + } + + if (!this.props._attachToForm) { + throw new Error('Form Mixin requires component to be nested in a Form'); + } + + if (this.props.required) { + this.props.validations = this.props.validations ? this.props.validations + ',' : ''; + this.props.validations += 'isValue'; + } + this.props._attachToForm(this); + }, + + // We have to make the validate method is kept when new props are added + componentWillReceiveProps: function (nextProps) { + nextProps._attachToForm = this.props._attachToForm; + nextProps._detachFromForm = this.props._detachFromForm; + nextProps._validate = this.props._validate; + }, + + // Detach it when component unmounts + componentWillUnmount: function () { + this.props._detachFromForm(this); + }, + + // We validate after the value has been set + setValue: function (value) { + this.setState({ + _value: value + }, function () { + this.props._validate(this); + }.bind(this)); + }, + resetValue: function () { + this.setState({ + _value: '' + }, function () { + this.props._validate(this); + }); + }, + getValue: function () { + return this.state._value; + }, + hasValue: function () { + return this.state._value !== ''; + }, + getErrorMessage: function () { + return this.isValid() || this.showRequired() ? null : this.state._serverError || this.props.validationError; + }, + isValid: function () { + return this.state._isValid; + }, + isRequired: function () { + return this.props.required; + }, + showRequired: function () { + return this.props.required && this.state._value === ''; + }, + showError: function () { + return !this.showRequired() && !this.state._isValid; + } +}; + +Formsy.addValidationRule = function (name, func) { + validationRules[name] = func; +}; + +Formsy.Form = React.createClass({ + getInitialState: function () { + return { + isValid: true, + isSubmitting: false + }; + }, + getDefaultProps: function () { + return { + onSuccess: function () {}, + onError: function () {}, + onSubmit: function () {}, + onSubmitted: function () {} + } + }, + + // Add a map to store the inputs of the form, a model to store + // the values of the form and register child inputs + componentWillMount: function () { + this.inputs = {}; + this.model = {}; + this.registerInputs(this.props.children); + }, + + componentDidMount: function () { + this.validateForm(); + }, + + // Update model, submit to url prop and send the model + submit: function (event) { + event.preventDefault(); + + if (!this.props.url) { + throw new Error('Formsy Form needs a url property to post the form'); + } + + this.updateModel(); + this.setState({ + isSubmitting: true + }); + this.props.onSubmit(); + ajax[this.props.method || 'post'](this.props.url, this.model, this.props.contentType || options.contentType || 'json') + .then(function (response) { + this.onSuccess(response); + this.onSubmitted(); + }.bind(this)) + .catch(this.updateInputsWithError); + }, + + // Goes through all registered components and + // updates the model values + updateModel: function () { + Object.keys(this.inputs).forEach(function (name) { + var component = this.inputs[name]; + this.model[name] = component.state._value; + }.bind(this)); + }, + + // Go through errors from server and grab the components + // stored in the inputs map. Change their state to invalid + // and set the serverError message + updateInputsWithError: function (errors) { + Object.keys(errors).forEach(function (name, index) { + var component = this.inputs[name]; + var args = [{ + _isValid: false, + _serverError: errors[name] + }]; + if (index === Object.keys(errors).length - 1) { + args.push(this.validateForm); + } + component.setState.apply(component, args); + }.bind(this)); + this.setState({ + isSubmitting: false + }); + this.props.onError(errors); + this.props.onSubmitted(); + }, + + // Traverse the children and children of children to find + // all inputs by checking the name prop. Maybe do a better + // check here + registerInputs: function (children) { + React.Children.forEach(children, function (child) { + + if (child.props && child.props.name) { + child.props._attachToForm = this.attachToForm; + child.props._detachFromForm = this.detachFromForm; + child.props._validate = this.validate; + } + + if (child.props && child.props.children) { + this.registerInputs(child.props.children); + } + + }.bind(this)); + }, + + // Use the binded values and the actual input value to + // validate the input and set its state. Then check the + // state of the form itself + validate: function (component) { + + if (!component.props.validations) { + return; + } + + // Run through the validations, split them up and call + // the validator IF there is a value or it is required + var isValid = true; + if (component.props.required || component.state._value !== '') { + component.props.validations.split(',').forEach(function (validation) { + var args = validation.split(':'); + var validateMethod = args.shift(); + args = args.map(function (arg) { return JSON.parse(arg); }); + args = [component.state._value].concat(args); + if (!validationRules[validateMethod]) { + throw new Error('Formsy does not have the validation rule: ' + validateMethod); + } + if (!validationRules[validateMethod].apply(null, args)) { + isValid = false; + } + }); + } + + component.setState({ + _isValid: isValid, + _serverError: null + }, this.validateForm); + + }, + + // Validate the form by going through all child input components + // and check their state + validateForm: function () { + var allIsValid = true; + var inputs = this.inputs; + + Object.keys(inputs).forEach(function (name) { + if (!inputs[name].state._isValid) { + allIsValid = false; + } + }); + + this.setState({ + isValid: allIsValid + }); + }, + + // Method put on each input component to register + // itself to the form + attachToForm: function (component) { + this.inputs[component.props.name] = component; + this.model[component.props.name] = component.state._value; + this.validate(component); + }, + + // Method put on each input component to unregister + // itself from the form + detachFromForm: function (component) { + delete this.inputs[component.props.name]; + delete this.model[component.props.name]; + }, + render: function () { + var submitButton = React.DOM.button({ + className: this.props.submitButtonClass || options.submitButtonClass, + disabled: this.state.isSubmitting || !this.state.isValid + }, this.props.submitLabel || 'Submit'); + + var cancelButton = React.DOM.button({ + onClick: this.props.onCancel, + disabled: this.state.isSubmitting, + className: this.props.cancelButtonClass || options.cancelButtonClass + }, this.props.cancelLabel || 'Cancel'); + + return React.DOM.form({ + onSubmit: this.submit, + className: this.props.className + }, + this.props.children, + React.DOM.div({ + className: this.props.buttonWrapperClass || options.buttonWrapperClass + }, + this.props.onCancel ? cancelButton : null, + this.props.hideSubmit || options.hideSubmit ? null : submitButton + ) + ); + + } +}); + +if (!global.exports && !global.module && (!global.define || !global.define.amd)) { + global.Formsy = Formsy; +} + +module.exports = Formsy; +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"react":"react"}]},{},["/Users/christianalfoni/Documents/dev/formsy-react/src/main.js"])("/Users/christianalfoni/Documents/dev/formsy-react/src/main.js") +}); \ No newline at end of file diff --git a/releases/0.2.3/formsy-react-0.2.3.min.js b/releases/0.2.3/formsy-react-0.2.3.min.js new file mode 100755 index 0000000..8f21743 --- /dev/null +++ b/releases/0.2.3/formsy-react-0.2.3.min.js @@ -0,0 +1 @@ +!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;"undefined"!=typeof window?e=window:"undefined"!=typeof global?e=global:"undefined"!=typeof self&&(e=self),e.Formsy=t()}}(function(){return function t(e,i,n){function s(r,u){if(!i[r]){if(!e[r]){var a="function"==typeof require&&require;if(!u&&a)return a(r,!0);if(o)return o(r,!0);var p=new Error("Cannot find module '"+r+"'");throw p.code="MODULE_NOT_FOUND",p}var l=i[r]={exports:{}};e[r][0].call(l.exports,function(t){var i=e[r][1][t];return s(i?i:t)},l,l.exports,t,e,i,n)}return i[r].exports}for(var o="function"==typeof require&&require,r=0;r=e&&t.length<=i:t.length>=e},equals:function(t,e){return t==e}},r=function(t,e,i){var i=i||[];if("object"==typeof t)for(var n in t)r(t[n],e?e+"["+n+"]":n,i);else i.push(e+"="+encodeURIComponent(t));return i.join("&")},u=function(t,e,i,n){var n="urlencoded"===n?"application/"+n.replace("urlencoded","x-www-form-urlencoded"):"application/json";return i="application/json"===n?JSON.stringify(i):r(i),new Promise(function(s,o){try{var r=new XMLHttpRequest;r.open(t,e,!0),r.setRequestHeader("Accept","application/json"),r.setRequestHeader("Content-Type",n),r.onreadystatechange=function(){4===r.readyState&&(r.status>=200&&r.status<300?s(r.responseText?JSON.parse(r.responseText):null):o(r.responseText?JSON.parse(r.responseText):null))},r.send(i)}catch(u){o(u)}})},a={post:u.bind(null,"POST"),put:u.bind(null,"PUT")},p={};s.defaults=function(t){p=t},s.Mixin={getInitialState:function(){return{_value:this.props.value?this.props.value:"",_isValid:!0}},componentWillMount:function(){if(!this.props.name)throw new Error("Form Input requires a name property when used");if(!this.props._attachToForm)throw new Error("Form Mixin requires component to be nested in a Form");this.props.required&&(this.props.validations=this.props.validations?this.props.validations+",":"",this.props.validations+="isValue"),this.props._attachToForm(this)},componentWillReceiveProps:function(t){t._attachToForm=this.props._attachToForm,t._detachFromForm=this.props._detachFromForm,t._validate=this.props._validate},componentWillUnmount:function(){this.props._detachFromForm(this)},setValue:function(t){this.setState({_value:t},function(){this.props._validate(this)}.bind(this))},resetValue:function(){this.setState({_value:""},function(){this.props._validate(this)})},getValue:function(){return this.state._value},hasValue:function(){return""!==this.state._value},getErrorMessage:function(){return this.isValid()||this.showRequired()?null:this.state._serverError||this.props.validationError},isValid:function(){return this.state._isValid},isRequired:function(){return this.props.required},showRequired:function(){return this.props.required&&""===this.state._value},showError:function(){return!this.showRequired()&&!this.state._isValid}},s.addValidationRule=function(t,e){o[t]=e},s.Form=n.createClass({getInitialState:function(){return{isValid:!0,isSubmitting:!1}},getDefaultProps:function(){return{onSuccess:function(){},onError:function(){},onSubmit:function(){},onSubmitted:function(){}}},componentWillMount:function(){this.inputs={},this.model={},this.registerInputs(this.props.children)},componentDidMount:function(){this.validateForm()},submit:function(t){if(t.preventDefault(),!this.props.url)throw new Error("Formsy Form needs a url property to post the form");this.updateModel(),this.setState({isSubmitting:!0}),this.props.onSubmit(),a[this.props.method||"post"](this.props.url,this.model,this.props.contentType||p.contentType||"json").then(function(t){this.onSuccess(t),this.onSubmitted()}.bind(this)).catch(this.updateInputsWithError)},updateModel:function(){Object.keys(this.inputs).forEach(function(t){var e=this.inputs[t];this.model[t]=e.state._value}.bind(this))},updateInputsWithError:function(t){Object.keys(t).forEach(function(e,i){var n=this.inputs[e],s=[{_isValid:!1,_serverError:t[e]}];i===Object.keys(t).length-1&&s.push(this.validateForm),n.setState.apply(n,s)}.bind(this)),this.setState({isSubmitting:!1}),this.props.onError(t),this.props.onSubmitted()},registerInputs:function(t){n.Children.forEach(t,function(t){t.props&&t.props.name&&(t.props._attachToForm=this.attachToForm,t.props._detachFromForm=this.detachFromForm,t.props._validate=this.validate),t.props&&t.props.children&&this.registerInputs(t.props.children)}.bind(this))},validate:function(t){if(t.props.validations){var e=!0;(t.props.required||""!==t.state._value)&&t.props.validations.split(",").forEach(function(i){var n=i.split(":"),s=n.shift();if(n=n.map(function(t){return JSON.parse(t)}),n=[t.state._value].concat(n),!o[s])throw new Error("Formsy does not have the validation rule: "+s);o[s].apply(null,n)||(e=!1)}),t.setState({_isValid:e,_serverError:null},this.validateForm)}},validateForm:function(){var t=!0,e=this.inputs;Object.keys(e).forEach(function(i){e[i].state._isValid||(t=!1)}),this.setState({isValid:t})},attachToForm:function(t){this.inputs[t.props.name]=t,this.model[t.props.name]=t.state._value,this.validate(t)},detachFromForm:function(t){delete this.inputs[t.props.name],delete this.model[t.props.name]},render:function(){var t=n.DOM.button({className:this.props.submitButtonClass||p.submitButtonClass,disabled:this.state.isSubmitting||!this.state.isValid},this.props.submitLabel||"Submit"),e=n.DOM.button({onClick:this.props.onCancel,disabled:this.state.isSubmitting,className:this.props.cancelButtonClass||p.cancelButtonClass},this.props.cancelLabel||"Cancel");return n.DOM.form({onSubmit:this.submit,className:this.props.className},this.props.children,n.DOM.div({className:this.props.buttonWrapperClass||p.buttonWrapperClass},this.props.onCancel?e:null,this.props.hideSubmit||p.hideSubmit?null:t))}}),i.exports||i.module||i.define&&i.define.amd||(i.Formsy=s),e.exports=s}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{react:"react"}]},{},["/Users/christianalfoni/Documents/dev/formsy-react/src/main.js"])("/Users/christianalfoni/Documents/dev/formsy-react/src/main.js")}); \ No newline at end of file diff --git a/src/main.js b/src/main.js index 109958a..95455bc 100644 --- a/src/main.js +++ b/src/main.js @@ -240,13 +240,13 @@ Formsy.Form = React.createClass({ registerInputs: function (children) { React.Children.forEach(children, function (child) { - if (child.props.name) { + if (child.props && child.props.name) { child.props._attachToForm = this.attachToForm; child.props._detachFromForm = this.detachFromForm; child.props._validate = this.validate; } - if (child.props.children) { + if (child.props && child.props.children) { this.registerInputs(child.props.children); }