├── .editorconfig ├── .gitignore ├── .travis.yml ├── .yarnrc ├── license.md ├── logo.png ├── package.json ├── packages ├── neoform-immutable-helpers │ ├── package.json │ ├── readme.md │ └── src │ │ └── index.js ├── neoform-plain-object-helpers │ ├── package.json │ ├── readme.md │ └── src │ │ └── index.js ├── neoform-validation │ ├── demo │ │ ├── Form │ │ │ └── index.jsx │ │ ├── Input │ │ │ └── index.jsx │ │ └── index.jsx │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── fieldValidation.js │ │ ├── formValidation.js │ │ └── index.js │ └── test │ │ ├── __snapshots__ │ │ └── demo.jsx.snap │ │ └── demo.jsx └── neoform │ ├── demo │ ├── Form │ │ └── index.jsx │ ├── Input │ │ └── index.jsx │ └── index.jsx │ ├── package.json │ ├── readme.md │ ├── src │ ├── field.js │ ├── form.js │ └── index.js │ └── test │ ├── __snapshots__ │ └── demo.jsx.snap │ └── demo.jsx ├── readme.md └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | es/ 4 | lib/ 5 | coverage/ 6 | .DS_Store 7 | *.log 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - node 5 | env: 6 | global: 7 | - PATH=$HOME/.yarn/bin:$PATH 8 | before_install: 9 | - curl -o- -L https://yarnpkg.com/install.sh | bash 10 | cache: 11 | yarn: true 12 | directories: 13 | - node_modules 14 | script: yarn start ci 15 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | workspaces-experimental true 2 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2017–present, Zero Plus X AB 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero-plus-x/neoform/8a49e2a66672b4687eceb37d2b75b90346d0d0b7/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "description": "Higher-Order Components for form state management and validation in React", 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "devDependencies": { 8 | "start-babel-cli": "^4.0.2", 9 | "start-deepsweet-react-components-monorepo-preset": "^0.0.14" 10 | }, 11 | "scripts": { 12 | "start": "start-runner --preset start-deepsweet-react-components-monorepo-preset" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/neoform-immutable-helpers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neoform-immutable-helpers", 3 | "library": "NeoFormImmutableHelpers", 4 | "version": "0.2.1", 5 | "description": "NeoForm helpers to work with Immutable.js state", 6 | "keywords": [ 7 | "neoform", 8 | "immutable" 9 | ], 10 | "main": "lib/index.js", 11 | "module": "es/index.js", 12 | "files": [ 13 | "dist/", 14 | "es/", 15 | "lib/" 16 | ], 17 | "repository": "zero-plus-x/neoform", 18 | "author": "Kir Belevich (https://github.com/deepsweet)", 19 | "license": { 20 | "type": "MIT", 21 | "url": "https://github.com/zero-plus-x/neoform/blob/master/license.md" 22 | }, 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "peerDependencies": { 27 | "neoform": "^0.3.0" 28 | }, 29 | "scripts": { 30 | "prepublishOnly": "cd ../..; yarn start build neoform-immutable-helpers" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/neoform-immutable-helpers/readme.md: -------------------------------------------------------------------------------- 1 | # neoform-immutable-helpers 2 | 3 | For more in depth documentation see https://github.com/zero-plus-x/neoform/blob/master/readme.md 4 | -------------------------------------------------------------------------------- /packages/neoform-immutable-helpers/src/index.js: -------------------------------------------------------------------------------- 1 | export const getValue = (target, name) => target.getIn(name.split('.')); 2 | export const setValue = (target, name, value) => target.setIn(name.split('.'), value); 3 | -------------------------------------------------------------------------------- /packages/neoform-plain-object-helpers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neoform-plain-object-helpers", 3 | "library": "NeoFormPlainObjectHelpers", 4 | "version": "0.2.1", 5 | "description": "NeoForm helpers to work with plain object state", 6 | "keywords": [ 7 | "neoform" 8 | ], 9 | "main": "lib/index.js", 10 | "module": "es/index.js", 11 | "files": [ 12 | "dist/", 13 | "es/", 14 | "lib/" 15 | ], 16 | "repository": "zero-plus-x/neoform", 17 | "author": "Kir Belevich (https://github.com/deepsweet)", 18 | "license": { 19 | "type": "MIT", 20 | "url": "https://github.com/zero-plus-x/neoform/blob/master/license.md" 21 | }, 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "peerDependencies": { 26 | "neoform": "^0.3.0" 27 | }, 28 | "dependencies": { 29 | "dot-prop-immutable": "^1.3.1" 30 | }, 31 | "scripts": { 32 | "prepublishOnly": "cd ../..; yarn start build neoform-plain-object-helpers" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/neoform-plain-object-helpers/readme.md: -------------------------------------------------------------------------------- 1 | # neoform-plain-object-helpers 2 | 3 | For more in depth documentation see https://github.com/zero-plus-x/neoform/blob/master/readme.md 4 | -------------------------------------------------------------------------------- /packages/neoform-plain-object-helpers/src/index.js: -------------------------------------------------------------------------------- 1 | import dotProp from 'dot-prop-immutable'; 2 | 3 | export const getValue = (data, name) => dotProp.get(data, name); 4 | export const setValue = (data, name, value) => dotProp.set(data, name, value); 5 | -------------------------------------------------------------------------------- /packages/neoform-validation/demo/Form/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-no-bind */ 2 | /* eslint-disable react/no-array-index-key */ 3 | import React from 'react'; 4 | 5 | import { form } from '../../../neoform/src'; 6 | import { formValidation } from '../../src'; 7 | 8 | import Input from '../Input'; 9 | 10 | const requiredValidator = (value) => { 11 | if (typeof value === 'undefined' || value === null || value === '') { 12 | return Promise.reject('required'); 13 | } 14 | 15 | return Promise.resolve('valid'); 16 | }; 17 | 18 | const MyForm = ({ 19 | data, 20 | validate, 21 | validationStatus, 22 | onSubmit, 23 | onInvalid 24 | }) => ( 25 |
{ 27 | e.preventDefault(); 28 | validate(onSubmit, onInvalid); 29 | }} 30 | > 31 |

