diff --git a/README.md b/README.md index cdc2f12..453b2a1 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,10 @@ The main concept is that forms, inputs and validation is done very differently a ## Changes +**0.4.0**: + - Possibility to handle form data manually using "onSubmit" + - Added two more default rules. *isWords* and *isSpecialWords* + **0.3.0**: - Deprecated everything related to buttons automatically added - Added onValid and onInvalid handlers, use those to manipulate submit buttons etc. @@ -197,7 +201,7 @@ Sets a class name on the form itself. ```html ``` -Will either **POST** or **PUT** to the url specified when submitted. +Will either **POST** or **PUT** to the url specified when submitted. If you do not pass a url the data for the form will be passed to the **onSubmit** handler. #### method ```html @@ -219,11 +223,13 @@ Supports **json** (default) and **urlencoded** (x-www-form-urlencoded). ``` Takes a function to run when the server has responded with a success http status code. -#### onSubmit() +#### onSubmit(data, resetForm) ```html ``` -Takes a function to run when the submit button has been clicked. +Takes a function to run when the submit button has been clicked. The first argument is the data of the form. The second argument will reset the form. + +**note!** When resetting the form the form elements needs to bind its current value using the *getValue* method. That will empty for example an input. #### onSubmitted() ```html @@ -503,9 +509,9 @@ Returns true if string is only letters ``` Returns true if string is only letters, including spaces and tabs -**isWordsSpecial** +**isSpecialWords** ```html - + ``` Returns true if string is only letters, including special letters (a-z,ú,ø,æ,å) diff --git a/package.json b/package.json index 1f94d3c..b1fcfe6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "formsy-react", - "version": "0.3.0", + "version": "0.4.0", "description": "A form input builder and validator for React JS", "main": "src/main.js", "scripts": { diff --git a/releases/0.4.0/formsy-react-0.4.0.js b/releases/0.4.0/formsy-react-0.4.0.js new file mode 100755 index 0000000..a1ffb1a --- /dev/null +++ b/releases/0.4.0/formsy-react-0.4.0.js @@ -0,0 +1,381 @@ +!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, 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); + + // Add passed headers + Object.keys(headers).forEach(function (header) { + xhr.setRequestHeader(header, headers[header]); + }); + + 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 { + headers: {}, + onSuccess: function () {}, + onError: function () {}, + onSubmit: function () {}, + onSubmitted: function () {}, + onValid: function () {}, + onInvalid: 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(); + + // 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) { + this.updateModel(); + this.props.onSubmit(this.model, this.resetModel); + return; + } + + this.updateModel(); + this.setState({ + isSubmitting: true + }); + + this.props.onSubmit(); + + var headers = (Object.keys(this.props.headers).length && this.props.headers) || options.headers; + + ajax[this.props.method || 'post'](this.props.url, this.model, this.props.contentType || options.contentType || 'json', headers) + .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)); + }, + + // Reset each key in the model to the original / initial value + resetModel: function() { + Object.keys(this.inputs).forEach(function (name) { + this.inputs[name].resetValue(); + }.bind(this)); + this.validateForm(); + }, + + // 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 + }); + + allIsValid && this.props.onValid(); + !allIsValid && this.props.onInvalid(); + }, + + // 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 () { + + return React.DOM.form({ + onSubmit: this.submit, + className: this.props.className + }, + this.props.children + ); + + } +}); + +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"}]},{},[1])(1) +}); \ No newline at end of file diff --git a/releases/0.4.0/formsy-react-0.4.0.min.js b/releases/0.4.0/formsy-react-0.4.0.min.js new file mode 100755 index 0000000..78f6338 --- /dev/null +++ b/releases/0.4.0/formsy-react-0.4.0.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 o(r,u){if(!i[r]){if(!e[r]){var a="function"==typeof require&&require;if(!u&&a)return a(r,!0);if(s)return s(r,!0);var p=new Error("Cannot find module '"+r+"'");throw p.code="MODULE_NOT_FOUND",p}var d=i[r]={exports:{}};e[r][0].call(d.exports,function(t){var i=e[r][1][t];return o(i?i:t)},d,d.exports,t,e,i,n)}return i[r].exports}for(var s="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,o){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,r){try{var u=new XMLHttpRequest;u.open(t,e,!0),u.setRequestHeader("Accept","application/json"),u.setRequestHeader("Content-Type",n),Object.keys(o).forEach(function(t){u.setRequestHeader(t,o[t])}),u.onreadystatechange=function(){4===u.readyState&&(u.status>=200&&u.status<300?s(u.responseText?JSON.parse(u.responseText):null):r(u.responseText?JSON.parse(u.responseText):null))},u.send(i)}catch(a){r(a)}})},a={post:u.bind(null,"POST"),put:u.bind(null,"PUT")},p={};o.defaults=function(t){p=t},o.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}},o.addValidationRule=function(t,e){s[t]=e},o.Form=n.createClass({getInitialState:function(){return{isValid:!0,isSubmitting:!1}},getDefaultProps:function(){return{headers:{},onSuccess:function(){},onError:function(){},onSubmit:function(){},onSubmitted:function(){},onValid:function(){},onInvalid: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)return this.updateModel(),void this.props.onSubmit(this.model,this.resetModel);this.updateModel(),this.setState({isSubmitting:!0}),this.props.onSubmit();var e=Object.keys(this.props.headers).length&&this.props.headers||p.headers;a[this.props.method||"post"](this.props.url,this.model,this.props.contentType||p.contentType||"json",e).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))},resetModel:function(){Object.keys(this.inputs).forEach(function(t){this.inputs[t].resetValue()}.bind(this)),this.validateForm()},updateInputsWithError:function(t){Object.keys(t).forEach(function(e,i){var n=this.inputs[e],o=[{_isValid:!1,_serverError:t[e]}];i===Object.keys(t).length-1&&o.push(this.validateForm),n.setState.apply(n,o)}.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(":"),o=n.shift();if(n=n.map(function(t){return JSON.parse(t)}),n=[t.state._value].concat(n),!s[o])throw new Error("Formsy does not have the validation rule: "+o);s[o].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}),t&&this.props.onValid(),!t&&this.props.onInvalid()},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(){return n.DOM.form({onSubmit:this.submit,className:this.props.className},this.props.children)}}),i.exports||i.module||i.define&&i.define.amd||(i.Formsy=o),e.exports=o}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{react:"react"}]},{},[1])(1)}); \ No newline at end of file diff --git a/src/main.js b/src/main.js index f249891..a6ad0b4 100644 --- a/src/main.js +++ b/src/main.js @@ -19,7 +19,7 @@ var validationRules = { 'isWords': function (value) { return value.match(/^[a-zA-Z\s]+$/); }, - 'isWordsSpecial': function (value) { + 'isSpecialWords': function (value) { return value.match(/^[a-zA-Z\s\u00C0-\u017F]+$/); }, isLength: function (value, min, max) { @@ -219,7 +219,7 @@ Formsy.Form = React.createClass({ this.props.onSubmit(); var headers = (Object.keys(this.props.headers).length && this.props.headers) || options.headers; - console.log('headers', headers); + ajax[this.props.method || 'post'](this.props.url, this.model, this.props.contentType || options.contentType || 'json', headers) .then(function (response) { this.onSuccess(response); @@ -242,6 +242,7 @@ Formsy.Form = React.createClass({ Object.keys(this.inputs).forEach(function (name) { this.inputs[name].resetValue(); }.bind(this)); + this.validateForm(); }, // Go through errors from server and grab the components