├── .babelrc ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .istanbul.yml ├── .npmignore ├── LICENSE ├── README.md ├── bin └── guard.js ├── circle.yml ├── examples ├── async-example │ ├── .babelrc │ ├── .eslintrc │ ├── LICENSE │ ├── index.css │ ├── index.html │ ├── index.js │ ├── package.json │ ├── server.js │ └── webpack.config.babel.js ├── basic-example │ ├── .babelrc │ ├── .eslintrc │ ├── index.css │ ├── index.html │ ├── index.js │ ├── package.json │ ├── server.js │ └── webpack.config.babel.js ├── dynamic-fields │ ├── .babelrc │ ├── .eslintrc │ ├── index.css │ ├── index.html │ ├── index.js │ ├── package.json │ ├── server.js │ └── webpack.config.babel.js ├── foundation.css └── mation-example │ ├── .babelrc │ ├── .eslintrc │ ├── index.css │ ├── index.html │ ├── index.js │ ├── package.json │ ├── server.js │ └── webpack.config.babel.js ├── package.json ├── src ├── createValidate.js ├── disabledFormSubmit.js ├── feedbackFormSubmit.js ├── form.js ├── from.js ├── getValue.js ├── index.js └── resetFormButton.js ├── test ├── .eslintrc.json ├── creatValidate-test.js ├── disabledFormSubmit-test.js ├── feedbackFormSubmit-test.js ├── form-combination-test.js ├── form-test.js ├── from-test.js ├── getValue-test.js ├── mocha.opts ├── resetFormButton-test.js └── test_setup.js └── webpack.config.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | "plugins": ["transform-decorators-legacy"], 4 | "env": { 5 | "test": { 6 | "plugins": ["rewire"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "react/forbid-prop-types": 0, 10 | "no-unused-vars": [1, { "varsIgnorePattern": "ignored" }], 11 | "react/jsx-filename-extension": 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe 9 | esproposal.class_instance_fields=enable 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | lib 4 | dist 5 | npm-debug.log 6 | coverage 7 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | root: src 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | doc 2 | src 3 | examples 4 | .DS_Store 5 | .eslintrc 6 | .gitignore 7 | .npmignore 8 | webpack.config.js 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Adam Nalisnick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Circle CI](https://circleci.com/gh/theadam/react-inform.svg?style=shield)](https://circleci.com/gh/theadam/react-inform) 2 | # react-inform 3 | 4 | Forms are not currently fun to work with in React. There are a lot of form libraries out there, but a lot of them have issues making them a pain to work with. These issues include: 5 | 6 | * You have to use provided input / form components rather than whatever components you want. 7 | * The provided inputs can have bugs and inconsistencies with the built-in components. 8 | * The forms cannot be controlled from outside of the library's components and user input. 9 | * You are forced into using refs to call methods on components. 10 | * Validations are not straightforward, and you cannot validate across fields (like having two different inputs that should have the same value). 11 | 12 | `react-inform` is a form library for React that avoids all of these issues. 13 | 14 | ## Demos 15 | 16 | [Simple Form](http://theadam.github.io/react-inform/examples/basic-example/) 17 | 18 | [Simple Form With Animations Using mation](http://theadam.github.io/react-inform/examples/mation-example/) 19 | 20 | [Form with Async validation](http://theadam.github.io/react-inform/examples/async-example/) 21 | 22 | [Dynamic Fields](http://theadam.github.io/react-inform/examples/dynamic-fields/) 23 | 24 | [Integration with react-intl v2](https://jsfiddle.net/theadam/d0hypvtz/21/) 25 | 26 | ## Installation 27 | 28 | `npm install --save react-inform` 29 | 30 | ## Guide 31 | 32 | Creating a [simple validating form](https://jsfiddle.net/theadam/Lc3nkx7g/5/embedded/result%2Cjs%2Ccss%2Chtml%2Cresources/) is easy with `react-inform`. 33 | 34 | `react-inform` provides a simple decorator. To that decorator you provide a list of fields in the form, and an optional validate function. The `validate` function takes in an object where the keys are fieldnames and the values are the values of the fields, and it should return an object where the keys are fieldnames and the values are error strings. 35 | 36 | We can configure a simple form that has the fields `username`, `password`, and `confirmPassword`. This form will just validate that the `username` and `password` exist and that `confirmPassword` matches the password. First just create the fields and validate function. There is a helper function to aid in creating these validate functions, but for now, we will write one out by hand to get the hang of it. 37 | 38 | ```jsx 39 | const fields = ['username', 'password', 'confirmPassword']; 40 | 41 | const validate = values => { 42 | const { username, password, confirmPassword } = values; 43 | const errors = {}; 44 | 45 | if (!username) errors.username = 'Username is required!'; 46 | if (!password) errors.password = 'Password is required!'; 47 | if (confirmPassword !== password) errors.confirmPassword = 'Passwords must match!'; 48 | 49 | return errors; 50 | } 51 | ``` 52 | 53 | Now that you have the fields and validate function, you can just use the form decorator: 54 | 55 | ```jsx 56 | import { form } from 'react-inform'; 57 | 58 | @form({ 59 | fields, 60 | validate 61 | }) 62 | class MyForm extends Component { 63 | ... 64 | ``` 65 | 66 | Or you can use form as a function. 67 | 68 | ```jsx 69 | class MyForm extends Component { 70 | ... 71 | } 72 | 73 | MyForm = form({ 74 | fields, 75 | validate 76 | })(MyForm); 77 | ``` 78 | 79 | The form function wraps your react component passing a `form` and `fields` property. The `fields` property can be "plugged into" regular inputs in your render function. The fields also willl have errors if your validate function returned any! 80 | 81 | ```jsx 82 | 83 | {username.error} 84 | 85 | {password.error} 86 | 87 | {confirmPassword.error} 88 | ``` 89 | 90 | Simple! Your form now validates, and keeps track of its state without all the boilerplate! The complete working example can be seen [here!](https://jsfiddle.net/theadam/Lc3nkx7g/5/embedded/result%2Cjs%2Ccss%2Chtml%2Cresources/) 91 | 92 | ## Api 93 | 94 | ### form({ fields, [validate] }) 95 | 96 | Creates a function used to wrap a react component. Accepts an object that contains the keys `fields` and optionally `validate`. `fields` is an array of fieldnames. `validate` is a function used to validate the field values. 97 | 98 | The validate function should accept a map of fieldnames to values, and return a map of fieldnames to error strings (or a Promise that resolves to a map of fieldnames to error strings). 99 | 100 | #### Accepted properties 101 | 102 | | Property | Description | 103 | |---------|----------| 104 | | `value`| to control the data in the form from the parent of a form. | 105 | | `onChange`| to react to changes to the form in the parent of a form. | 106 | | `onValidate`| to react to changes in the validation state of the form. The callback will be passed a boolean that is `true` when the form becomes valid. | 107 | | `fields`| Can be used instead of the `field` key in the decorator to control the list of fields in the form | 108 | | `validate`| Can be used instead of the `validate` key in the decorator to control the validate function | 109 | | `touched`| An object to control the `touched` state of each field. The objects shape is `{ [fieldName]: [boolean], ... }` | 110 | | `onTouch`| An event fired anytime the touch state of any field in the form changes. The handler receives the entire touched state of the form, with the same shape as the value for the `touched` prop | 111 | 112 | #### Properties passed to wrapped components 113 | 114 | ##### form 115 | 116 | Form contains some utility functions to affect the wrapping form. 117 | 118 | | Function | Description | 119 | |----------|-------| 120 | | `isValid()`| returns true if all of the fields are valid | 121 | | `forceValidate()`| Causes all of the fields to get passed their error properties by setting every field's touched state to true. Usually errors are only passed after the field has been "touched" (after onBlur). | 122 | | `values()`| returns the current value of all the form fields as a map. | 123 | | `onValues(values)`| forcefully sets all of the values in the form to the passed values. | 124 | | `touch(fields)`| sets the touched state to true for all of the fields named in the passed array arg | 125 | | `untouch(fields)`| sets the touched state to false for all of the fields named in the passed array arg | 126 | | `resetTouched()`| sets the touched state for all fields to false | 127 | 128 | ##### fields 129 | 130 | `fields` is a map of the fields you passed in. Each field has a value, onChange, and onBlur property so that they can be passed into regular input components and have them be controlled with `react-inform`. If there is an error message on a field, the field will also have an error property. 131 | 132 | All of the props that should be passed to your rendered input component (value, onChange, and onBlur) are also available using the `props` property. For example: 133 | 134 | ```jsx 135 | const { fieldName } = this.props.fields; 136 | ... 137 | 138 | ``` 139 | 140 | This keeps react from complaining about unknown props being passed to input components. See [this link](https://gist.github.com/jimfb/d99e0678e9da715ccf6454961ef04d1b) for more details. 141 | 142 | ### createValidate(ruleMap) 143 | 144 | A helper to create `validate` functions from maps of rules. Rule functions can return Promises that resolve to booleans to support async validations. 145 | 146 | This is an example rule map that ensures that username exists, password exists, and confirmPassword matches password. Notice the keys to the rules are the error messages that will appear when the field is invalid. 147 | 148 | ```jsx 149 | const exists = v => Boolean(v); 150 | 151 | const ruleMap = { 152 | username: { 153 | 'Username must exist': exists 154 | }, 155 | password: { 156 | 'Password must exist': exists 157 | }, 158 | confirmPassword: { 159 | 'Passwords must match': (value, values) => value === values.password 160 | } 161 | }; 162 | ``` 163 | 164 | ### from(rulesMap) 165 | 166 | Creates an object that can be passed directly to the form function / decorator using a ruleMap. All fields must be represented in the ruleMap. Fields without validations should have empty objects as their values in the rule map. 167 | 168 | `@form(from(rulesMap))` 169 | 170 | ### ResetFormButton 171 | 172 | A Component which can be used inside a React Component wrapped with `react-inform`. When clicked it will reset the form so that it contains no values. 173 | 174 | Accepts a `resetTouched` boolean prop. When set to true, clicking the reset button will also reset all the fields' touched states. 175 | 176 | ### DisabledFormSubmit 177 | 178 | A Component which can be used inside a React Component wrapped with `react-inform`. It will remain disabled until all of the fields in the form are valid. Once its enabled, clicking it will submit the form. 179 | 180 | ### FeedbackFormSubmit 181 | 182 | A Component which can be used inside a React Component wrapped with `react-inform`. When clicked, if the form has invalid fields, it will force those fields' errors to be passed as props. If the form is valid, it will submit the form. 183 | 184 | ## License 185 | 186 | MIT 187 | -------------------------------------------------------------------------------- /bin/guard.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | import chokidar from 'chokidar'; 3 | import childProcess from 'child_process'; 4 | import fs from 'fs'; 5 | 6 | let proc; 7 | 8 | function toTest(path) { 9 | if (path.startsWith('test') && path.endsWith('test.js')) { 10 | return path; 11 | } 12 | if (!path.startsWith('src')) return undefined; 13 | 14 | const testPath = `test/${path.replace(/^src\//, '').replace(/\.js/, '-test.js')}`; 15 | return fs.existsSync(testPath) ? testPath : undefined; 16 | } 17 | 18 | chokidar.watch(['src', 'test']).on('change', path => { 19 | const testPath = toTest(path); 20 | if (!testPath) return; 21 | console.log(`Running Test file ${testPath}`); 22 | if (proc) proc.kill('SIGINT'); 23 | 24 | proc = childProcess.spawn('./node_modules/.bin/_mocha', [testPath], { stdio: 'inherit' }); 25 | proc.on('error', err => console.log(err)); 26 | }); 27 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6.3.0 4 | -------------------------------------------------------------------------------- /examples/async-example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | "plugins": ["transform-decorators-legacy"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/async-example/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "import/no-unresolved": 0, 10 | "react/prop-types": 0, 11 | "react/no-multi-comp": 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/async-example/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Adam Nalisnick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/async-example/index.css: -------------------------------------------------------------------------------- 1 | #container { 2 | margin-top: 40px; 3 | } 4 | 5 | .ui-hint { 6 | color: #DD0000; 7 | display: block; 8 | font-size: 1em; 9 | margin-top: -15px; 10 | } 11 | -------------------------------------------------------------------------------- /examples/async-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-inform-async-example 4 | 5 | 6 | Fork me on GitHub 7 |
8 |
9 |
10 |
11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/async-example/index.js: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | import '../foundation.css'; 3 | 4 | import React, { Component } from 'react'; 5 | import { render } from 'react-dom'; 6 | import alertify from 'alertify.js'; 7 | 8 | import { 9 | form, 10 | from, 11 | FeedbackFormSubmit, 12 | } from 'react-inform'; 13 | 14 | alertify.logPosition('top right'); 15 | 16 | function LabeledInput({ text, error, id, props }) { 17 | return ( 18 |
19 | 20 | 21 | {error} 22 |
23 | ); 24 | } 25 | 26 | function validateUsernameAsync(username) { 27 | return Promise.resolve().then(() => username.length > 3); 28 | } 29 | 30 | @form(from({ 31 | username: { 32 | 'Username is required': Boolean, 33 | 'Username must longer than 3 characters (async)': validateUsernameAsync, 34 | }, 35 | password: { 36 | 'Password is required': Boolean, 37 | }, 38 | })) 39 | class MyForm extends Component { 40 | handleSubmit(e) { 41 | e.preventDefault(); 42 | alertify.success('Yay, it submitted'); 43 | } 44 | 45 | render() { 46 | const { username, password } = this.props.fields; 47 | 48 | return ( 49 |
this.handleSubmit(e)}> 50 | 51 | 52 | alertify.error('Please fix errors!')} 56 | /> 57 | 58 | ); 59 | } 60 | } 61 | 62 | function App() { 63 | return ( 64 |
65 |
66 | 67 |
68 | 69 |
70 | ); 71 | } 72 | 73 | render(, document.getElementById('container')); 74 | -------------------------------------------------------------------------------- /examples/async-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-inform-async-example-example", 3 | "version": "1.0.0", 4 | "description": "async-example", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "babel-node server.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/theadam/react-inform.git" 12 | }, 13 | "license": "MIT", 14 | "devDependencies": { 15 | "babel-cli": "^6.11.4", 16 | "babel-core": "^6.11.4", 17 | "babel-eslint": "^6.1.2", 18 | "babel-loader": "^6.2.4", 19 | "babel-plugin-rewire": "^1.0.0-rc-4", 20 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 21 | "babel-preset-es2015": "^6.9.0", 22 | "babel-preset-react": "^6.11.1", 23 | "babel-preset-stage-0": "^6.5.0", 24 | "css-loader": "^0.23.1", 25 | "eslint": "^3.1.1", 26 | "eslint-config-airbnb": "^9.0.1", 27 | "eslint-plugin-import": "^1.11.1", 28 | "eslint-plugin-jsx-a11y": "^2.0.1", 29 | "eslint-plugin-react": "^5.2.2", 30 | "extract-text-webpack-plugin": "^1.0.1", 31 | "react": "^15.2.1", 32 | "style-loader": "^0.13.0", 33 | "webpack": "^1.13.1", 34 | "webpack-dev-server": "^1.14.1" 35 | }, 36 | "dependencies": { 37 | "alertify.js": "^1.0.10", 38 | "react": "^15.0.2", 39 | "react-dom": "^15.0.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/async-example/server.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import WebpackDevServer from 'webpack-dev-server'; 3 | import config from './webpack.config.babel'; 4 | 5 | config.entry.push('webpack-dev-server/client?http://localhost:3000'); 6 | config.entry.push('webpack/hot/only-dev-server'); 7 | 8 | config.plugins.push(new webpack.HotModuleReplacementPlugin()); 9 | 10 | new WebpackDevServer(webpack(config), { 11 | publicPath: config.output.publicPath, 12 | hot: true, 13 | historyApiFallback: true, 14 | stats: { 15 | colors: true, 16 | }, 17 | }).listen(3000, 'localhost', err => { 18 | if (err) { 19 | // eslint-disable-next-line no-console 20 | console.log(err); 21 | } 22 | 23 | // eslint-disable-next-line no-console 24 | console.log('Listening at localhost:3000'); 25 | }); 26 | -------------------------------------------------------------------------------- /examples/async-example/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | 4 | module.exports = { 5 | devtool: 'eval-source-map', 6 | entry: [ 7 | './index', 8 | ], 9 | output: { 10 | path: path.join(__dirname, 'dist'), 11 | filename: 'bundle.js', 12 | publicPath: '/dist', 13 | }, 14 | plugins: [ 15 | new webpack.NoErrorsPlugin(), 16 | ], 17 | resolve: { 18 | alias: { 19 | 'react-inform': path.join(__dirname, '..', '..', 'src'), 20 | }, 21 | extensions: ['', '.js'], 22 | }, 23 | module: { 24 | loaders: [{ 25 | test: /\.js$/, 26 | loaders: ['babel'], 27 | exclude: /node_modules/, 28 | include: __dirname, 29 | }, { 30 | test: /\.js$/, 31 | loaders: ['babel'], 32 | include: path.join(__dirname, '..', '..', 'src'), 33 | }, 34 | { 35 | test: /\.css$/, 36 | loader: 'style-loader!css-loader', 37 | }], 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /examples/basic-example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | "plugins": ["transform-decorators-legacy"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/basic-example/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "import/no-unresolved": 0, 10 | "react/prop-types": 0, 11 | "react/no-multi-comp": 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/basic-example/index.css: -------------------------------------------------------------------------------- 1 | #container { 2 | margin-top: 40px; 3 | } 4 | 5 | .ui-hint { 6 | color: #DD0000; 7 | display: block; 8 | font-size: 1em; 9 | margin-top: -15px; 10 | } 11 | -------------------------------------------------------------------------------- /examples/basic-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-inform-basic-example 4 | 5 | 6 | Fork me on GitHub 7 |
8 |
9 |
10 |
11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/basic-example/index.js: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | import '../foundation.css'; 3 | 4 | import React, { Component } from 'react'; 5 | import TextArea from 'react-textarea-autosize'; 6 | import { render } from 'react-dom'; 7 | import alertify from 'alertify.js'; 8 | import { isEmail } from 'string-validator'; 9 | import { 10 | form, 11 | from, 12 | DisabledFormSubmit, 13 | FeedbackFormSubmit, 14 | ResetFormButton, 15 | } from 'react-inform'; 16 | 17 | alertify.logPosition('top right'); 18 | 19 | function LabeledInput({ text, error, id, props }) { 20 | return ( 21 |
22 | 23 | 24 | {error} 25 |
26 | ); 27 | } 28 | 29 | const isRequired = value => value; 30 | 31 | const rulesMap = { 32 | checkbox: { 33 | 'Must be checked': v => v, 34 | }, 35 | username: { 36 | 'Username is required': isRequired, 37 | }, 38 | email: { 39 | 'Email is required': isRequired, 40 | 'Must be a valid email': isEmail(), 41 | }, 42 | password: { 43 | 'Password is required': isRequired, 44 | }, 45 | confirmPassword: { 46 | 'Passwords must match': (value, { password }) => value === password, 47 | }, 48 | }; 49 | 50 | @form(from(rulesMap)) 51 | class MyForm extends Component { 52 | handleSubmit(e) { 53 | e.preventDefault(); 54 | alertify.success('Awesome, it submitted!'); 55 | } 56 | 57 | render() { 58 | const { username, email, password, confirmPassword, checkbox } = this.props.fields; 59 | return ( 60 |
this.handleSubmit(e)}> 61 | 66 | 67 | 68 | 69 | 75 | 76 | alertify.error('Please fix errors!')} 80 | /> 81 | 82 | 83 | ); 84 | } 85 | } 86 | 87 | class App extends Component { 88 | state= { 89 | value: { 90 | checkbox: true, 91 | username: 'test user', 92 | email: 'badEmail', 93 | }, 94 | }; 95 | 96 | setValue(value) { 97 | this.setState({ value, typedValue: undefined }); 98 | } 99 | 100 | handleChange(e) { 101 | const { value } = e.target; 102 | this.setState({ typedValue: value }); 103 | 104 | try { 105 | const objValue = JSON.parse(value); 106 | this.setState({ value: objValue }); 107 | } catch (error) { 108 | // do nothing 109 | } 110 | } 111 | 112 | render() { 113 | const { value, typedValue } = this.state; 114 | return ( 115 |
116 |
117 | this.setValue(v)} value={value} /> 118 |
119 | Try editing this 120 |