├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── .babelrc ├── DevTools.js ├── build │ └── bundle.js ├── components │ └── Form.js ├── index.html ├── index.js ├── package.json ├── reducers │ └── FormReducer.js ├── utils │ └── config.js └── webpack.config.js ├── lib └── index.js ├── package.json ├── src └── index.js └── test └── test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": ["react"], 4 | "ecmaFeatures": { 5 | "jsx": true, 6 | "objectLiteralShorthandMethods": true 7 | }, 8 | "env": { 9 | "browser": true, 10 | "node": true, 11 | "es6": true 12 | }, 13 | "rules": { 14 | "quotes": [0], 15 | "new-parens": [1], 16 | "no-alert": [1], 17 | "handle-callback-err": [1], 18 | "strict": [1], 19 | "no-script-url": [0], 20 | "space-unary-ops": [0], 21 | "consistent-return": [0], 22 | "comma-dangle": 1, 23 | "no-mixed-requires": [1], 24 | "no-underscore-dangle": [0], 25 | "no-multi-spaces": [1], 26 | "no-unused-vars": [1], 27 | "key-spacing": [0], 28 | "no-empty": [1], 29 | "no-shadow": [0], 30 | "no-use-before-define": [0], 31 | "no-unused-expressions": [1], 32 | "no-new-func": [0], 33 | "new-cap": [0], 34 | "eqeqeq": [0], 35 | "curly": [0], 36 | "strict": [0], 37 | "no-new": [1], 38 | "eol-last": [1], 39 | "space-infix-ops": [1], 40 | "no-return-assign": [1], 41 | "comma-spacing": [1], 42 | "no-extra-boolean-cast": [1], 43 | "no-constant-condition": [1], 44 | 45 | "react/display-name": 0, 46 | "react/jsx-boolean-value": 1, 47 | "react/jsx-no-duplicate-props": 1, 48 | "react/jsx-no-undef": 1, 49 | "react/jsx-quotes": 1, 50 | "react/jsx-sort-prop-types": 0, 51 | "react/jsx-sort-props": 0, 52 | "react/jsx-uses-react": 1, 53 | "react/jsx-uses-vars": 1, 54 | "react/no-danger": 1, 55 | "react/no-did-mount-set-state": 0, 56 | "react/no-did-update-set-state": 1, 57 | "react/no-multi-comp": 1, 58 | "react/no-unknown-property": 1, 59 | "react/prop-types": 0, 60 | "react/react-in-jsx-scope": 0, 61 | "react/require-extension": 1, 62 | "react/self-closing-comp": 1, 63 | "react/sort-comp": 1, 64 | "react/wrap-multilines": 1 65 | }, 66 | "globals": { 67 | "describe": false, 68 | "chai": false, 69 | "beforeEach": false, 70 | "afterEach": false, 71 | "it": false 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 28 | node_modules 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5.0" 4 | script: "npm run test" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Sen Yang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | redux-form-utils [![Build Status](https://travis-ci.org/jasonslyvia/redux-form-utils.svg)](https://travis-ci.org/jasonslyvia/redux-form-utils) [![npm version](https://badge.fury.io/js/redux-form-utils.svg)](http://badge.fury.io/js/redux-form-utils) 2 | ========================== 3 | 4 | Make handling forms in Redux less painful by providing two helpful utility functions: 5 | 6 | - `createForm(options)`: return a [Higher Order Component](https://gist.github.com/sebmarkbage/ef0bf1f338a7182b6775) which will pass all required form bindings (eg. `value`, `onChange` and more) to children 7 | - `bindRedux(options)`: return an object consists of four keys: 8 | - `state`: the initialState of the form 9 | - `reducer`: a reducer function handling form related actions 10 | - `actionCreators`: an object consists of two helpful action creators `clear(filed)` and `clearAll()` 11 | - `setInitValue`: a function to set initial value for form, useful when in `edit` mode 12 | 13 | [Live Demo](http://jasonslyvia.github.io/redux-form-utils/examples/) 14 | 15 | ## Why 16 | 17 | Suppose you have a form component in Redux app which consists of many `input[type=text]` and `select`s. In Redux, you have to give each input an `onChange` event handler, and handle the change action inside your reducers respectively. 18 | 19 | That might lead to a great load of redundant and duplicated code base. 20 | 21 | **Before** 22 | 23 | ```javascript 24 | class Form extends React.Component { 25 | handleChangeName(e) { 26 | this.props.changeName(e.target.value); 27 | } 28 | 29 | handleChangeAddress(e) { 30 | this.props.changeAddress(e.target.value); 31 | } 32 | 33 | handleChangeGender(e) { 34 | this.props.changeGender(e.target.value); 35 | } 36 | 37 | render() { 38 | return ( 39 |
40 | 41 | 42 | 46 |
47 | ); 48 | } 49 | } 50 | ``` 51 | 52 | By using `redux-form-utils`, you're freed from all these repetitive work. 53 | 54 | **After** 55 | 56 | ```javascript 57 | import { createForm } from 'redux-form-utils'; 58 | 59 | @createForm({ 60 | form: 'my-form', 61 | fields: ['name', 'address', 'gender'] 62 | }) 63 | class Form extends React.Component { 64 | render() { 65 | // What is `this.props.fields`? This will be explained in the following docs. 66 | const { name, address, gender } = this.props.fields; 67 | return ( 68 |
69 | 70 | 71 | 75 |
76 | ); 77 | } 78 | } 79 | ``` 80 | 81 | Notice how many lines of code have been reduced when you use `redux-form-utils`. 82 | 83 | That's why I create this. 84 | 85 | ## How about `redux-form`? 86 | 87 | It's great but it's too enormous, I just want a simple utility function to help me reduce repetitive work. 88 | 89 | ## Usage 90 | 91 | ``` 92 | $ npm install --save redux-form-utils 93 | ``` 94 | 95 | To completely make use of `redux-form-utils`, you have at least 2 steps to go. 96 | 97 | ### 1. Enhance your component 98 | 99 | First thing is you should enhance your component by using `createForm` function. 100 | 101 | In aforementioned example, I use this function as a [decorater](https://developer.mozilla.org/en-US/docs/Decorators). If it bugs you, you can switch to normal function paradigm. 102 | 103 | ```javascript 104 | import { createForm } from 'redux-form-utils'; 105 | 106 | class Form extends React.Component { 107 | render() { 108 | const { name, address } = this.props.fields; 109 | return ( 110 |
111 | 112 | 113 |
114 | ); 115 | } 116 | } 117 | 118 | const EnhancedForm = createForm({ 119 | form: 'my-form', 120 | fields: ['name', 'address'] 121 | })(Form); 122 | ``` 123 | 124 | By enhancing your component, it achieves 3 extra `props`: 125 | 126 | - `fields`(*Object*) An object contains fields you defined in `createForm` option, it looks like this `{fields: { name: { value: '', onChange: Function }}}` 127 | - `clear(field)`(*Function*) An action creator that will clear certain field 128 | - `clearAll()`(*Function*) An action creator to clear all fields in this form 129 | 130 | Then in your component's `render()` method, destructure these fields to form controls like `input`, `textarea` or `select`. 131 | 132 | ```javascript 133 | const { name } = this.props.fields; 134 | // Give `input` a `value` props and a `onChange` props 135 | ``` 136 | 137 | At last, when you enhance your component, make sure it has Redux store's `dispatch` function as a props. 138 | 139 | Alternatively, you can connect your component using `react-redux`'s `connect` method, in this case `dispatch` is passed as props to your component too. 140 | 141 | ### 2. Enhance your reducer 142 | 143 | The second and the last thing to do is to enhance your reducer. 144 | 145 | Basically you should compose your form state to your reducer's `initialState`, and handle form actions in your reducer. 146 | 147 | ```javascript 148 | import { bindRedux } from 'redux-form-utils'; 149 | const { state: formState , reducer: formReducer, actionCreators: formActionCreators } = bindRedux({ 150 | form: 'my-form', 151 | fields: ['name', 'address'] 152 | }); 153 | 154 | 155 | // `formState` has a shape of: 156 | // { 157 | // form: { 158 | // name: { 159 | // value: '', 160 | // }, 161 | // address: { 162 | // value: '', 163 | // } 164 | // } 165 | // } 166 | 167 | // Compose initialState with formState 168 | const initialState = { 169 | foo: 1, 170 | bar: 2, 171 | ...formState 172 | }; 173 | 174 | function reducer(state = initialState, action) { 175 | switch (action.type) { 176 | case 'XXX_ACTION': { 177 | // Do sth for your own action 178 | } 179 | 180 | default: 181 | // Let formReducer handle default situation instead of returning state directly 182 | return formReducer(state, action); 183 | } 184 | } 185 | ``` 186 | 187 | **Bonus** 188 | 189 | If you some redux flow control middleware like [redux-sequence-action](https://github.com/jasonslyvia/redux-sequence-action), you can make use of actionCreators returned by `bindRedux`. It's an object consists of two keys: `clear(field)` and `clearAll()`. 190 | 191 | So you can dispatch some action in sequence, for example send an AJAX request and then clear all form fields. 192 | 193 | ```js 194 | function add() { 195 | return [sendReqeust(), clearAll()]; 196 | } 197 | ``` 198 | 199 | ## Options 200 | 201 | Both `createForm` and `bindRedux` accept the same parameter: an object of your form's configuration. 202 | 203 | This object is in shape of: 204 | 205 | ### form 206 | 207 | Type: *String* Default: *undefined* Required: *true* 208 | 209 | A unique string key for your form. 210 | 211 | ### fields 212 | 213 | Type: *Array* Default: *[]* Required: *true* 214 | 215 | An array of form fields configuration. 216 | 217 | For the simple way, you can pass an array of strings. 218 | 219 | ``` 220 | // Configure fields like this 221 | fields: ['name'] 222 | 223 | // Get a props in your component like this 224 | { 225 | fields: { 226 | name: { 227 | value: '', 228 | onChange: Function 229 | } 230 | } 231 | } 232 | 233 | ``` 234 | 235 | It's quite enough for normal `input` and `select`, but for composite React components, like a `Calendar` or `react-reselect`, `value` and `onChange` seems insufficient. 236 | 237 | So you can configure your field in an object as well: 238 | 239 | ```javascript 240 | // Configure fields like this 241 | fields: [{ 242 | key: 'startDate', 243 | changeType: 'onSwitch', 244 | valueKey: 'date', 245 | // This resolver is called when your `onChange` callback (In this case, `onSwitch`) is called, 246 | // it will be called with excatly the same arguments provided to `onChange`, so you can resolve 247 | //the payload of what to change by your own 248 | resolver(date){ 249 | return { 250 | date: date.focusedDate 251 | }; 252 | } 253 | }] 254 | 255 | // Get a props in your component like this 256 | { 257 | fields: { 258 | startDate: { 259 | date: '', 260 | onSwitch: Function 261 | } 262 | } 263 | } 264 | 265 | // Use props in component like this 266 | const { startDate } = this.props.fields; 267 | 268 | ``` 269 | 270 | ## Tips 271 | 272 | Since both `createForm` and `bindRedux` require the same option, it's wise to store these options into separate files and require them in your component and reducer. 273 | 274 | Check the [Live Demo](http://jasonslyvia.github.io/redux-form-utils/examples/) for more clue. 275 | 276 | ## Scripts 277 | 278 | ``` 279 | $ npm run test 280 | ``` 281 | 282 | ## License 283 | 284 | MIT 285 | 286 | -------------------------------------------------------------------------------- /examples/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | "env": { 4 | "development": { 5 | "plugins": [ 6 | ["transform-decorators-legacy"], 7 | ] 8 | }, 9 | "production": { 10 | "plugins": [ 11 | ["transform-decorators-legacy"] 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import LogMonitor from 'redux-devtools-log-monitor'; 4 | import DockMonitor from 'redux-devtools-dock-monitor'; 5 | 6 | export default createDevTools( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /examples/components/Form.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createForm } from '../../lib/'; 3 | import formConfig from '../utils/config'; 4 | 5 | @createForm(formConfig) 6 | class Form extends React.Component { 7 | render() { 8 | const { clear, clearAll } = this.props; 9 | const { name, address, gender } = this.props.fields; 10 | 11 | return ( 12 |
13 | 14 | 15 | 20 |
21 | 22 | 23 | 24 | 25 |
26 |
27 | ); 28 | } 29 | } 30 | 31 | export default Form; 32 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | redux-form-utils example 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, compose } from 'redux'; 4 | import { Provider, connect } from 'react-redux'; 5 | import DevTools from './DevTools'; 6 | import reducers from './reducers/FormReducer'; 7 | import Form from './components/Form'; 8 | 9 | 10 | const finalCreateStore = compose( 11 | DevTools.instrument() 12 | )(createStore); 13 | const store = finalCreateStore(reducers); 14 | 15 | 16 | class Root extends React.Component { 17 | render() { 18 | return ( 19 | 20 |
21 | 22 | 23 |
24 |
25 | ); 26 | } 27 | } 28 | 29 | @connect(state => { 30 | return { 31 | form: state, 32 | }; 33 | }) 34 | class App extends React.Component { 35 | render() { 36 | return ( 37 |
38 | ); 39 | } 40 | } 41 | 42 | ReactDOM.render(, document.getElementById('app')); 43 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "demo": "./node_modules/.bin/webpack" 8 | }, 9 | "author": "jasonslyvia (http://undefinedblog.com/)", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "babel-core": "^6.4.5", 13 | "babel-loader": "^6.2.1", 14 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 15 | "babel-preset-es2015": "^6.3.13", 16 | "babel-preset-es2015-loose": "^7.0.0", 17 | "babel-preset-react": "^6.3.13", 18 | "babel-preset-stage-0": "^6.3.13", 19 | "react": "^0.14.6", 20 | "react-dom": "^0.14.6", 21 | "react-redux": "^4.0.6", 22 | "redux": "^3.0.5", 23 | "redux-devtools": "^3.0.1", 24 | "redux-devtools-dock-monitor": "^1.0.1", 25 | "redux-devtools-log-monitor": "^1.0.2", 26 | "webpack": "^1.12.11" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/reducers/FormReducer.js: -------------------------------------------------------------------------------- 1 | import { bindRedux } from '../../lib/'; 2 | import formConfig from '../utils/config'; 3 | 4 | const { state: formState, reducer: formReducer } = bindRedux(formConfig); 5 | 6 | const initialState = { 7 | foo: 1, 8 | bar: 2, 9 | ...formState 10 | }; 11 | 12 | export default function reducer(state = initialState, action) { 13 | switch (action.type) { 14 | case 'SOME_ACTION_NON_EXISTENT': { 15 | return { 16 | foo: 2, 17 | ...state, 18 | }; 19 | } 20 | 21 | default: 22 | return formReducer(state, action); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/utils/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | form: 'myForm', 3 | fields: ['name', 'address', 'gender'] 4 | }; 5 | -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | var isDebug = process.env.NODE_ENV !== 'production'; 5 | 6 | module.exports = { 7 | watch: isDebug, 8 | 9 | entry: './index.js', 10 | 11 | devtool: isDebug ? 'inline-source-map' : 'source-map', 12 | 13 | output: { 14 | filename: 'bundle.js', 15 | path: path.join(__dirname, 'build') 16 | }, 17 | 18 | module: { 19 | loaders: [{ 20 | test: /\.js$/, 21 | exclude: /node_modules/, 22 | loader: 'babel' 23 | }] 24 | }, 25 | 26 | plugins: isDebug ? [] : [ 27 | new webpack.optimize.UglifyJsPlugin({ 28 | compress: { 29 | unused: true, 30 | dead_code: true, 31 | }, 32 | }) 33 | ] 34 | }; 35 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 4 | 5 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | Object.defineProperty(exports, "__esModule", { 10 | value: true 11 | }); 12 | exports.createForm = createForm; 13 | exports.bindRedux = bindRedux; 14 | 15 | var _react = require('react'); 16 | 17 | var _react2 = _interopRequireDefault(_react); 18 | 19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 20 | 21 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 22 | 23 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 24 | 25 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 26 | 27 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 28 | 29 | /** 30 | * @param {string} options.form A unique key to identify your form throughout the app 31 | * @param {array} options.fields An array of string or object to configure the form fields 32 | * @return {object} Enhanced React Component 33 | */ 34 | function createForm(_ref) { 35 | var form = _ref.form; 36 | var fields = _ref.fields; 37 | 38 | return function (Component) { 39 | var ReduxForm = function (_React$Component) { 40 | _inherits(ReduxForm, _React$Component); 41 | 42 | function ReduxForm(props) { 43 | _classCallCheck(this, ReduxForm); 44 | 45 | var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(ReduxForm).call(this, props)); 46 | 47 | _this.displayName = form + 'Form'; 48 | _this.state = {}; 49 | return _this; 50 | } 51 | 52 | _createClass(ReduxForm, [{ 53 | key: 'componentWillMount', 54 | value: function componentWillMount() { 55 | this.dispatch = this.props.dispatch || this.context && this.context.store && this.context.store.dispatch; 56 | if (typeof this.dispatch !== 'function') { 57 | throw new ReferenceError('[redux-form-utils] Please pass `dispatch` to ' + form + ' as props or connect it with Redux\'s store.'); 58 | } 59 | } 60 | }, { 61 | key: 'handleChange', 62 | value: function handleChange(key, e) { 63 | var value = e; 64 | 65 | if ((typeof e === 'undefined' ? 'undefined' : _typeof(e)) === 'object') { 66 | if (_typeof(e.target) === 'object') { 67 | if (e.target.tagName.toLowerCase() === 'input' && ['checkbox', 'radio'].indexOf(e.target.type) > -1) { 68 | value = e.target.checked; 69 | } else { 70 | value = e.target.value; 71 | } 72 | } else if (e.value !== undefined) { 73 | value = e.value; 74 | } 75 | } 76 | 77 | this.dispatch({ 78 | type: '@@form/VALUE_CHANGE', 79 | meta: { 80 | form: form, 81 | field: key, 82 | complex: value === undefined 83 | }, 84 | payload: value !== undefined ? value : _extends({}, e) 85 | }); 86 | } 87 | }, { 88 | key: 'clearAll', 89 | value: function clearAll() { 90 | this.dispatch({ 91 | type: '@@form/CLEAR_ALL', 92 | meta: { 93 | form: form 94 | } 95 | }); 96 | } 97 | }, { 98 | key: 'clear', 99 | value: function clear(field) { 100 | if (field && fields.indexOf(field) > -1) { 101 | this.dispatch({ 102 | type: '@@form/CLEAR', 103 | meta: { 104 | form: form, 105 | field: field 106 | } 107 | }); 108 | } 109 | } 110 | }, { 111 | key: 'render', 112 | value: function render() { 113 | var _this2 = this; 114 | 115 | return _react2.default.createElement(Component, _extends({}, this.props, { fields: fields.reduce(function (prev, curr) { 116 | if (!_this2.props.form) { 117 | throw new Error('[redux-form-utils] `' + _this2.displayName + '.props.form` is not found, make sure add `formState` to initialState using `bindRedux` in your reducer.'); 118 | } 119 | 120 | if (typeof curr === 'string') { 121 | prev[curr] = { 122 | value: _this2.props.form[curr].value, 123 | onChange: _this2.handleChange.bind(_this2, curr) 124 | }; 125 | } else { 126 | (function () { 127 | var _prev$key; 128 | 129 | var key = curr.key; 130 | var _curr$valueKey = curr.valueKey; 131 | var valueKey = _curr$valueKey === undefined ? 'value' : _curr$valueKey; 132 | var _curr$changeType = curr.changeType; 133 | var changeType = _curr$changeType === undefined ? 'onChange' : _curr$changeType; 134 | var resolver = curr.resolver; 135 | var resovler = curr.resovler; 136 | 137 | // backward compatible for a typo 138 | 139 | if (!resolver) { 140 | resolver = resovler; 141 | } 142 | 143 | if (!key || typeof key !== 'string') { 144 | throw new TypeError('[redux-form-utils] If you provide an object within \`fields\` options, make sure this object has a key which named \`key\`, and the type of it\'s value is string.'); 145 | } 146 | 147 | prev[key] = (_prev$key = {}, _defineProperty(_prev$key, valueKey, _this2.props.form[key][valueKey]), _defineProperty(_prev$key, changeType, function (a, b, c, d) { 148 | if (resolver) { 149 | var payload = resolver(a, b, c, d); 150 | _this2.handleChange.call(_this2, key, payload); 151 | } else { 152 | _this2.handleChange.call(_this2, key, a, b, c, d); 153 | } 154 | }), _prev$key); 155 | })(); 156 | } 157 | 158 | return prev; 159 | }, {}), 160 | clearAll: this.clearAll.bind(this), 161 | clear: this.clear.bind(this) })); 162 | } 163 | }]); 164 | 165 | return ReduxForm; 166 | }(_react2.default.Component); 167 | 168 | ReduxForm.propTypes = { 169 | form: _react.PropTypes.object 170 | }; 171 | ReduxForm.contextTypes = { 172 | store: _react2.default.PropTypes.object 173 | }; 174 | 175 | return ReduxForm; 176 | }; 177 | } 178 | 179 | function bindRedux(_ref2) { 180 | var form = _ref2.form; 181 | var fields = _ref2.fields; 182 | 183 | return { 184 | state: { 185 | form: fields.reduce(function (prev, curr) { 186 | if (typeof curr === 'string') { 187 | prev[curr] = { 188 | value: '' 189 | }; 190 | } else { 191 | var key = curr.key; 192 | var _curr$valueKey2 = curr.valueKey; 193 | var valueKey = _curr$valueKey2 === undefined ? 'value' : _curr$valueKey2; 194 | var initValue = curr.initValue; 195 | 196 | prev[key] = _defineProperty({}, valueKey, initValue !== undefined ? initValue : ''); 197 | } 198 | 199 | return prev; 200 | }, {}) 201 | }, 202 | 203 | setInitValue: function setInitValue(initObj, state) { 204 | if (!state || !state.form) { 205 | return state; 206 | } 207 | 208 | return _extends({}, state, { 209 | form: _extends({}, state.form, Object.keys(initObj).reduce(function (prev, curr) { 210 | if (_typeof(initObj[curr]) !== 'object') { 211 | return _extends({}, prev, _defineProperty({}, curr, { 212 | value: initObj[curr] 213 | })); 214 | } 215 | 216 | return _extends({}, prev, _defineProperty({}, curr, initObj[curr])); 217 | }, {})) 218 | }); 219 | }, 220 | reducer: function reducer(state, action) { 221 | if (action.type.indexOf('@@form') !== 0 || action.meta.form !== form) { 222 | return state; 223 | } 224 | 225 | function findConfig(field) { 226 | var fieldConfig = fields.filter(function (k) { 227 | if ((typeof k === 'undefined' ? 'undefined' : _typeof(k)) === 'object') { 228 | return k.key === field; 229 | } 230 | 231 | return k === field; 232 | }); 233 | 234 | return fieldConfig[0] || {}; 235 | } 236 | 237 | switch (action.type) { 238 | case '@@form/VALUE_CHANGE': 239 | { 240 | var fieldConfig = findConfig(action.meta.field); 241 | var newField = undefined; 242 | if (action.meta.complex) { 243 | return _extends({}, state, { 244 | form: _extends({}, state.form, _defineProperty({}, action.meta.field, _extends({}, state.form[action.meta.field], action.payload))) 245 | }); 246 | } 247 | 248 | if (_typeof(action.payload) === 'object') { 249 | return _extends({}, state, { 250 | form: _extends({}, state.form, _defineProperty({}, action.meta.field, _extends({}, state.form[action.meta.field], action.payload))) 251 | }); 252 | } 253 | 254 | return _extends({}, state, { 255 | form: _extends({}, state.form, _defineProperty({}, action.meta.field, _extends({}, state.form[action.meta.field], _defineProperty({}, '' + (fieldConfig.valueKey || 'value'), action.payload)))) 256 | }); 257 | } 258 | 259 | case '@@form/CLEAR_ALL': 260 | { 261 | return _extends({}, state, { 262 | form: Object.keys(state.form).reduce(function (prev, curr) { 263 | var fieldConfig = findConfig(curr); 264 | prev[curr] = _extends({}, state.form[curr], _defineProperty({}, '' + (fieldConfig.valueKey || 'value'), fieldConfig.initValue || '')); 265 | 266 | return prev; 267 | }, {}) 268 | }); 269 | } 270 | 271 | case '@@form/CLEAR': 272 | { 273 | var fieldConfig = findConfig(action.meta.field); 274 | 275 | return _extends({}, state, { 276 | form: _extends({}, state.form, _defineProperty({}, action.meta.field, _extends({}, state.form[action.meta.field], _defineProperty({}, '' + (fieldConfig.valueKey || 'value'), fieldConfig.initValue || '')))) 277 | }); 278 | } 279 | 280 | default: 281 | return state; 282 | } 283 | }, 284 | 285 | actionCreators: { 286 | clear: function clear(field) { 287 | return { 288 | type: '@@form/CLEAR', 289 | meta: { 290 | form: form, 291 | field: field 292 | } 293 | }; 294 | }, 295 | clearAll: function clearAll() { 296 | return { 297 | type: '@@form/CLEAR_ALL', 298 | meta: { 299 | form: form 300 | } 301 | }; 302 | } 303 | } 304 | }; 305 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-form-utils", 3 | "version": "1.1.3", 4 | "description": "Ease the pain of handling form bindings in Redux", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/mocha --compilers js:babel-register test", 8 | "test:watch": "./node_modules/.bin/mocha --compilers js:babel-register test -w", 9 | "watch": "NODE_ENV=production ./node_modules/.bin/babel src/ --out-dir lib/ -w", 10 | "build": "NODE_ENV=production ./node_modules/.bin/babel src/ --out-dir lib/" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/jasonslyvia/redux-form-utils.git" 15 | }, 16 | "keywords": [ 17 | "redux", 18 | "redux-form", 19 | "redux-form-utils", 20 | "react", 21 | "flux" 22 | ], 23 | "author": "jasonslyvia (http://undefinedblog.com/)", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/jasonslyvia/redux-form-utils/issues" 27 | }, 28 | "homepage": "https://github.com/jasonslyvia/redux-form-utils#readme", 29 | "devDependencies": { 30 | "babel-cli": "^6.4.5", 31 | "babel-preset-es2015": "~6.3.13", 32 | "babel-preset-react": "^6.3.13", 33 | "babel-preset-stage-0": "~6.3.13", 34 | "babel-register": "^6.4.3", 35 | "chai": "^3.4.1", 36 | "mocha": "^2.3.4", 37 | "react": "^0.14.6", 38 | "react-dom": "^0.14.6" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | /** 4 | * @param {string} options.form A unique key to identify your form throughout the app 5 | * @param {array} options.fields An array of string or object to configure the form fields 6 | * @return {object} Enhanced React Component 7 | */ 8 | export function createForm({ form, fields }) { 9 | return (Component) => { 10 | class ReduxForm extends React.Component { 11 | static propTypes = { 12 | form: PropTypes.object, 13 | }; 14 | 15 | static contextTypes = { 16 | store: React.PropTypes.object 17 | }; 18 | 19 | constructor(props) { 20 | super(props); 21 | this.displayName = form + 'Form'; 22 | this.state = {}; 23 | } 24 | 25 | componentWillMount() { 26 | this.dispatch = this.props.dispatch || (this.context && this.context.store && this.context.store.dispatch); 27 | if (typeof this.dispatch !== 'function') { 28 | throw new ReferenceError(`[redux-form-utils] Please pass \`dispatch\` to ${form} as props or connect it with Redux's store.`); 29 | } 30 | } 31 | 32 | handleChange(key, e) { 33 | let value = e; 34 | 35 | if (typeof e === 'object') { 36 | if (typeof e.target === 'object') { 37 | if (e.target.tagName.toLowerCase() === 'input' && ['checkbox', 'radio'].indexOf(e.target.type) > -1) { 38 | value = e.target.checked; 39 | } else { 40 | value = e.target.value; 41 | } 42 | } else if (e.value !== undefined) { 43 | value = e.value; 44 | } 45 | } 46 | 47 | this.dispatch({ 48 | type: '@@form/VALUE_CHANGE', 49 | meta: { 50 | form: form, 51 | field: key, 52 | complex: value === undefined, 53 | }, 54 | payload: value !== undefined ? value : { ...e }, 55 | }); 56 | } 57 | 58 | clearAll() { 59 | this.dispatch({ 60 | type: '@@form/CLEAR_ALL', 61 | meta: { 62 | form: form, 63 | }, 64 | }); 65 | } 66 | 67 | clear(field) { 68 | if (field && fields.indexOf(field) > -1) { 69 | this.dispatch({ 70 | type: '@@form/CLEAR', 71 | meta: { 72 | form: form, 73 | field: field, 74 | }, 75 | }); 76 | } 77 | } 78 | 79 | render() { 80 | return ( 81 | { 82 | if (!this.props.form) { 83 | throw new Error(`[redux-form-utils] \`${this.displayName}.props.form\` is not found, make sure add \`formState\` to initialState using \`bindRedux\` in your reducer.`); 84 | } 85 | 86 | if (typeof curr === 'string') { 87 | prev[curr] = { 88 | value: this.props.form[curr].value, 89 | onChange: this.handleChange.bind(this, curr), 90 | }; 91 | } else { 92 | const { key, valueKey = 'value', changeType = 'onChange' } = curr; 93 | let { resolver, resovler } = curr; 94 | 95 | // backward compatible for a typo 96 | if (!resolver) { 97 | resolver = resovler; 98 | } 99 | 100 | if (!key || typeof key !== 'string') { 101 | throw new TypeError('[redux-form-utils] If you provide an object within \`fields\` options, make sure this object has a key which named \`key\`, and the type of it\'s value is string.'); 102 | } 103 | 104 | prev[key] = { 105 | [valueKey]: this.props.form[key][valueKey], 106 | [changeType]: (a, b, c, d) => { 107 | if (resolver) { 108 | const payload = resolver(a, b, c, d); 109 | this.handleChange.call(this, key, payload); 110 | } else { 111 | this.handleChange.call(this, key, a, b, c, d); 112 | } 113 | }, 114 | }; 115 | } 116 | 117 | return prev; 118 | }, {})} 119 | clearAll={::this.clearAll} 120 | clear={::this.clear} /> 121 | ); 122 | } 123 | } 124 | 125 | return ReduxForm; 126 | }; 127 | } 128 | 129 | 130 | export function bindRedux({ form, fields }) { 131 | return { 132 | state: { 133 | form: fields.reduce((prev, curr) => { 134 | if (typeof curr === 'string') { 135 | prev[curr] = { 136 | value: '', 137 | }; 138 | } else { 139 | const { key, valueKey = 'value', initValue } = curr; 140 | prev[key] = { 141 | [valueKey]: initValue !== undefined ? initValue : '', 142 | }; 143 | } 144 | 145 | return prev; 146 | }, {}), 147 | }, 148 | 149 | setInitValue(initObj, state) { 150 | if (!state || !state.form) { 151 | return state; 152 | } 153 | 154 | return { 155 | ...state, 156 | form: { 157 | ...state.form, 158 | ...Object.keys(initObj).reduce((prev, curr) => { 159 | if (typeof initObj[curr] !== 'object') { 160 | return { 161 | ...prev, 162 | [curr]: { 163 | value: initObj[curr], 164 | }, 165 | }; 166 | } 167 | 168 | return { 169 | ...prev, 170 | [curr]: initObj[curr], 171 | }; 172 | }, {}), 173 | }, 174 | }; 175 | }, 176 | 177 | reducer(state, action) { 178 | if (action.type.indexOf('@@form') !== 0 || action.meta.form !== form) { 179 | return state; 180 | } 181 | 182 | function findConfig(field) { 183 | const fieldConfig = fields.filter(k => { 184 | if (typeof k === 'object') { 185 | return k.key === field; 186 | } 187 | 188 | return k === field; 189 | }); 190 | 191 | return fieldConfig[0] || {}; 192 | } 193 | 194 | switch (action.type) { 195 | case '@@form/VALUE_CHANGE': { 196 | const fieldConfig = findConfig(action.meta.field); 197 | let newField; 198 | if (action.meta.complex) { 199 | return { 200 | ...state, 201 | form: { 202 | ...state.form, 203 | [action.meta.field]: { 204 | ...state.form[action.meta.field], 205 | ...action.payload, 206 | }, 207 | }, 208 | }; 209 | } 210 | 211 | if (typeof action.payload === 'object') { 212 | return { 213 | ...state, 214 | form: { 215 | ...state.form, 216 | [action.meta.field]: { 217 | ...state.form[action.meta.field], 218 | ...action.payload, 219 | }, 220 | }, 221 | }; 222 | } 223 | 224 | return { 225 | ...state, 226 | form: { 227 | ...state.form, 228 | [action.meta.field]: { 229 | ...state.form[action.meta.field], 230 | [`${fieldConfig.valueKey || 'value'}`]: action.payload, 231 | }, 232 | }, 233 | }; 234 | } 235 | 236 | case '@@form/CLEAR_ALL': { 237 | return { 238 | ...state, 239 | form: Object.keys(state.form).reduce((prev, curr) => { 240 | const fieldConfig = findConfig(curr); 241 | prev[curr] = { 242 | ...state.form[curr], 243 | [`${fieldConfig.valueKey || 'value'}`]: fieldConfig.initValue || '', 244 | }; 245 | 246 | return prev; 247 | }, {}), 248 | }; 249 | } 250 | 251 | case '@@form/CLEAR': { 252 | const fieldConfig = findConfig(action.meta.field); 253 | 254 | return { 255 | ...state, 256 | form: { 257 | ...state.form, 258 | [action.meta.field]: { 259 | ...state.form[action.meta.field], 260 | [`${fieldConfig.valueKey || 'value'}`]: fieldConfig.initValue || '', 261 | }, 262 | }, 263 | }; 264 | } 265 | 266 | default: 267 | return state; 268 | } 269 | }, 270 | 271 | actionCreators: { 272 | clear(field) { 273 | return { 274 | type: '@@form/CLEAR', 275 | meta: { 276 | form: form, 277 | field: field, 278 | }, 279 | }; 280 | }, 281 | 282 | clearAll() { 283 | return { 284 | type: '@@form/CLEAR_ALL', 285 | meta: { 286 | form: form, 287 | }, 288 | }; 289 | } 290 | }, 291 | }; 292 | } 293 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createForm, bindRedux } from '../lib/'; 4 | import { expect } from 'chai'; 5 | 6 | describe('redux-form-utils', () => { 7 | it('should return initialState for `bindRedux`', () => { 8 | const redux = bindRedux({ 9 | form: 'test', 10 | fields: ['name'] 11 | }); 12 | 13 | expect(redux).to.be.an('object'); 14 | expect(redux.state).to.be.an('object'); 15 | expect(redux.state.form).to.be.an('object'); 16 | expect(redux.state.form.name).to.be.an('object'); 17 | expect(redux.state.form.name.value).to.equal(''); 18 | }); 19 | 20 | 21 | it('should return all fields defined in options', () => { 22 | const redux = bindRedux({ 23 | form: 'test', 24 | fields: ['name', 'address'] 25 | }); 26 | 27 | expect(redux.state.form.name.value).to.equal(''); 28 | expect(redux.state.form.address.value).to.equal(''); 29 | }); 30 | 31 | 32 | it('should return all fields defined in options as object', () => { 33 | const redux = bindRedux({ 34 | form: 'test', 35 | fields: [{ 36 | key: 'name', 37 | valueKey: 'testName', 38 | }, { 39 | key: 'address', 40 | valueKey: 'awesomeAddress', 41 | }] 42 | }); 43 | 44 | expect(redux.state.form.name.testName).to.equal(''); 45 | expect(redux.state.form.address.awesomeAddress).to.equal(''); 46 | }); 47 | 48 | 49 | it('should return a reducer for `bindRedux`', () => { 50 | const redux = bindRedux({ 51 | form: 'test', 52 | fields: ['name'] 53 | }); 54 | 55 | expect(redux.reducer).to.be.a('function'); 56 | expect(redux.reducer.length).to.equal(2); 57 | }); 58 | 59 | it('reducer should do nothing if it is not form change', () => { 60 | const redux = bindRedux({ 61 | form: 'test', 62 | fields: ['name'] 63 | }); 64 | 65 | const newState = redux.reducer({a: 1}, {type: 'SOME_ACTION'}); 66 | expect(newState).to.eql({a: 1}); 67 | }); 68 | 69 | it('should change form value in reducer', () => { 70 | const redux = bindRedux({ 71 | form: 'test', 72 | fields: ['name'] 73 | }); 74 | 75 | const newState = redux.reducer({ 76 | form: { 77 | name: { 78 | value: '' 79 | } 80 | } 81 | }, { 82 | type: '@@form/VALUE_CHANGE', 83 | meta: { 84 | field: 'name', 85 | form: 'test', 86 | }, 87 | payload: '123' 88 | }); 89 | 90 | expect(newState.form.name.value).to.equal('123'); 91 | }); 92 | 93 | it('should clear form value in reducer', () => { 94 | const redux = bindRedux({ 95 | form: 'test', 96 | fields: ['name'] 97 | }); 98 | 99 | const newState = redux.reducer({ 100 | form: { 101 | name: { 102 | value: '123' 103 | } 104 | } 105 | }, { 106 | type: '@@form/CLEAR', 107 | meta: { 108 | field: 'name', 109 | form: 'test', 110 | } 111 | }); 112 | 113 | expect(newState.form.name.value).to.equal(''); 114 | }); 115 | 116 | it('should clear all form value in reducer', () => { 117 | const redux = bindRedux({ 118 | form: 'test', 119 | fields: ['name', 'address'] 120 | }); 121 | 122 | const newState = redux.reducer({ 123 | form: { 124 | name: { 125 | value: '123' 126 | }, 127 | address: { 128 | value: '345' 129 | } 130 | } 131 | }, { 132 | type: '@@form/CLEAR_ALL', 133 | meta: { 134 | form: 'test', 135 | } 136 | }); 137 | 138 | expect(newState.form.name.value).to.equal(''); 139 | expect(newState.form.address.value).to.equal(''); 140 | }); 141 | 142 | }); 143 | --------------------------------------------------------------------------------