From 6727636536eca1d6e121bb25a921af5926e095f8 Mon Sep 17 00:00:00 2001 From: Christian Alfoni Date: Wed, 22 Oct 2014 14:51:26 +0200 Subject: [PATCH] Initial commit --- Gulpfile.js | 94 +++++ README.md | 445 +++++++++++++++++++++++ bower.json | 8 + package.json | 25 ++ releases/0.1.0/formsy-react-0.1.0.js | 341 +++++++++++++++++ releases/0.1.0/formsy-react-0.1.0.min.js | 1 + src/main.js | 336 +++++++++++++++++ 7 files changed, 1250 insertions(+) create mode 100644 Gulpfile.js create mode 100644 bower.json create mode 100644 package.json create mode 100755 releases/0.1.0/formsy-react-0.1.0.js create mode 100755 releases/0.1.0/formsy-react-0.1.0.min.js create mode 100644 src/main.js diff --git a/Gulpfile.js b/Gulpfile.js new file mode 100644 index 0000000..6e1b01d --- /dev/null +++ b/Gulpfile.js @@ -0,0 +1,94 @@ +var gulp = require('gulp'); +var browserify = require('browserify'); +var watchify = require('watchify'); +var source = require('vinyl-source-stream'); +var gulpif = require('gulp-if'); +var uglify = require('gulp-uglify'); +var streamify = require('gulp-streamify'); +var notify = require('gulp-notify'); +var gutil = require('gulp-util'); +var package = require('./package.json'); +var shell = require('gulp-shell'); +var reactify = require('reactify'); + +// The task that handles both development and deployment +var runBrowserifyTask = function (options) { + + // This bundle is for our application + var bundler = browserify({ + debug: options.debug, // Need that sourcemapping + standalone: 'Formsy', + // These options are just for Watchify + cache: {}, packageCache: {}, fullPaths: true + }) + .require(require.resolve('./src/main.js'), { entry: true }) + .transform(reactify) // Transform JSX + .external('react'); + + // The actual rebundle process + var rebundle = function () { + var start = Date.now(); + bundler.bundle() + .on('error', gutil.log) + .pipe(source(options.name)) + .pipe(gulpif(options.uglify, streamify(uglify()))) + .pipe(gulp.dest(options.dest)) + .pipe(notify(function () { + + // Fix for requirejs + var fs = require('fs'); + var file = fs.readFileSync(options.dest + '/' + options.name).toString(); + file = file.replace('define([],e)', 'define(["react"],e)'); + fs.writeFileSync(options.dest + '/' + options.name, file); + + console.log('Built in ' + (Date.now() - start) + 'ms'); + + })); + + }; + + // Fire up Watchify when developing + if (options.watch) { + bundler = watchify(bundler); + bundler.on('update', rebundle); + } + + return rebundle(); + +}; + +gulp.task('default', function () { + + runBrowserifyTask({ + watch: true, + dest: './build', + uglify: false, + debug: true, + name: 'formsy-react.js' + }); + +}); + +gulp.task('deploy', function () { + + runBrowserifyTask({ + watch: false, + dest: './releases/' + package.version, + uglify: true, + debug: false, + name: 'formsy-react-' + package.version + '.min.js' + }); + + runBrowserifyTask({ + watch: false, + dest: './releases/' + package.version, + uglify: false, + debug: false, + name: 'formsy-react-' + package.version + '.js' + }); + +}); + +gulp.task('test', shell.task([ + './node_modules/.bin/jasmine-node ./specs --autotest --watch ./src --color' +])); \ No newline at end of file diff --git a/README.md b/README.md index c063b9d..1f293ac 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,448 @@ formsy-react ============ A form input builder and validator for React JS + +- [Background](#background) +- [What you can do](#whatyoucando) +- [Install](#install) +- [How to use](#howtouse) +- [API](#API) + - [Formsy.defaults](#formsydefaults) + - [Formsy.Form](#formsyform) + - [className](#classname) + - [url](#url) + - [method](#method) + - [contentType](#contenttype) + - [showCancel](#showcancel) + - [hideSubmit](#hideSubmit) + - [submitButtonClass](#submitButtonClass) + - [cancelButtonClass](#cancelButtonClass) + - [buttonWrapperClass](#buttonWrapperClass) + - [onSuccess()](#onsuccess) + - [onSubmit()](#onsubmit) + - [onSubmitted()](#onsubmitted) + - [onCancel()](#oncancel) + - [onError()](#onerror) + - [Formsy.Mixin](#formsymixin) + - [name](#name) + - [validations](#validations) + - [validationError](#validationerror) + - [required](#required) + - [getValue()](#getvalue) + - [setValue()](#setvalue) + - [getErrorMessage()](#geterrormessage) + - [isValid()](#isvalid) + - [isRequired()](#isrequired) + - [showRequired()](#showrequired) + - [showError()](#showerror) + - [Formsy.addValidationRule](#formsyaddvalidationrule) +- [Validators](#validators) +## Background +I wrote an article on forms and validation with React JS, [Nailing that validation with React JS](), the result of that was this extension. + +The main concept is that forms, inputs and validation is done very differently across developers and projects. This extension to React JS aims to be that "sweet spot" between flexibility and reusability. + +## What you can do + + 1. Build any kind of form input components. Not just traditional inputs, but anything you want and get that validation for free + + 2. Add validation rules and use them with simple syntax + + 3. Use handlers for different states of your form. Ex. "onSubmit", "onError" etc. + + 4. Server validation errors automatically binds to the correct form input component + +## Install + + 1. Download from this REPO and use globally (Formsy) or with requirejs + 2. Install with `npm install formsy-react` and use with browserify etc. + +## How to use + +#### Formsy gives you a form straight out of the box + +```javascript + /** @jsx React.DOM */ + var Formsy = require('formsy-react'); + var MyAppForm = React.createClass({ + changeUrl: function () { + location.href = '/success'; + }, + render: function () { + return ( + + + + + + ); + } + }); +``` + +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 what you can enjoy building +```javascript + /** @jsx React.DOM */ + var Formsy = require('formsy-react'); + var MyOwnInput = React.createClass({ + + // Add the Formsy Mixin + mixins: [Formsy.Mixin], + + // setValue() will set the value of the component, which in + // turn will validate it and the rest of the form + changeValue: function (event) { + this.setValue(event.currentTarget.value); + }, + + // Set a specific className based on the validation + // state of this component + setClassName: function () { + var className = 'input-wrapper'; + + // showRequired() is true when the value is empty and the + // required prop is passed to the input + if (this.showRequired()) { + className += ' required'; + + // showError() is true when the value typed is invalid + } else if (this.showError()) { + className += ' error'; + } + + return className; + }, + render: function () { + var className = this.setClassName(); + + // An error message is returned ONLY if there is an invalid value based + // on validation rule or the server has returned an error message + var errorMessage = this.getErrorMessage(); + return ( +
+ + {errorMessage} +
+ ); + } + }); +``` +So this is basically how you build your form elements. As you can see it is very flexible, you just have a small API to help you identify the state of the component and set its value. + +## API + +### Formsy.defaults(options) +```javascript +Formsy.defaults({ + contentType: 'urlencoded', // default: 'json' + hideSubmit: true, // default: false + showCancel: true, // default: false + submitButtonClass: 'btn btn-success', // default: null + resetButtonClass: 'btn btn-default', // default: null + buttonWrapperClass: 'my-wrapper' // default: null +}); +``` +Use **defaults** to set general settings for all your forms. + +### Formsy.Form + +#### className +```html + +``` +Sets a class name on the form itself. + +#### url +```html + +``` +Will either **POST** or **PUT** to the url specified when submitted. + +#### method +```html + +``` +Supports **POST** (default) and **PUT**. + +#### contentType +```html + +``` +Supports **json** (default) and **urlencoded** (x-www-form-urlencoded). + +**Note!** Response has to be **json**. + +#### showCancel +```html + +``` +Shows the cancel button that runs the **onCancel** handler. + +#### hideSubmit +```html + +``` +Hides the submit button. Submit is done by ENTER on an input. + +#### submitButtonClass +```html + +``` +Sets a class name on the submit button. + +#### cancelButtonClass +```html + +``` +Sets a class name on the reset button. + +#### buttonWrapperClass +```html + +``` +Sets a class name on the container that wraps the **submit** and **reset** buttons. + +#### onSuccess(serverResponse) +```html + +``` +Takes a function to run when the server has responded with a success http status code. + +#### onSubmit() +```html + +``` +Takes a function to run when the submit button has been clicked. + +#### onSubmitted() +```html + +``` +Takes a function to run when either a success or error response is received from the server. + +#### onError(serverResponse) +```html + +``` +Takes a function to run when the server responds with an error http status code. + +### Formsy.Mixin + +#### name +```html + +``` +The name is required to register the form input component in the form. + +#### validations +```html + + +``` +An comma seperated list with validation rules. Take a look at **Formsy.addValidationRule()** to see default rules. Use ":" to separate arguments passed to the validator. The arguments will go through a **JSON.parse** converting them into correct JavaScript types. Meaning: + +```html + + +``` +Works just fine. + +#### validationError +```html + +``` +The message that will show when the form input component is invalid. + +#### required +```html + +``` +A property that tells the form that the form input component value is required. + +#### getValue() +```javascript +var MyInput = React.createClass({ + mixins: [Formsy.Mixin], + render: function () { + return ( + + ); + } +}); +``` +Gets the current value of the form input component. + +#### setValue(value) +```javascript +var MyInput = React.createClass({ + changeValue: function (event) { + this.setValue(event.currentTarget.value); + }, + render: function () { + return ( + + ); + } +}); +``` +Sets the value of your form input component. Notice that it does not have to be a text input. Anything can set a value on the component. Think calendars, checkboxes, autocomplete stuff etc. + +#### getErrorMessage() +```javascript +var MyInput = React.createClass({ + changeValue: function (event) { + this.setValue(event.currentTarget.value); + }, + render: function () { + return ( +
+ + {this.getErrorMessage} +
+ ); + } +}); +``` +Will return the server error mapped to the form input component or return the validation message set if the form input component is invalid. If no server error and form input component is valid it returns **null**. + +#### isValid() +```javascript +var MyInput = React.createClass({ + changeValue: function (event) { + this.setValue(event.currentTarget.value); + }, + render: function () { + var face = this.isValid() ? ':-)' : ':-('; + return ( +
+ {face} + + {this.getErrorMessage} +
+ ); + } +}); +``` +Returns the valid state of the form input component. + +#### isRequired() +```javascript +var MyInput = React.createClass({ + changeValue: function (event) { + this.setValue(event.currentTarget.value); + }, + render: function () { + return ( +
+ {this.props.label} {this.isRequired() ? '*' : null} + + {this.getErrorMessage} +
+ ); + } +}); +``` +Returns true if the required property has been passed. + +#### showRequired() +```javascript +var MyInput = React.createClass({ + changeValue: function (event) { + this.setValue(event.currentTarget.value); + }, + render: function () { + var className = this.showRequired() ? 'required' : ''; + return ( +
+ + {this.getErrorMessage} +
+ ); + } +}); +``` +Lets you check if the form input component should indicate if it is a required field. This happens when the form input component value is empty and the required prop has been passed. + +#### showError() +```javascript +var MyInput = React.createClass({ + changeValue: function (event) { + this.setValue(event.currentTarget.value); + }, + render: function () { + var className = this.showRequired() ? 'required' : this.showError() ? 'error' : ''; + return ( +
+ + {this.getErrorMessage} +
+ ); + } +}); +``` +Lets you check if the form input component should indicate if there is an error. This happens if there is a form input component value and it is invalid or if a server error is received. + +### Formsy.addValidationRule(name, ruleFunc) +An example: +```javascript +Formsy.addValidationRule('isFruit', function (value) { + return ['apple', 'orange', 'pear'].indexOf(value) >= 0; +}); +``` +```html + +``` +Another example: +```javascript +Formsy.addValidationRule('isIn', function (value, array) { + return array.indexOf(value) >= 0; +}); +``` +```html + +``` +## Validators +**isValue** +```html + +``` +Returns true if the value is thruthful + +**isEmail** +```html + +``` +Return true if it is an email + +**isTrue** +```html + +``` +Returns true if the value is the boolean true + +**isNumeric** +```html + +``` +Returns true if string only contains numbers + +**isAlpha** +```html + +``` +Returns true if string is only letters + +**isLength:min**, **isLength:min:max** +```html + + +``` +Returns true if the value length is the equal or more than minimum and equal or less than maximum, if maximum is passed + +**equals:value** +```html + +``` +Return true if the value from input component matches value passed (==). \ No newline at end of file diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..97457bb --- /dev/null +++ b/bower.json @@ -0,0 +1,8 @@ +{ + "name": "formsy-react", + "version": "0.1.0", + "main": "src/main.js", + "dependencies": { + "react": "^0.11.2" + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..d27880f --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "formsy-react", + "version": "0.1.0", + "description": "A form input builder and validator for React JS", + "main": "src/main.js", + "scripts": { + "test": "./node_modules/.bin/jasmine-node ./specs" + }, + "author": "Christian Alfoni", + "license": "MIT", + "devDependencies": { + "browserify": "^5.11.2", + "gulp": "^3.8.8", + "gulp-if": "^1.2.4", + "gulp-notify": "^1.6.0", + "gulp-shell": "^0.2.9", + "gulp-streamify": "0.0.5", + "gulp-uglify": "^1.0.1", + "gulp-util": "^3.0.1", + "react": "^0.11.2", + "reactify": "^0.14.0", + "vinyl-source-stream": "^1.0.0", + "watchify": "^1.0.2" + } +} diff --git a/releases/0.1.0/formsy-react-0.1.0.js b/releases/0.1.0/formsy-react-0.1.0.js new file mode 100755 index 0000000..df5601d --- /dev/null +++ b/releases/0.1.0/formsy-react-0.1.0.js @@ -0,0 +1,341 @@ +!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); + }, + + // 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)); + }, + getValue: 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.length; + }, + 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.name) { + child.props._attachToForm = this.attachToForm; + child.props._detachFromForm = this.detachFromForm; + child.props._validate = this.validate; + } + + if (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.resetButtonClass || options.resetButtonClass + }, 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.showCancel || options.showCancel ? 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.1.0/formsy-react-0.1.0.min.js b/releases/0.1.0/formsy-react-0.1.0.min.js new file mode 100755 index 0000000..984d46b --- /dev/null +++ b/releases/0.1.0/formsy-react-0.1.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 s(o,u){if(!i[o]){if(!e[o]){var a="function"==typeof require&&require;if(!u&&a)return a(o,!0);if(r)return r(o,!0);var p=new Error("Cannot find module '"+o+"'");throw p.code="MODULE_NOT_FOUND",p}var l=i[o]={exports:{}};e[o][0].call(l.exports,function(t){var i=e[o][1][t];return s(i?i:t)},l,l.exports,t,e,i,n)}return i[o].exports}for(var r="function"==typeof require&&require,o=0;o=e&&t.length<=i:t.length>=e},equals:function(t,e){return t==e}},o=function(t,e,i){var i=i||[];if("object"==typeof t)for(var n in t)o(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):o(i),new Promise(function(s,r){try{var o=new XMLHttpRequest;o.open(t,e,!0),o.setRequestHeader("Accept","application/json"),o.setRequestHeader("Content-Type",n),o.onreadystatechange=function(){4===o.readyState&&(o.status>=200&&o.status<300?s(o.responseText?JSON.parse(o.responseText):null):r(o.responseText?JSON.parse(o.responseText):null))},o.send(i)}catch(u){r(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)},componentWillUnmount:function(){this.props._detachFromForm(this)},setValue:function(t){this.setState({_value:t},function(){this.props._validate(this)}.bind(this))},getValue: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.length},showError:function(){return!this.showRequired()&&!this.state._isValid}},s.addValidationRule=function(t,e){r[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.name&&(t.props._attachToForm=this.attachToForm,t.props._detachFromForm=this.detachFromForm,t.props._validate=this.validate),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),!r[s])throw new Error("Formsy does not have the validation rule: "+s);r[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.resetButtonClass||p.resetButtonClass},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.showCancel||p.showCancel?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 new file mode 100644 index 0000000..0f348d7 --- /dev/null +++ b/src/main.js @@ -0,0 +1,336 @@ +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) { + + 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); + }, + + // 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)); + }, + getValue: 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.length; + }, + 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.name) { + child.props._attachToForm = this.attachToForm; + child.props._detachFromForm = this.detachFromForm; + child.props._validate = this.validate; + } + + if (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.resetButtonClass || options.resetButtonClass + }, 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.showCancel || options.showCancel ? 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; \ No newline at end of file