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