├── .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 `
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |