Added new handlers

This commit is contained in:
Christian Alfoni 2015-01-05 15:03:01 +01:00
parent 20f1d71f96
commit df4e84a10a
7 changed files with 465 additions and 95 deletions

2
.gitignore vendored
View File

@ -26,3 +26,5 @@ node_modules
# Users Environment Variables
.lock-wscript
.DS_Store
build/

View File

@ -19,7 +19,7 @@ var runBrowserifyTask = function (options) {
debug: options.debug, // Need that sourcemapping
standalone: 'Formsy',
// These options are just for Watchify
cache: {}, packageCache: {}, fullPaths: true
cache: {}, packageCache: {}, fullPaths: options.watch
})
.require(require.resolve('./src/main.js'), { entry: true })
.transform(reactify) // Transform JSX
@ -91,4 +91,4 @@ gulp.task('deploy', function () {
gulp.task('test', shell.task([
'./node_modules/.bin/jasmine-node ./specs --autotest --watch ./src --color'
]));
]));

View File

@ -15,15 +15,13 @@ A form input builder and validator for React JS
- [url](#url)
- [method](#method)
- [contentType](#contenttype)
- [hideSubmit](#hideSubmit)
- [submitButtonClass](#submitButtonClass)
- [cancelButtonClass](#cancelButtonClass)
- [buttonWrapperClass](#buttonWrapperClass)
- [onSuccess()](#onsuccess)
- [onSubmit()](#onsubmit)
- [onSubmitted()](#onsubmitted)
- [onCancel()](#oncancel)
- [onError()](#onerror)
- [onValid()](#onvalid)
- [onInvalid()](#oninvalid)
- [Formsy.Mixin](#formsymixin)
- [name](#name)
- [validations](#validations)
@ -53,7 +51,7 @@ The main concept is that forms, inputs and validation is done very differently a
2. Add validation rules and use them with simple syntax
3. Use handlers for different states of your form. Ex. "onSubmit", "onError" etc.
3. Use handlers for different states of your form. Ex. "onSubmit", "onError", "onValid" etc.
4. Server validation errors automatically binds to the correct form input component
@ -65,6 +63,10 @@ The main concept is that forms, inputs and validation is done very differently a
## <a name="changes">Changes</a>
**0.3.0**:
- Deprecated everything related to buttons automatically added
- Added onValid and onInvalid handlers, use those to manipulate submit buttons etc.
**0.2.3**:
- Fixed bug where child does not have props property
@ -105,12 +107,21 @@ The main concept is that forms, inputs and validation is done very differently a
changeUrl: function () {
location.href = '/success';
},
enableButton: function () {
this.setState({
canSubmit: true
});
},
disableButton: function () {
this.setState({
canSubmit: false
});
},
render: function () {
return (
<Formsy.Form url="/users" onSuccess={this.changeUrl}>
<Formsy.Form url="/users" onSuccess={this.changeUrl} onValid={this.enableButton} onInvalid={this.disableButton}>
<MyOwnInput name="email" validations="isEmail" validationError="This is not a valid email" required/>
<button type="submit" disabled={!this.state.canSubmit}>Submit</button>
</Formsy.Form>
);
}
@ -119,7 +130,7 @@ The main concept is that forms, inputs and validation is done very differently a
This code results in a form with a submit button that will POST to /users when clicked. The submit button is disabled as long as the input is empty (required) or the value is not an email (isEmail). On validation error it will show the message: "This is not a valid email".
#### This is an example of what you can enjoy building
#### Building a form element
```javascript
/** @jsx React.DOM */
var Formsy = require('formsy-react');
@ -163,10 +174,13 @@ So this is basically how you build your form elements. As you can see it is very
```javascript
Formsy.defaults({
contentType: 'urlencoded', // default: 'json'
headers: {} // default headers
/* DEPRECATED
hideSubmit: true, // default: false
submitButtonClass: 'btn btn-success', // default: null
cancelButtonClass: 'btn btn-default', // default: null
buttonWrapperClass: 'my-wrapper' // default: null
*/
});
```
Use **defaults** to set general settings for all your forms.
@ -199,30 +213,6 @@ Supports **json** (default) and **urlencoded** (x-www-form-urlencoded).
**Note!** Response has to be **json**.
#### <a name="hidesubmit">hideSubmit</a>
```html
<Formsy.Form url="/users" method="PUT" hideSubmit></Formsy.Form>
```
Hides the submit button. Submit is done by ENTER on an input.
#### <a name="submitbuttonclass">submitButtonClass</a>
```html
<Formsy.Form url="/users" method="PUT" submitButtonClass="btn btn-success"></Formsy.Form>
```
Sets a class name on the submit button.
#### <a name="cancelbuttonclass">cancelButtonClass</a>
```html
<Formsy.Form url="/users" method="PUT" cancelButtonClass="btn btn-default"></Formsy.Form>
```
Sets a class name on the cancel button.
#### <a name="buttonwrapperclass">buttonWrapperClass</a>
```html
<Formsy.Form url="/users" method="PUT" buttonWrapperClass="my-wrapper"></Formsy.Form>
```
Sets a class name on the container that wraps the **submit** and **cancel** buttons.
#### <a name="onsuccess">onSuccess(serverResponse)</a>
```html
<Formsy.Form url="/users" onSuccess={this.changeUrl}></Formsy.Form>
@ -253,6 +243,18 @@ Will display a "cancel" button next to submit. On click it runs the function han
```
Takes a function to run when the server responds with an error http status code.
#### <a name="onvalid">onValid()</a>
```html
<Formsy.Form url="/users" onValid={this.enableSubmitButton}></Formsy.Form>
```
Whenever the form becomes valid the "onValid" handler is called. Use it to change state of buttons or whatever your heart desires.
#### <a name="oninvalid">onInvalid()</a>
```html
<Formsy.Form url="/users" onInvalid={this.disableSubmitButton}></Formsy.Form>
```
Whenever the form becomes invalid the "onInvalid" handler is called. Use it to for example revert "onValid" state.
### <a name="formsymixin">Formsy.Mixin</a>
#### <a name="name">name</a>
@ -342,7 +344,7 @@ var MyInput = React.createClass({
return (
<div>
<input type="text" onChange={this.changeValue} value={this.getValue()}/>
<button onClick={this.resetValue}>Reset</button>
<button onClick={this.resetValue()}>Reset</button>
</div>
);
}
@ -360,7 +362,7 @@ var MyInput = React.createClass({
return (
<div>
<input type="text" onChange={this.changeValue} value={this.getValue()}/>
<span>{this.getErrorMessage}</span>
<span>{this.getErrorMessage()}</span>
</div>
);
}
@ -380,7 +382,7 @@ var MyInput = React.createClass({
<div>
<span>{face}</span>
<input type="text" onChange={this.changeValue} value={this.getValue()}/>
<span>{this.getErrorMessage}</span>
<span>{this.getErrorMessage()}</span>
</div>
);
}
@ -399,7 +401,7 @@ var MyInput = React.createClass({
<div>
<span>{this.props.label} {this.isRequired() ? '*' : null}</span>
<input type="text" onChange={this.changeValue} value={this.getValue()}/>
<span>{this.getErrorMessage}</span>
<span>{this.getErrorMessage()}</span>
</div>
);
}
@ -418,7 +420,7 @@ var MyInput = React.createClass({
return (
<div className={className}>
<input type="text" onChange={this.changeValue} value={this.getValue()}/>
<span>{this.getErrorMessage}</span>
<span>{this.getErrorMessage()}</span>
</div>
);
}
@ -437,7 +439,7 @@ var MyInput = React.createClass({
return (
<div className={className}>
<input type="text" onChange={this.changeValue} value={this.getValue()}/>
<span>{this.getErrorMessage}</span>
<span>{this.getErrorMessage()}</span>
</div>
);
}

View File

@ -1,6 +1,6 @@
{
"name": "formsy-react",
"version": "0.2.3",
"version": "0.3.0",
"description": "A form input builder and validator for React JS",
"main": "src/main.js",
"scripts": {

View File

@ -0,0 +1,362 @@
!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<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
(function (global){
var React = global.React || require('react');
var Formsy = {};
var validationRules = {
'isValue': function (value) {
return value !== '';
},
'isEmail': function (value) {
return 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 (value) {
return value === true;
},
'isNumeric': function (value) {
return value.match(/^-?[0-9]+$/)
},
'isAlpha': function (value) {
return value.match(/^[a-zA-Z]+$/);
},
isLength: function (value, min, max) {
if (max !== undefined) {
return value.length >= 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();
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();
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);
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
});
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)
});

1
releases/0.3.0/formsy-react-0.3.0.min.js vendored Executable file

File diff suppressed because one or more lines are too long

View File

@ -26,18 +26,19 @@ var validationRules = {
return value == eql;
}
};
var toURLEncoded = function (element,key,list){
var toURLEncoded = function (element, key, list) {
var list = list || [];
if(typeof(element)=='object'){
if (typeof (element) == 'object') {
for (var idx in element)
toURLEncoded(element[idx],key?key+'['+idx+']':idx,list);
toURLEncoded(element[idx], key ? key + '[' + idx + ']' : idx, list);
} else {
list.push(key+'='+encodeURIComponent(element));
list.push(key + '=' + encodeURIComponent(element));
}
return list.join('&');
};
var request = function (method, url, data, contentType) {
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);
@ -48,6 +49,12 @@ var request = function (method, url, data, contentType) {
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) {
@ -78,26 +85,26 @@ Formsy.defaults = function (passedOptions) {
Formsy.Mixin = {
getInitialState: function () {
return {
_value: this.props.value ? this.props.value : '',
_isValid: true
};
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.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._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);
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
@ -109,16 +116,16 @@ Formsy.Mixin = {
// Detach it when component unmounts
componentWillUnmount: function () {
this.props._detachFromForm(this);
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));
this.setState({
_value: value
}, function () {
this.props._validate(this);
}.bind(this));
},
resetValue: function () {
this.setState({
@ -163,11 +170,14 @@ Formsy.Form = React.createClass({
},
getDefaultProps: function () {
return {
headers: {},
onSuccess: function () {},
onError: function () {},
onSubmit: function () {},
onSubmitted: function () {}
}
onSubmitted: function () {},
onValid: function () {},
onInvalid: function () {}
};
},
// Add a map to store the inputs of the form, a model to store
@ -194,8 +204,12 @@ Formsy.Form = React.createClass({
this.setState({
isSubmitting: true
});
this.props.onSubmit();
ajax[this.props.method || 'post'](this.props.url, this.model, this.props.contentType || options.contentType || 'json')
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);
this.onSubmitted();
@ -269,7 +283,9 @@ Formsy.Form = React.createClass({
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 = 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);
@ -292,7 +308,7 @@ Formsy.Form = React.createClass({
validateForm: function () {
var allIsValid = true;
var inputs = this.inputs;
Object.keys(inputs).forEach(function (name) {
if (!inputs[name].state._isValid) {
allIsValid = false;
@ -302,6 +318,9 @@ Formsy.Form = React.createClass({
this.setState({
isValid: allIsValid
});
allIsValid && this.props.onValid();
!allIsValid && this.props.onInvalid();
},
// Method put on each input component to register
@ -319,30 +338,14 @@ Formsy.Form = React.createClass({
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
)
onSubmit: this.submit,
className: this.props.className
},
this.props.children
);
}
});
@ -350,4 +353,4 @@ if (!global.exports && !global.module && (!global.define || !global.define.amd))
global.Formsy = Formsy;
}
module.exports = Formsy;
module.exports = Formsy;