├── .babelrc ├── .gitignore ├── .npmignore ├── .editorconfig ├── test ├── .setup.js └── components │ └── connectField.js ├── src ├── index.js ├── connectField.js ├── FieldTypes.js ├── Field.js └── Form.js ├── todo.md ├── examples ├── basic-state-access │ └── index.js ├── cli.js ├── index.html ├── server.js ├── validation │ └── index.js ├── initial-values │ └── index.js ├── basic-usage │ └── index.js ├── submit-handling │ └── index.js ├── reset-form │ └── index.js ├── field-wrap-hoc │ └── index.js ├── field-wraps │ └── index.js └── full-featured │ └── index.js ├── webpack.config.js ├── LICENSE.md ├── package.json └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | npm-debug.log 5 | dist 6 | sandbox 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | src 3 | .babelrc 4 | .editorconfig 5 | .npmignore 6 | npm-debug.log 7 | todo.md 8 | webpack.* 9 | test 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /test/.setup.js: -------------------------------------------------------------------------------- 1 | require('babel-register')(); 2 | var jsdom = require('jsdom'); 3 | 4 | const doc = jsdom.jsdom('') 5 | const win = doc.defaultView 6 | 7 | global.document = doc 8 | global.window = win 9 | global.navigator = win.navigator 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Form from './Form' 2 | import Field from './Field' 3 | import connectField from './connectField' 4 | import { InputField, CheckboxField, RadioField, SelectField, TextareaField } from './FieldTypes' 5 | 6 | export { 7 | Form, 8 | Field, 9 | connectField, 10 | InputField, 11 | CheckboxField, 12 | RadioField, 13 | SelectField, 14 | TextareaField 15 | } 16 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # Todo's 2 | 3 | - Form's call to `setState` after form has unmounted issues a warning 4 | - To circumvent HTML form validation, we may need to provide a `noValidate` on the `
` when a validation method is passed in. Otherwise our `onSubmit` does not get called 5 | - Need to make an ability for fields to have multiple/custom values 6 | - Build other input field types 7 | - textarea 8 | - select 9 | - radio 10 | - checkbox etc 11 | -------------------------------------------------------------------------------- /examples/basic-state-access/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Form, Field } from 'src' 4 | 5 | const InputField = props => { 6 | const { name, type, events, fieldState, formState } = props 7 | 8 | // Access to field and form state 9 | console.log('Field State', fieldState) 10 | console.log('Form State State', formState) 11 | 12 | return 13 | } 14 | 15 | const Example = props => ( 16 | 17 | 18 | 19 | ) 20 | 21 | ReactDOM.render(, document.getElementById('root')) 22 | -------------------------------------------------------------------------------- /examples/cli.js: -------------------------------------------------------------------------------- 1 | var inquirer = require('inquirer') 2 | 3 | var examples = { 4 | 'Basic Usage': 'basic-usage', 5 | 'Basic State Access': 'basic-state-access', 6 | 'Submit Handling': 'submit-handling', 7 | 'Validation': 'validation', 8 | 'Initial Values': 'initial-values', 9 | 'Reset Form': 'reset-form', 10 | 'Field Wraps': 'field-wraps', 11 | 'Field Wraps with connectField HoC': 'field-wrap-hoc', 12 | 'Full Featured Example': 'full-featured' 13 | } 14 | 15 | inquirer.prompt([{ 16 | type: 'list', 17 | name: 'example', 18 | message: 'Which Example?', 19 | choices: Object.keys(examples) 20 | }]).then(function(answers) { 21 | process.env.EXAMPLE = examples[answers.example] 22 | var server = require('./server') 23 | }) 24 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example Form 6 | 7 | 33 | 34 | 35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var express = require('express') 3 | var webpack = require('webpack') 4 | var config = require('../webpack.config') 5 | 6 | var app = express() 7 | var compiler = webpack(config) 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })) 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)) 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'index.html')) 18 | }) 19 | 20 | app.listen(3030, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err) 23 | return 24 | } 25 | console.log('Listening at http://localhost:3030\nWaiting for build...') 26 | }) 27 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | var examplePath = process.env.EXAMPLE 5 | 6 | module.exports = { 7 | devtool: 'inline-source-map', 8 | entry: [ 9 | 'webpack-hot-middleware/client', 10 | path.resolve(__dirname, 'examples', examplePath, 'index.js') 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/dist/' 16 | }, 17 | plugins: [ 18 | new webpack.HotModuleReplacementPlugin() 19 | ], 20 | resolve: { 21 | alias: { 22 | 'src': path.resolve('./src') 23 | } 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.js$/, 29 | loaders: 'babel-loader', 30 | exclude: /node_mudles/ 31 | } 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/validation/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Form, Field } from 'src' 4 | 5 | class LoginForm extends React.Component { 6 | 7 | validate(values) { 8 | const errors = {} 9 | if (!/^[\w\d\.]+@[\w\d]+\.[\w]{2,9}$/.test(values.email)) errors.email = 'Invalid Email' 10 | if (!/^[\w\d]{6,20}$/.test(values.password)) errors.password = 'Invalid Password' 11 | console.log('Validation Errors', errors) 12 | return errors 13 | } 14 | 15 | render() { 16 | return ( 17 |
18 |
19 |
20 | 21 | 22 | ) 23 | } 24 | } 25 | 26 | ReactDOM.render(, document.getElementById('root')) 27 | -------------------------------------------------------------------------------- /examples/initial-values/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Form, Field } from 'src' 4 | 5 | class EditUser extends React.Component { 6 | 7 | constructor() { 8 | super() 9 | this.state = {} 10 | } 11 | 12 | componentWillMount() { 13 | // Simulate network latency 14 | setTimeout(() => { 15 | this.setState({ 16 | email: 'example@example.com', 17 | password: 'abc123' 18 | }) 19 | }, 500) 20 | } 21 | 22 | render() { 23 | return ( 24 |
25 |
26 |
27 | 28 | 29 | ) 30 | } 31 | } 32 | 33 | ReactDOM.render(, document.getElementById('root')) 34 | -------------------------------------------------------------------------------- /examples/basic-usage/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Form, Field } from 'src' 4 | 5 | const RegistrationForm = props => ( 6 |
7 |

This form is really, truely "basic". It's simply the API's version of an HTML form that submits using HTML submissions with no JavaScript interuption

8 |
9 |
10 | 11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 | 19 | ) 20 | 21 | ReactDOM.render(, document.getElementById('root')) 22 | -------------------------------------------------------------------------------- /examples/submit-handling/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Form, Field } from 'src' 4 | 5 | class LoginForm extends React.Component { 6 | 7 | onSubmit(values, formState) { 8 | console.log('Values', values) 9 | console.log('Form State', formState) 10 | 11 | // Always return a promise to let the API know the status 12 | // of your submission. Typically you'll probably do some 13 | // sort of async operation here like a network request, so 14 | // a resolved promise will tell the API that everything went 15 | // well or not 16 | return Promise.resolve() 17 | } 18 | 19 | render() { 20 | return ( 21 |
22 |
23 |
24 | 25 | 26 | ) 27 | } 28 | } 29 | 30 | ReactDOM.render(, document.getElementById('root')) 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Brad Westfall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /examples/reset-form/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Form, Field } from 'src' 4 | 5 | class UpdatePassword extends React.Component { 6 | static _myform 7 | 8 | constructor() { 9 | super() 10 | this.resetForm = this.resetForm.bind(this) 11 | } 12 | 13 | resetForm() { 14 | // The
component has a `resetForm` method that you can access via refs 15 | this._myform.resetForm() 16 | } 17 | 18 | validate(values) { 19 | const errors = {} 20 | // Allows us to see values as the user types for testing 21 | console.log('Validate', values) 22 | return errors 23 | } 24 | 25 | render() { 26 | return ( 27 | {this._myform = form}} validate={this.validate}> 28 |
29 |
30 | 31 | 32 | 33 | 34 |
35 |
36 | 37 |
38 | 39 | 40 | ) 41 | } 42 | } 43 | 44 | ReactDOM.render(, document.getElementById('root')) 45 | -------------------------------------------------------------------------------- /examples/field-wrap-hoc/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Form, connectField } from 'src' 4 | 5 | const InputField = props => { 6 | const { name, type, input } = props 7 | return 8 | } 9 | 10 | const FieldWrap = props => { 11 | const { input, fieldState, formState, label, type, name } = props 12 | 13 | // Access to field and form state 14 | console.log('Field State', fieldState) 15 | console.log('Form State State', formState) 16 | 17 | return ( 18 |
19 | 20 |
21 | 22 |
23 |
24 | {fieldState.error} 25 |
26 |
27 | ) 28 | } 29 | 30 | // High-level abstraction fields created with `connectField` HoC 31 | const FieldEmail = connectField('email')(FieldWrap) 32 | 33 | class LoginForm extends React.Component { 34 | 35 | validate(values) { 36 | const errors = {} 37 | if (!/^[\w\d\.]+@[\w\d]+\.[\w]{2,9}$/.test(values.email)) errors.email = 'Invalid Email' 38 | return errors 39 | } 40 | 41 | render() { 42 | 43 | // The password field was left out of this example on purpose to limit 44 | // the amount of console logs for state changes. 45 | 46 | return ( 47 |
48 | 49 | 50 | 51 | ) 52 | } 53 | } 54 | 55 | ReactDOM.render(, document.getElementById('root')) 56 | -------------------------------------------------------------------------------- /examples/field-wraps/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Form, Field, InputField } from 'src' 4 | 5 | const FieldWrap = props => { 6 | const { label, component: Component, children, name, value, ...rest } = props 7 | 8 | return ( 9 | { 10 | 11 | // Access to field and form state 12 | console.log('Field State', fieldState) 13 | console.log('Form State State', formState) 14 | 15 | return ( 16 |
17 | 18 |
19 | 20 | {children} 21 | 22 |
23 |
24 | {fieldState.error} 25 |
26 |
27 | ) 28 | 29 | }} /> 30 | ) 31 | } 32 | 33 | class LoginForm extends React.Component { 34 | 35 | validate(values) { 36 | const errors = {} 37 | if (!/^[\w\d\.]+@[\w\d]+\.[\w]{2,9}$/.test(values.email)) errors.email = 'Invalid Email' 38 | return errors 39 | } 40 | 41 | render() { 42 | 43 | // The password field was left out of this example on purpose to limit 44 | // the amount of console logs for state changes. 45 | 46 | return ( 47 |
48 | 49 | 50 | 51 | ) 52 | } 53 | } 54 | 55 | ReactDOM.render(, document.getElementById('root')) 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "informative", 3 | "version": "0.6.3", 4 | "description": "React forms with internal state-management", 5 | "main": "dist/index.js", 6 | "repository": { 7 | "url": "https://github.com/bradwestfall/informative.git" 8 | }, 9 | "keywords": [ 10 | "react", 11 | "forms", 12 | "state" 13 | ], 14 | "author": "Brad Westfall", 15 | "license": "MIT", 16 | "scripts": { 17 | "prepare": "npm run build", 18 | "build": "npm run clean && cross-env babel src --out-dir dist", 19 | "clean": "rimraf dist/*", 20 | "example": "npm run examples", 21 | "examples": "cross-env node examples/cli.js", 22 | "example:server": "cross-env EXAMPLE=$form node examples/server.js", 23 | "example:basic-usage": "form=basic-usage npm run example:server", 24 | "example:basic-state-access": "form=basic-state-access npm run example:server", 25 | "example:submit-handling": "form=submit-handling npm run example:server", 26 | "example:validation": "form=validation npm run example:server", 27 | "example:initial-values": "form=initial-values npm run example:server", 28 | "example:reset-form": "form=reset-form npm run example:server", 29 | "example:field-wraps": "form=field-wraps npm run example:server", 30 | "example:field-wrap-hoc": "form=field-wrap-hoc npm run example:server", 31 | "example:full-featured": "form=full-featured npm run example:server", 32 | "test": "cross-env mocha test/.setup.js test/**/**.js", 33 | "sandbox": "EXAMPLE=sandbox node examples/server.js" 34 | }, 35 | "devDependencies": { 36 | "babel-cli": "^6.23.0", 37 | "babel-core": "^6.23.1", 38 | "babel-loader": "^6.3.2", 39 | "babel-preset-es2015": "^6.22.0", 40 | "babel-preset-react": "^6.23.0", 41 | "babel-preset-stage-2": "^6.22.0", 42 | "babel-register": "^6.24.0", 43 | "chai": "^3.5.0", 44 | "cross-env": "^3.2.3", 45 | "enzyme": "^2.7.1", 46 | "express": "^4.15.2", 47 | "inquirer": "^3.0.6", 48 | "jsdom": "^9.12.0", 49 | "lodash": "^4.17.4", 50 | "mocha": "^3.2.0", 51 | "prop-types": "^15.5.10", 52 | "react": "^15.4.2", 53 | "react-addons-test-utils": "^15.4.2", 54 | "react-dom": "^15.4.2", 55 | "rimraf": "^2.6.1", 56 | "webpack": "^2.2.1", 57 | "webpack-dev-middleware": "^1.10.1", 58 | "webpack-hot-middleware": "^2.17.1" 59 | }, 60 | "dependencies": {} 61 | } 62 | -------------------------------------------------------------------------------- /src/connectField.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const connectField = name => { 5 | return WrappedComponent => { 6 | return class extends Component { 7 | 8 | constructor() { 9 | super() 10 | this.onChange = this.onChange.bind(this) 11 | this.updateFieldState = this.updateFieldState.bind(this) 12 | } 13 | 14 | componentWillMount() { 15 | this.context.registerField(name, this.props.value) 16 | } 17 | 18 | static contextTypes = { 19 | registerField: PropTypes.func, 20 | getFormState: PropTypes.func, 21 | setFieldState: PropTypes.func, 22 | onChange: React.PropTypes.func, 23 | } 24 | 25 | // Prop Change for `value` 26 | componentWillReceiveProps(nextProps) { 27 | if (nextProps.value === this.props.value) return false 28 | this.updateFieldState({ value: nextProps.value }) 29 | } 30 | 31 | // DOM Change 32 | onChange(e) { 33 | const { type, target } = e 34 | const value = target.type === 'checkbox' ? target.checked : target.value 35 | this.updateFieldState({ value, dirty: true }) 36 | } 37 | 38 | updateFieldState(newState) { 39 | const { onChange } = this.props 40 | this.context.setFieldState(name, newState, formState => { 41 | if (onChange) onChange(e, formState) // call the field's onChange if the user provided one 42 | this.context.onChange(name, formState) // call the form's onChange if the user provided one 43 | }) 44 | } 45 | 46 | render() { 47 | // Bail if name not provided 48 | if (!name) throw new Error('You must provide a `name` to `connectField`. Usage `connectField(name)(Component)`') 49 | const formState = this.context.getFormState() || {} 50 | const fieldState = formState.fields[name] 51 | 52 | // Don't render if fieldState hasn't been setup 53 | if (!fieldState) return null 54 | 55 | const input = { 56 | name, 57 | value: fieldState.value, 58 | onChange: this.onChange, 59 | onFocus: e => this.context.setFieldState(name, { visited: true, active: true }), 60 | onBlur: e => this.context.setFieldState(name, { active: false, touched: true }) 61 | } 62 | 63 | return ; 64 | } 65 | 66 | } 67 | } 68 | } 69 | 70 | export default connectField 71 | -------------------------------------------------------------------------------- /examples/full-featured/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Form, Field, InputField } from 'src' 4 | 5 | const FieldWrap = props => { 6 | const { label, component: Component, children, name, value, ...rest } = props 7 | 8 | return ( 9 | ( 10 |
11 | 12 |
13 | 14 | {children} 15 | 16 |
17 |
18 | {fieldState.error} 19 |
20 |
21 | )} /> 22 | ) 23 | } 24 | 25 | // High-level Field Abstractions 26 | const FieldFirstName = props => 27 | const FieldLastName = props => 28 | const FieldEmail = props => 29 | const FieldPassword = props => 30 | 31 | class UserForm extends React.Component { 32 | 33 | constructor() { 34 | super() 35 | this.state = {} 36 | } 37 | 38 | componentWillMount() { 39 | // Simulate network latency for asynchronous `initialValues` 40 | setTimeout(() => { 41 | this.setState({ 42 | firstName: 'Sally', 43 | lastName: 'Jane', 44 | email: 'example@example.com', 45 | password: 'abc123' 46 | }) 47 | }, 500) 48 | } 49 | 50 | validate(values) { 51 | const errors = {} 52 | if (!/^[\w\d\.]+@[\w\d]+\.[\w]{2,9}$/.test(values.email)) errors.email = 'Invalid Email' 53 | if (!/^[\w\d]{6,20}$/.test(values.password)) errors.password = 'Invalid Password' 54 | return errors 55 | } 56 | 57 | onSubmit(values, formState) { 58 | console.log('Submit Form', values) 59 | return Promise.resolve() 60 | } 61 | 62 | render() { 63 | return ( 64 |
( 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | )} /> 73 | ) 74 | } 75 | } 76 | 77 | ReactDOM.render(, document.getElementById('root')) 78 | -------------------------------------------------------------------------------- /src/FieldTypes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const cleanProps = props => { 5 | delete props.name 6 | delete props.originalValue 7 | delete props.formState 8 | delete props.fieldState 9 | delete props.events 10 | return props 11 | } 12 | 13 | /**************************************** 14 | 15 | *****************************************/ 16 | 17 | const InputField = ({ name, fieldState, events, ...rest }) => { 18 | const props = cleanProps(rest) 19 | return 20 | } 21 | 22 | InputField.propTypes = { 23 | name: PropTypes.string.isRequired, 24 | fieldState: PropTypes.object.isRequired, 25 | events: PropTypes.object.isRequired 26 | } 27 | 28 | /**************************************** 29 | 37 | } 38 | 39 | CheckboxField.propTypes = { 40 | name: PropTypes.string.isRequired, 41 | originalValue: PropTypes.any.isRequired, 42 | fieldState: PropTypes.object.isRequired, 43 | events: PropTypes.object.isRequired 44 | } 45 | 46 | /**************************************** 47 | 55 | } 56 | 57 | RadioField.propTypes = { 58 | name: PropTypes.string.isRequired, 59 | originalValue: PropTypes.any.isRequired, 60 | fieldState: PropTypes.object.isRequired, 61 | events: PropTypes.object.isRequired 62 | } 63 | 64 | /**************************************** 65 | {children} 71 | } 72 | 73 | SelectField.propTypes = { 74 | name: PropTypes.string.isRequired, 75 | fieldState: PropTypes.object.isRequired, 76 | events: PropTypes.object.isRequired 77 | } 78 | 79 | /**************************************** 80 |