personal data

32 |
33 | 37 |
38 |
39 | 43 |
44 |

friends

45 |
    46 | { 47 | data.friends.map((friend, index) => ( 48 |
  • 49 |
    50 | 54 |
    55 |
    56 | 60 |
    61 |
  • 62 | )) 63 | } 64 |
65 | 66 | { 67 | validationStatus === false && ( 68 |
Form is invalid
69 | ) 70 | } 71 |
72 | ); 73 | 74 | export default form(formValidation(MyForm)); 75 | -------------------------------------------------------------------------------- /packages/neoform-validation/demo/Input/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-no-bind */ 2 | import React from 'react'; 3 | 4 | import { field } from '../../../neoform/src'; 5 | import { fieldValidation } from '../../src'; 6 | 7 | const MyInput = ({ 8 | value = '', 9 | onChange, 10 | validate, 11 | validationStatus, 12 | validationMessage, 13 | ...props 14 | }) => { 15 | const style = { 16 | backgroundColor: validationStatus === false ? 'red' : 'white' 17 | }; 18 | 19 | return ( 20 | 21 | onChange(e.target.value)} 27 | /> 28 | {validationStatus === false && ( 29 | {validationMessage} 30 | )} 31 | 32 | ); 33 | }; 34 | 35 | export default field(fieldValidation(MyInput)); 36 | -------------------------------------------------------------------------------- /packages/neoform-validation/demo/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import React, { Component } from 'react'; 3 | 4 | import { getValue, setValue } from '../../neoform-plain-object-helpers/src'; 5 | import Form from './Form'; 6 | 7 | class Demo extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | firstName: 'John', 13 | lastName: 'Doe', 14 | friends: [ 15 | { 16 | firstName: 'Pepe', 17 | lastName: 'Sad' 18 | }, 19 | { 20 | firstName: '', 21 | lastName: 'Darkness' 22 | } 23 | ] 24 | }; 25 | this.onChange = this.onChange.bind(this); 26 | this.onInvalid = this.onInvalid.bind(this); 27 | this.onSubmit = this.onSubmit.bind(this); 28 | } 29 | 30 | onChange(name, value) { 31 | this.setState((prevState) => setValue(prevState, name, value)); 32 | } 33 | 34 | onInvalid() { 35 | console.log('invalid:', this.state); 36 | } 37 | 38 | onSubmit() { 39 | console.log('submit:', this.state); 40 | } 41 | 42 | render() { 43 | return ( 44 |
51 | ); 52 | } 53 | } 54 | 55 | export default Demo; 56 | -------------------------------------------------------------------------------- /packages/neoform-validation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neoform-validation", 3 | "library": "NeoFormValidation", 4 | "version": "0.6.1", 5 | "description": "Higher-Order Components for form validation in React", 6 | "keywords": [ 7 | "neoform", 8 | "validation" 9 | ], 10 | "main": "lib/index.js", 11 | "module": "es/index.js", 12 | "files": [ 13 | "dist/", 14 | "es/", 15 | "lib/" 16 | ], 17 | "repository": "zero-plus-x/neoform", 18 | "author": "Kir Belevich (https://github.com/deepsweet)", 19 | "license": { 20 | "type": "MIT", 21 | "url": "https://github.com/zero-plus-x/neoform/blob/master/license.md" 22 | }, 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "peerDependencies": { 27 | "neoform": "^0.3.0", 28 | "react": "^15.4.2 || ^16.0.0", 29 | "recompose": "^0.26.0", 30 | "prop-types": "^15.5.10" 31 | }, 32 | "dependencies": { 33 | "just-omit": "^1.0.1" 34 | }, 35 | "scripts": { 36 | "prepublishOnly": "cd ../..; yarn start build neoform-validation" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/neoform-validation/readme.md: -------------------------------------------------------------------------------- 1 | # neoform-validation 2 | 3 | For more in depth documentation see https://github.com/zero-plus-x/neoform/blob/master/readme.md 4 | -------------------------------------------------------------------------------- /packages/neoform-validation/src/fieldValidation.js: -------------------------------------------------------------------------------- 1 | import { createElement, Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { setDisplayName, wrapDisplayName } from 'recompose'; 4 | import omit from 'just-omit'; 5 | 6 | const fieldValidation = (Target) => { 7 | class FieldValidation extends Component { 8 | constructor(props, context) { 9 | super(props, context); 10 | 11 | this.validate = this.validate.bind(this); 12 | } 13 | 14 | componentDidMount() { 15 | if (this.props.validator) { 16 | this.context.neoform.registerValidator(this.props.name, this.props.validator); 17 | } 18 | } 19 | 20 | componentWillReceiveProps(nextProps) { 21 | // TODO: think about how to test this as a part of demo test suite 22 | if (nextProps.validator && nextProps.validator !== this.props.validator) { 23 | this.context.neoform.registerValidator(this.props.name, nextProps.validator); 24 | } 25 | } 26 | 27 | componentWillUnmount() { 28 | if (this.props.validator) { 29 | this.context.neoform.unregisterValidator(this.props.name); 30 | } 31 | } 32 | 33 | validate(event) { 34 | if (this.props.validator) { 35 | const type = event ? event.type : 'unknown'; 36 | 37 | this.context.neoform.validate(this.props.name, type); 38 | } 39 | } 40 | 41 | render() { 42 | const validation = this.context.neoform.getValidation(this.props.name); 43 | 44 | return createElement(Target, { 45 | ...omit(this.props, 'validator'), 46 | validate: this.validate, 47 | validationStatus: validation.status, 48 | validationMessage: validation.message 49 | }); 50 | } 51 | } 52 | 53 | FieldValidation.contextTypes = { 54 | neoform: PropTypes.object 55 | }; 56 | 57 | FieldValidation.propTypes = { 58 | validator: PropTypes.func 59 | }; 60 | 61 | if (process.env.NODE_ENV !== 'production') { 62 | return setDisplayName(wrapDisplayName(Target, 'fieldValidation'))(FieldValidation); 63 | } 64 | 65 | return FieldValidation; 66 | }; 67 | 68 | export default fieldValidation; 69 | -------------------------------------------------------------------------------- /packages/neoform-validation/src/formValidation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable promise/always-return */ 2 | import { createElement, Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { setDisplayName, wrapDisplayName } from 'recompose'; 5 | import omit from 'just-omit'; 6 | 7 | const isValidForm = (fields) => Object.keys(fields).every((name) => fields[name].status === true); 8 | 9 | const formValidation = (Target) => { 10 | class FormValidation extends Component { 11 | constructor(props, context) { 12 | super(props, context); 13 | 14 | this.validators = {}; 15 | this.state = { 16 | fields: {} 17 | }; 18 | this.getValidation = this.getValidation.bind(this); 19 | this.validateForm = this.validateForm.bind(this); 20 | this.validateField = this.validateField.bind(this); 21 | this.registerValidator = this.registerValidator.bind(this); 22 | this.unregisterValidator = this.unregisterValidator.bind(this); 23 | } 24 | 25 | getChildContext() { 26 | return { 27 | neoform: { 28 | ...this.context.neoform, 29 | getValidation: this.getValidation, 30 | validate: this.validateField, 31 | registerValidator: this.registerValidator, 32 | unregisterValidator: this.unregisterValidator 33 | } 34 | }; 35 | } 36 | 37 | registerValidator(name, validator) { 38 | this.validators[name] = validator; 39 | } 40 | 41 | unregisterValidator(name) { 42 | this.validators = omit(this.validators, name); 43 | 44 | this.setState((prevState) => ({ 45 | fields: omit(prevState.fields, name) 46 | })); 47 | } 48 | 49 | getValidation(name) { 50 | return this.state.fields[name] || {}; 51 | } 52 | 53 | validateField(name, type) { 54 | const validator = this.validators[name]; 55 | const value = this.context.neoform.getValue(name); 56 | 57 | validator(value, type) 58 | .then((message) => { 59 | this.setState((prevState) => ({ 60 | fields: { 61 | ...prevState.fields, 62 | [name]: { 63 | status: true, 64 | message 65 | } 66 | } 67 | })); 68 | }) 69 | .catch((message) => { 70 | this.setState((prevState) => ({ 71 | fields: { 72 | ...prevState.fields, 73 | [name]: { 74 | status: false, 75 | message 76 | } 77 | } 78 | })); 79 | }); 80 | } 81 | 82 | validateForm(successHandler, errorHandler) { 83 | const fields = {}; 84 | 85 | return Promise.all( 86 | Object.keys(this.validators) 87 | .map((name) => { 88 | const validator = this.validators[name]; 89 | const value = this.context.neoform.getValue(name); 90 | 91 | return validator(value, 'submit') 92 | .then((message) => { 93 | fields[name] = { 94 | status: true, 95 | message 96 | }; 97 | }) 98 | .catch((message) => { 99 | fields[name] = { 100 | status: false, 101 | message 102 | }; 103 | }); 104 | }) 105 | ) 106 | .then(() => { 107 | this.setState({ fields }); 108 | 109 | if (isValidForm(fields) && typeof successHandler === 'function') { 110 | successHandler(); 111 | } else if (typeof errorHandler === 'function') { 112 | errorHandler(); 113 | } 114 | }); 115 | } 116 | 117 | render() { 118 | return createElement(Target, { 119 | ...this.props, 120 | validate: this.validateForm, 121 | validationStatus: isValidForm(this.state.fields) 122 | }); 123 | } 124 | } 125 | 126 | FormValidation.contextTypes = { 127 | neoform: PropTypes.object 128 | }; 129 | 130 | FormValidation.childContextTypes = { 131 | neoform: PropTypes.object 132 | }; 133 | 134 | if (process.env.NODE_ENV !== 'production') { 135 | return setDisplayName(wrapDisplayName(Target, 'formValidation'))(FormValidation); 136 | } 137 | 138 | return FormValidation; 139 | }; 140 | 141 | export default formValidation; 142 | -------------------------------------------------------------------------------- /packages/neoform-validation/src/index.js: -------------------------------------------------------------------------------- 1 | export { default as formValidation } from './formValidation'; 2 | export { default as fieldValidation } from './fieldValidation'; 3 | -------------------------------------------------------------------------------- /packages/neoform-validation/test/__snapshots__/demo.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`neoform-validation field invalid 1`] = ` 4 | Object { 5 | "data": Object { 6 | "firstName": "John", 7 | "friends": Array [ 8 | Object { 9 | "firstName": "Pepe", 10 | "lastName": "Sad", 11 | }, 12 | Object { 13 | "firstName": "", 14 | "lastName": "Darkness", 15 | }, 16 | ], 17 | "lastName": "Doe", 18 | }, 19 | "getValue": [Function], 20 | "onChange": [Function], 21 | "onInvalid": [Function], 22 | "onSubmit": [Function], 23 | "validate": [Function], 24 | "validationStatus": false, 25 | } 26 | `; 27 | 28 | exports[`neoform-validation field invalid 2`] = ` 29 | Object { 30 | "name": "friends.1.firstName", 31 | "onChange": [Function], 32 | "validate": [Function], 33 | "validationMessage": "required", 34 | "validationStatus": false, 35 | "value": "", 36 | } 37 | `; 38 | 39 | exports[`neoform-validation field unmount invalid field 1`] = ` 40 | Object { 41 | "data": Object { 42 | "firstName": "John", 43 | "friends": Array [ 44 | Object { 45 | "firstName": "Pepe", 46 | "lastName": "Sad", 47 | }, 48 | ], 49 | "lastName": "Doe", 50 | }, 51 | "getValue": [Function], 52 | "onChange": [Function], 53 | "onInvalid": [Function], 54 | "onSubmit": [Function], 55 | "validate": [Function], 56 | "validationStatus": true, 57 | } 58 | `; 59 | 60 | exports[`neoform-validation field valid 1`] = ` 61 | Object { 62 | "data": Object { 63 | "firstName": "John", 64 | "friends": Array [ 65 | Object { 66 | "firstName": "Pepe", 67 | "lastName": "Sad", 68 | }, 69 | Object { 70 | "firstName": "hey from test", 71 | "lastName": "Darkness", 72 | }, 73 | ], 74 | "lastName": "Doe", 75 | }, 76 | "getValue": [Function], 77 | "onChange": [Function], 78 | "onInvalid": [Function], 79 | "onSubmit": [Function], 80 | "validate": [Function], 81 | "validationStatus": true, 82 | } 83 | `; 84 | 85 | exports[`neoform-validation field valid 2`] = ` 86 | Object { 87 | "name": "friends.1.firstName", 88 | "onChange": [Function], 89 | "validate": [Function], 90 | "validationMessage": "valid", 91 | "validationStatus": true, 92 | "value": "hey from test", 93 | } 94 | `; 95 | 96 | exports[`neoform-validation field without validator 1`] = ` 97 | Object { 98 | "data": Object { 99 | "firstName": "John", 100 | "friends": Array [ 101 | Object { 102 | "firstName": "Pepe", 103 | "lastName": "Sad", 104 | }, 105 | Object { 106 | "firstName": "", 107 | "lastName": "Darkness", 108 | }, 109 | ], 110 | "lastName": "Doe", 111 | }, 112 | "getValue": [Function], 113 | "onChange": [Function], 114 | "onInvalid": [Function], 115 | "onSubmit": [Function], 116 | "validate": [Function], 117 | "validationStatus": true, 118 | } 119 | `; 120 | 121 | exports[`neoform-validation field without validator 2`] = ` 122 | Object { 123 | "name": "friends.1.lastName", 124 | "onChange": [Function], 125 | "validate": [Function], 126 | "validationMessage": undefined, 127 | "validationStatus": undefined, 128 | "value": "Darkness", 129 | } 130 | `; 131 | 132 | exports[`neoform-validation form invalid 1`] = ` 133 | Object { 134 | "name": "friends.1.firstName", 135 | "onChange": [Function], 136 | "validate": [Function], 137 | "validationMessage": "required", 138 | "validationStatus": false, 139 | "value": "", 140 | } 141 | `; 142 | 143 | exports[`neoform-validation form invalid 2`] = ` 144 | Object { 145 | "data": Object { 146 | "firstName": "John", 147 | "friends": Array [ 148 | Object { 149 | "firstName": "Pepe", 150 | "lastName": "Sad", 151 | }, 152 | Object { 153 | "firstName": "", 154 | "lastName": "Darkness", 155 | }, 156 | ], 157 | "lastName": "Doe", 158 | }, 159 | "getValue": [Function], 160 | "onChange": [Function], 161 | "onInvalid": [Function], 162 | "onSubmit": [Function], 163 | "validate": [Function], 164 | "validationStatus": false, 165 | } 166 | `; 167 | 168 | exports[`neoform-validation form valid 1`] = ` 169 | Object { 170 | "name": "friends.1.firstName", 171 | "onChange": [Function], 172 | "validate": [Function], 173 | "validationMessage": "valid", 174 | "validationStatus": true, 175 | "value": "hey from test", 176 | } 177 | `; 178 | 179 | exports[`neoform-validation form valid 2`] = ` 180 | Object { 181 | "data": Object { 182 | "firstName": "John", 183 | "friends": Array [ 184 | Object { 185 | "firstName": "Pepe", 186 | "lastName": "Sad", 187 | }, 188 | Object { 189 | "firstName": "hey from test", 190 | "lastName": "Darkness", 191 | }, 192 | ], 193 | "lastName": "Doe", 194 | }, 195 | "getValue": [Function], 196 | "onChange": [Function], 197 | "onInvalid": [Function], 198 | "onSubmit": [Function], 199 | "validate": [Function], 200 | "validationStatus": true, 201 | } 202 | `; 203 | -------------------------------------------------------------------------------- /packages/neoform-validation/test/demo.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import React from 'react'; 3 | import { mount } from 'enzyme'; 4 | 5 | import Demo from '../demo'; 6 | 7 | describe('neoform-validation', () => { 8 | describe('field', () => { 9 | it('invalid', (done) => { 10 | const wrapper = mount( 11 | 12 | ); 13 | 14 | const testField = wrapper 15 | .find('MyInput') 16 | .filterWhere((node) => node.prop('name') === 'friends.1.firstName'); 17 | 18 | testField.prop('validate')(); 19 | 20 | global.setImmediate(() => { 21 | expect(wrapper.find('MyForm').props()).toMatchSnapshot(); 22 | expect(testField.props()).toMatchSnapshot(); 23 | done(); 24 | }); 25 | }); 26 | 27 | it('valid', (done) => { 28 | const wrapper = mount( 29 | 30 | ); 31 | 32 | const testField = wrapper 33 | .find('MyInput') 34 | .filterWhere((node) => node.prop('name') === 'friends.1.firstName'); 35 | 36 | testField.prop('onChange')('hey from test'); 37 | testField.prop('validate')(); 38 | 39 | global.setImmediate(() => { 40 | expect(wrapper.find('MyForm').props()).toMatchSnapshot(); 41 | expect(testField.props()).toMatchSnapshot(); 42 | done(); 43 | }); 44 | }); 45 | 46 | it('without validator', (done) => { 47 | const wrapper = mount( 48 | 49 | ); 50 | 51 | const testField = wrapper 52 | .find('MyInput') 53 | .filterWhere((node) => node.prop('name') === 'friends.1.lastName'); 54 | 55 | testField.prop('validate')(); 56 | 57 | global.setImmediate(() => { 58 | expect(wrapper.find('MyForm').props()).toMatchSnapshot(); 59 | expect(testField.props()).toMatchSnapshot(); 60 | done(); 61 | }); 62 | }); 63 | 64 | it('unmount invalid field', (done) => { 65 | const wrapper = mount( 66 | 67 | ); 68 | 69 | const testField = wrapper 70 | .find('MyInput') 71 | .filterWhere((node) => node.prop('name') === 'friends.1.firstName'); 72 | 73 | testField.prop('validate')(); 74 | 75 | global.setImmediate(() => { 76 | wrapper.setState({ 77 | friends: [ 78 | { 79 | firstName: 'Pepe', 80 | lastName: 'Sad' 81 | } 82 | ] 83 | }, () => { 84 | global.setTimeout(() => { 85 | expect(wrapper.find('MyForm').props()).toMatchSnapshot(); 86 | done(); 87 | }, 100); 88 | }); 89 | }); 90 | }); 91 | }); 92 | 93 | describe('form', () => { 94 | it('invalid', (done) => { 95 | const mockOnSuccess = jest.fn(); 96 | const mockOnError = jest.fn(); 97 | const wrapper = mount( 98 | 99 | ); 100 | const form = wrapper.find('MyForm'); 101 | 102 | form.prop('validate')(mockOnSuccess, mockOnError); 103 | 104 | global.setImmediate(() => { 105 | const testField = wrapper 106 | .find('MyInput') 107 | .filterWhere((node) => node.prop('name') === 'friends.1.firstName'); 108 | 109 | expect(testField.props()).toMatchSnapshot(); 110 | expect(form.props()).toMatchSnapshot(); 111 | expect(mockOnSuccess).toHaveBeenCalledTimes(0); 112 | expect(mockOnError).toHaveBeenCalledTimes(1); 113 | done(); 114 | }); 115 | }); 116 | 117 | it('valid', (done) => { 118 | const mockOnSuccess = jest.fn(); 119 | const mockOnError = jest.fn(); 120 | const wrapper = mount( 121 | 122 | ); 123 | const form = wrapper.find('MyForm'); 124 | const testField = wrapper 125 | .find('MyInput') 126 | .filterWhere((node) => node.prop('name') === 'friends.1.firstName'); 127 | 128 | testField.prop('onChange')('hey from test'); 129 | form.prop('validate')(mockOnSuccess, mockOnError); 130 | 131 | global.setImmediate(() => { 132 | expect(testField.props()).toMatchSnapshot(); 133 | expect(form.props()).toMatchSnapshot(); 134 | expect(mockOnSuccess).toHaveBeenCalledTimes(1); 135 | expect(mockOnError).toHaveBeenCalledTimes(0); 136 | done(); 137 | }); 138 | }); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /packages/neoform/demo/Form/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-no-bind */ 2 | /* eslint-disable react/no-array-index-key */ 3 | import React from 'react'; 4 | 5 | import { form } from '../../src'; 6 | import Input from '../Input'; 7 | 8 | const MyForm = ({ data, onSubmit }) => ( 9 | { 11 | e.preventDefault(); 12 | onSubmit(); 13 | }} 14 | > 15 |

personal data

16 |
17 | 21 |
22 |
23 | 27 |
28 |

friends

29 |
    30 | { 31 | data.friends.map((friend, index) => ( 32 |
  • 33 |
    34 | 38 |
    39 |
    40 | 44 |
    45 |
  • 46 | )) 47 | } 48 |
49 | 50 | 51 | ); 52 | 53 | export default form(MyForm); 54 | -------------------------------------------------------------------------------- /packages/neoform/demo/Input/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-no-bind */ 2 | import React from 'react'; 3 | 4 | import { field } from '../../src'; 5 | 6 | const MyInput = ({ 7 | value = '', 8 | onChange, 9 | ...props 10 | }) => ( 11 | onChange(e.target.value)} 15 | /> 16 | ); 17 | 18 | export default field(MyInput); 19 | -------------------------------------------------------------------------------- /packages/neoform/demo/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import React, { Component } from 'react'; 3 | 4 | import { getValue, setValue } from '../../neoform-plain-object-helpers/src'; 5 | import Form from './Form'; 6 | 7 | class Demo extends Component { 8 | constructor(props, context) { 9 | super(props, context); 10 | 11 | this.state = { 12 | firstName: 'John', 13 | lastName: 'Doe', 14 | friends: [ 15 | { 16 | firstName: 'Pepe', 17 | lastName: 'Sad' 18 | }, 19 | { 20 | firstName: '', 21 | lastName: 'Darkness' 22 | } 23 | ] 24 | }; 25 | this.onChange = this.onChange.bind(this); 26 | this.onSubmit = this.onSubmit.bind(this); 27 | } 28 | 29 | onSubmit() { 30 | console.log('submit:', this.state); 31 | } 32 | 33 | onChange(name, value) { 34 | this.setState((prevState) => setValue(prevState, name, value)); 35 | } 36 | 37 | render() { 38 | return ( 39 |
45 | ); 46 | } 47 | } 48 | 49 | export default Demo; 50 | -------------------------------------------------------------------------------- /packages/neoform/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neoform", 3 | "library": "NeoForm", 4 | "version": "0.4.1", 5 | "description": "Higher-Order Components for form state management in React", 6 | "keywords": [ 7 | "neoform" 8 | ], 9 | "main": "lib/index.js", 10 | "module": "es/index.js", 11 | "files": [ 12 | "dist/", 13 | "es/", 14 | "lib/" 15 | ], 16 | "repository": "zero-plus-x/neoform", 17 | "author": "Kir Belevich (https://github.com/deepsweet)", 18 | "license": { 19 | "type": "MIT", 20 | "url": "https://github.com/zero-plus-x/neoform/blob/master/license.md" 21 | }, 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "peerDependencies": { 26 | "react": "^15.4.2 || ^16.0.0", 27 | "recompose": "^0.26.0", 28 | "prop-types": "^15.5.10" 29 | }, 30 | "scripts": { 31 | "prepublishOnly": "cd ../..; yarn start build neoform" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/neoform/readme.md: -------------------------------------------------------------------------------- 1 | # neoform 2 | 3 | For more in depth documentation see https://github.com/zero-plus-x/neoform/blob/master/readme.md 4 | -------------------------------------------------------------------------------- /packages/neoform/src/field.js: -------------------------------------------------------------------------------- 1 | import { createElement, Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { setDisplayName, wrapDisplayName } from 'recompose'; 4 | 5 | const field = (Target) => { 6 | class Field extends Component { 7 | constructor(props, context) { 8 | super(props, context); 9 | 10 | this.onChange = this.onChange.bind(this); 11 | } 12 | 13 | onChange(value) { 14 | return this.context.neoform.updateData(this.props.name, value); 15 | } 16 | 17 | render() { 18 | return createElement(Target, { 19 | ...this.props, 20 | value: this.context.neoform.getValue(this.props.name), 21 | onChange: this.onChange 22 | }); 23 | } 24 | } 25 | 26 | Field.propTypes = { 27 | name: PropTypes.string.isRequired 28 | }; 29 | 30 | Field.contextTypes = { 31 | neoform: PropTypes.object 32 | }; 33 | 34 | if (process.env.NODE_ENV !== 'production') { 35 | return setDisplayName(wrapDisplayName(Target, 'field'))(Field); 36 | } 37 | 38 | return Field; 39 | }; 40 | 41 | export default field; 42 | -------------------------------------------------------------------------------- /packages/neoform/src/form.js: -------------------------------------------------------------------------------- 1 | import { createElement, Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { setDisplayName, wrapDisplayName } from 'recompose'; 4 | 5 | const form = (Target) => { 6 | class Form extends Component { 7 | getChildContext() { 8 | const { data, getValue, onChange } = this.props; 9 | 10 | return { 11 | neoform: { 12 | state: data, 13 | updateData: onChange, 14 | getValue: (name) => getValue(data, name) 15 | } 16 | }; 17 | } 18 | 19 | render() { 20 | return createElement(Target, this.props); 21 | } 22 | } 23 | 24 | Form.propTypes = { 25 | data: PropTypes.object.isRequired, 26 | getValue: PropTypes.func.isRequired, 27 | onChange: PropTypes.func.isRequired 28 | }; 29 | 30 | Form.childContextTypes = { 31 | neoform: PropTypes.object 32 | }; 33 | 34 | if (process.env.NODE_ENV !== 'production') { 35 | return setDisplayName(wrapDisplayName(Target, 'form'))(Form); 36 | } 37 | 38 | return Form; 39 | }; 40 | 41 | export default form; 42 | -------------------------------------------------------------------------------- /packages/neoform/src/index.js: -------------------------------------------------------------------------------- 1 | export { default as form } from './form'; 2 | export { default as field } from './field'; 3 | -------------------------------------------------------------------------------- /packages/neoform/test/__snapshots__/demo.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`neoform change field 1`] = ` 4 | 9 | `; 10 | 11 | exports[`neoform change field in array 1`] = ` 12 | 17 | `; 18 | 19 | exports[`neoform values 1`] = ` 20 | 25 | `; 26 | 27 | exports[`neoform values 2`] = ` 28 | 33 | `; 34 | 35 | exports[`neoform values 3`] = ` 36 | 41 | `; 42 | 43 | exports[`neoform values 4`] = ` 44 | 49 | `; 50 | 51 | exports[`neoform values 5`] = ` 52 | 57 | `; 58 | 59 | exports[`neoform values 6`] = ` 60 | 65 | `; 66 | -------------------------------------------------------------------------------- /packages/neoform/test/demo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | 4 | import Demo from '../demo'; 5 | 6 | describe('neoform', () => { 7 | it('values', () => { 8 | const wrapper = mount( 9 | 10 | ); 11 | 12 | expect(wrapper.find('[name="firstName"]')).toMatchSnapshot(); 13 | expect(wrapper.find('[name="lastName"]')).toMatchSnapshot(); 14 | expect(wrapper.find('[name="friends.0.firstName"]')).toMatchSnapshot(); 15 | expect(wrapper.find('[name="friends.0.lastName"]')).toMatchSnapshot(); 16 | expect(wrapper.find('[name="friends.1.firstName"]')).toMatchSnapshot(); 17 | expect(wrapper.find('[name="friends.1.lastName"]')).toMatchSnapshot(); 18 | }); 19 | 20 | it('change field', () => { 21 | const wrapper = mount( 22 | 23 | ); 24 | 25 | const testField = wrapper.find('[name="firstName"]'); 26 | 27 | testField.simulate('change', { 28 | target: { 29 | value: 'hey from tests' 30 | } 31 | }); 32 | 33 | expect(testField).toMatchSnapshot(); 34 | }); 35 | 36 | it('change field in array', () => { 37 | const wrapper = mount( 38 | 39 | ); 40 | 41 | const testField = wrapper.find('[name="friends.0.lastName"]'); 42 | 43 | testField.simulate('change', { 44 | target: { 45 | value: 'hey from tests' 46 | } 47 | }); 48 | 49 | expect(testField).toMatchSnapshot(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | travis 7 | travis 8 |

9 | 10 | --- 11 | 12 | Better form state management for React where data state is directly mapped to form fields, so form becomes just a representation and changing interface for that data state. 13 | 14 | * [Usage](#usage) 15 | * [Intro](#intro) 16 | * [`field`](#field) 17 | * [`form`](#form) 18 | * [App](#app) 19 | * [`getValue`](#getvalue) 20 | * [`setValue`](#setvalue) 21 | * [Validation](#validation) 22 | * [`fieldValidation`](#fieldvalidation) 23 | * [`formValidation`](#formvalidation) 24 | * [Validators](#validators) 25 | * [FAQ](#faq) 26 | * [Status](#status) 27 | * [Development](#development) 28 | 29 | ## Usage 30 | 31 | ### Intro 32 | 33 | Let's say you have some data and you want to represent it as an HTML form with an Input for each data field. 34 | 35 | ```json 36 | "user": { 37 | "name": "Pepe", 38 | "status": "sad", 39 | "friends": [ 40 | "darkness" 41 | ] 42 | } 43 | ``` 44 | 45 | Each data field can be referenced with a "key" or "property" path. You might be familiar with this concept from working with immutable data structures or helpers like `lodash.get()`. 46 | 47 | ```js 48 | "user": { 49 | "name": "Pepe", // "user.name" 50 | "status": "sad", // "user.status" 51 | "friends": [ 52 | "darkness" // "user.friends.0" 53 | ] 54 | } 55 | ``` 56 | 57 | The first core idea of NeoForm is to map data to form fields using these key/property paths. We'll refer to this data as "form state" below. 58 | 59 | Let's see how it works with a step-by-step example. First, we need to install the following set of dependencies: 60 | 61 | ``` 62 | yarn add prop-types recompose neoform neoform-validation neoform-plain-object-helpers 63 | ``` 64 | 65 | We'll start with creating a simple input: 66 | 67 | ### `field` 68 | 69 | ```js 70 | const MyInput = () => ( 71 | 72 | ); 73 | 74 | export default MyInput; 75 | ``` 76 | 77 | After wrapping this input with `field` [HOC](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750) from NeoForm we'll have: 78 | 79 | #### `value` and `onChange` props 80 | 81 | A `value` from a form state (can be used in checkbox as a `checked` attribute if it's boolean, and so on) and `onChange` handler to let NeoForm know that value should be changed: 82 | 83 | ```js 84 | import { field } from 'neoform'; 85 | 86 | const MyInput = ({ value, onChange }) => ( 87 | onChange(e.target.value)} 90 | /> 91 | ); 92 | 93 | export default field(MyInput); 94 | ``` 95 | 96 | Use `(e) => e.target.checked` if you have a checkbox or just `(value) => value` if you have some custom/3rd-party field implementation. 97 | 98 | ### `form` 99 | 100 | Now when the input is ready we can use it in a form: 101 | 102 | ```js 103 | import MyInput from '../MyInput'; 104 | 105 | const MyForm = () => ( 106 | 107 | 108 | 109 | 110 | 111 | ); 112 | 113 | export default MyForm; 114 | ``` 115 | 116 | Let's connect this form to NeoForm by wrapping it with a `form` HOC: 117 | 118 | ```js 119 | import { form } from 'neoform'; 120 | 121 | import MyInput from '../MyInput'; 122 | 123 | const MyForm = () => ( 124 |
125 | 126 | 127 | 128 | 129 | ); 130 | 131 | export default form(MyForm); 132 | ``` 133 | 134 | ### App 135 | 136 | Finally, we assemble everything together: 137 | 138 | ```js 139 | import { setValue, getValue } from 'neoform-plain-object-helpers'; 140 | 141 | import MyForm from '../MyForm'; 142 | 143 | class App extends Component { 144 | constructor(props) { 145 | super(props); 146 | 147 | this.state = { 148 | data: props.data 149 | }; 150 | this.onChange = this.onChange.bind(this); 151 | this.onSubmit = this.onSubmit.bind(this); 152 | } 153 | 154 | onChange(name, value) { 155 | this.setState((prevState) => setValue(prevState, name, value)); 156 | } 157 | 158 | onSubmit() { 159 | console.log('submit:', this.state.data); 160 | } 161 | 162 | render() { 163 | 169 | } 170 | } 171 | ``` 172 | 173 | What's going on here? As you may guessed, all fields in NeoForm are controlled. So, in order to update them, we need to update data state: 174 | 175 | #### `getValue` 176 | 177 | First, we need to specify `getValue` prop to tell NeoForm how exactly it should retrieve field value from data state. The reason to do that is because you might have a plain object data, Immutable or something else with a different "interface". 178 | 179 | Instead of writing your own `getValue` function, you can use one from [neoform-plain-object-helpers](https://github.com/zero-plus-x/neoform/tree/master/packages/neoform-plain-object-helpers) or [neoform-immutable-helpers](https://github.com/zero-plus-x/neoform/tree/master/packages/neoform-immutable-helpers) package. 180 | 181 | `getValue` arguments: 182 | 183 | * `data` — form data state 184 | * `name` — field name 185 | 186 | #### `setValue` 187 | 188 | Second, we have only one `onChange` handler for the entire form instead of multiple ones for each field. So, whenever some field requests a change, we need to update form data by updating the state so updated value is passed to that field with a new render. 189 | 190 | :information_source: Consider using [Recompose `pure()` HOC](https://github.com/acdlite/recompose/blob/master/docs/API.md#pure) or [`React.PureComponent`](https://facebook.github.io/react/docs/react-api.html#react.purecomponent) for fields to avoid unnecessary renders and get performance boost in some cases. 191 | 192 | Instead of writing your own handler, you can use `setValue` helper from [neoform-plain-object-helpers](https://github.com/zero-plus-x/neoform/tree/master/packages/neoform-plain-object-helpers) or [neoform-immutable-helpers](https://github.com/zero-plus-x/neoform/tree/master/packages/neoform-immutable-helpers) package. 193 | 194 | `setValue` arguments: 195 | 196 | * `data` — form data state 197 | * `name` — field name 198 | * `value` — new field value 199 | 200 | ``` 201 | +--------------+ 202 | | | 203 | | | 204 | | +---------v---------+ 205 | | | | 206 | | | MyForm.data | 207 | | | | 208 | | +---------+---------+ 209 | | | 210 | | name | 211 | | | 212 | | +---------v---------+ 213 | | | | 214 | | | MyInput.value | 215 | | | | 216 | | +---------+---------+ 217 | | | 218 | | | 219 | | +---------v---------+ 220 | | | | 221 | | | MyInput.onChange | 222 | | | | 223 | | +---------+---------+ 224 | | | 225 | | name | value 226 | | | 227 | | +---------v---------+ 228 | | | | 229 | | | MyForm.onChange | 230 | | | | 231 | | +---------+---------+ 232 | | | 233 | | name | value 234 | | | 235 | +--------------+ 236 | ``` 237 | 238 | ### Validation 239 | 240 | Validation in NeoForm is always asynchronous. 241 | 242 | #### `fieldValidation` 243 | 244 | `fieldValidation` is another HOC: 245 | 246 | ```js 247 | import { field } from 'neoform'; 248 | import { fieldValidation } from 'neoform-validation'; 249 | 250 | const MyInput = ({ 251 | validate, 252 | validationStatus, 253 | validationMessage, 254 | ...props 255 | }) => ( 256 | 257 | { 258 | validationStatus === false && ( 259 | {validationMessage} 260 | ) 261 | } 262 | ) 263 | 264 | export default field(fieldValidation(MyInput)); 265 | ``` 266 | 267 | Where the props are: 268 | 269 | * `validate` – validation action, can be called whenever you want (`onChange`, `onBlur`, etc) 270 | * `validationStatus` – `true` | `false` | `undefined` status of field validation 271 | * `validationMessage` – an optional message passed from validator 272 | 273 | #### `formValidation` 274 | 275 | ```js 276 | import { form } from 'neoform'; 277 | import { formValidation } from 'neoform-validation'; 278 | 279 | import MyInput from '../MyInput'; 280 | 281 | const MyForm = ({ 282 | /* data, */ 283 | validate, 284 | validationStatus, 285 | onInvalid, 286 | onSubmit 287 | }) => ( 288 |
{ 289 | validate(onSubmit, onInvalid) 290 | e.preventDefault(); 291 | }}> 292 | 293 | 294 | 295 | 296 | ); 297 | 298 | export default form(formValidation(MyForm)); 299 | ``` 300 | 301 | Where: 302 | 303 | * `validate` – entire form validation action: it will validate all fields and if they're valid it will invoke a first provided callback (`onSubmit` handler in most cases) or second callback (something like `onInvalid`) if they're invalid 304 | * `validationStatus` – `true` | `false` | `undefined` status of entire form validation 305 | 306 | #### Validators 307 | 308 | "Validator" is just a Promise. Rejected one is for `validationStatus: false` prop and resolved is for `validationStatus: true`. An optional argument passed to a rejected or fulfilled Promise becomes `validationMessage` prop. 309 | 310 | ```js 311 | export const requiredValidator = (value, type) => { 312 | if (value === '') { 313 | return Promise.reject('💩'); 314 | } 315 | 316 | return Promise.resolve('🎉'); 317 | }; 318 | ``` 319 | 320 | Where: 321 | 322 | * `value` – field value for validation 323 | * `type` – event type. Can be `submit`, `change`, `blur` or anything you will provide to field `validate` method 324 | 325 | It's up to you how to manage multiple validators — with a simple `Promise.all()` or some complex asynchronous sequences — as long as validator returns a single Promise. 326 | 327 | To use a validator you should just pass it in a `validator` prop to an individual field: 328 | 329 | ```js 330 | import { requiredValidator } from '../validators' 331 | 332 | // … 333 | 334 |
335 | 336 | 337 | 338 | 339 | 340 | // … 341 | ``` 342 | 343 | :tv: [Check out live demo](https://www.webpackbin.com/bins/-KrbNqAfDYNwm07UmzTb). 344 | 345 | ## FAQ 346 | 347 | > But this is just like my entire form is a single component with a single `onChange`! 348 | 349 | Right. 350 | 351 | > Does it affect performance because of re-rendering entire form on every field change? 352 | 353 | Probably in some cases it does. But as it was mentioned here before consider using [Recompose `pure()` HOC](https://github.com/acdlite/recompose/blob/master/docs/API.md#pure) or [`React.PureComponent`](https://facebook.github.io/react/docs/react-api.html#react.purecomponent) to avoid that. 354 | 355 | > What about Redux? 356 | 357 | Absolutely same approach: call an action on form `onChange` and then use plain/immutable helper to return updated data state from a reducer. 358 | 359 | 360 | ## Status 361 | 362 | This is a [monorepo](https://github.com/babel/babel/blob/master/doc/design/monorepo.md) composed of these packages: 363 | 364 | | package | version | description | 365 | | ------- | ------- | ----------- | 366 | | [neoform](packages/neoform/) | [![npm](https://img.shields.io/npm/v/neoform.svg?style=flat-square)](https://www.npmjs.com/package/neoform) | Core toolkit with `form` and `field` HOCs | 367 | | [neoform-validation](packages/neoform-validation/) | [![npm](https://img.shields.io/npm/v/neoform-validation.svg?style=flat-square)](https://www.npmjs.com/package/neoform-validation) | `formValidation` and `fieldValidation` HOCs | 368 | | [neoform-plain-object-helpers](packages/neoform-plain-object-helpers/) | [![npm](https://img.shields.io/npm/v/neoform-plain-object-helpers.svg?style=flat-square)](https://www.npmjs.com/package/neoform-plain-object-helpers) | `getValue` and `setValue` helpers for plain object state | 369 | | [neoform-immutable-helpers](packages/neoform-immutable-helpers/) | [![npm](https://img.shields.io/npm/v/neoform-immutable-helpers.svg?style=flat-square)](https://www.npmjs.com/package/neoform-immutable-helpers) | `getValue` and `setValue` helpers for [Immutable](https://github.com/facebook/immutable-js) state | 370 | 371 | ## Development 372 | 373 | 1. Create a new folder in `packages/`, let's say `neoform-foo`. 374 | 2. See `package.json` in already existing packages and create new `neoform-foo/package.json`. 375 | 3. Put source code in `neoform-foo/src/`, it will be transpiled and bundled into `neoform-foo/dist/`, `neoform-foo/lib/` and `neoform-foo/es/`. 376 | 4. Put tests written with Jest in `neoform-foo/test/`. 377 | 5. Put demo in `neoform-foo/demo/`, it will be rendered and wrapped with HMR. 378 | 379 | Available scripts using [Start](https://github.com/start-runner/start): 380 | 381 | ``` 382 | yarn start build 383 | yarn start demo 384 | yarn start test 385 | yarn start testWatch 386 | yarn start lint 387 | ``` 388 | 389 | Available demos: 390 | 391 | ``` 392 | yarn start demo neoform 393 | yarn start demo neoform-validation 394 | ``` 395 | --------------------------------------------------------------------------------