├── .babelrc ├── .gitignore ├── .npmrc ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── etc └── enzymeSetup.js ├── example ├── index.html ├── index.jsx ├── package.json ├── src │ ├── Input.jsx │ └── MyForm.jsx ├── webpack.config.js └── yarn.lock ├── gulpfile.js ├── lerna.json ├── package.json ├── packages ├── redux-forms-react │ ├── README.md │ ├── __tests__ │ │ └── integration.spec.tsx │ ├── package.json │ └── src │ │ ├── Form.ts │ │ ├── __tests__ │ │ ├── Form.spec.tsx │ │ ├── connectField.spec.tsx │ │ ├── field.spec.tsx │ │ └── fieldArray.spec.tsx │ │ ├── connectField.ts │ │ ├── field.ts │ │ ├── fieldArray.ts │ │ └── index.ts └── redux-forms │ ├── README.md │ ├── actions.d.ts │ ├── actions.js │ ├── package.json │ ├── selectors.d.ts │ ├── selectors.js │ └── src │ ├── __tests__ │ ├── actions.spec.ts │ ├── arrays.spec.ts │ ├── reducer.spec.ts │ └── selectors.spec.ts │ ├── actions.ts │ ├── arrays.ts │ ├── containers.ts │ ├── index.ts │ ├── reducer.ts │ ├── selectors.ts │ └── shared │ ├── __tests__ │ ├── fieldArrayProps.spec.ts │ ├── fieldProps.spec.ts │ ├── formProps.spec.ts │ ├── getValue.spec.ts │ └── helpers.spec.ts │ ├── fieldArrayProps.ts │ ├── fieldProps.ts │ ├── formProps.ts │ ├── getValue.ts │ └── helpers.ts ├── tsconfig.json ├── tslint.json ├── types └── prop-types.d.ts ├── webpack.config.js ├── webpack.packages.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", ["es2015", { "loose": true }], "stage-3"], 3 | "plugins": ["ramda"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .atom 3 | .idea 4 | node_modules 5 | coverage 6 | .tmp 7 | lib 8 | dist 9 | bundle.js 10 | npm-debug.log 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | git-tag-version = true 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "6" 5 | - "7" 6 | - "stable" 7 | 8 | script: 9 | - npm run bootstrap 10 | - npm test 11 | - npm run lint 12 | 13 | after_success: 14 | - bash <(curl -s https://codecov.io/bash) 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Feel free to create an issue general questions, bugs or feature requests. 4 | 5 | ## Workflow 6 | 7 | * Fork the repo 8 | * Run `npm run bootstrap` 9 | * Do your changes + **write tests** 10 | * Run `npm run build`, `npm run test` and `npm run lint` 11 | * **Nicely** commit your changes if all is OK 12 | * Submit your PR and describe your changes! 13 | 14 | Happy contributing! 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Boris Petrenko 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-forms 2 | 3 | [![Build Status](https://travis-ci.org/oreqizer/redux-forms.svg?branch=master)](https://travis-ci.org/oreqizer/redux-forms) 4 | [![codecov](https://codecov.io/gh/oreqizer/redux-forms/branch/master/graph/badge.svg)](https://codecov.io/gh/oreqizer/redux-forms) 5 | 6 | A simple form manager for **Redux**. Bindings available for **React**! 7 | 8 | ### Packages 9 | 10 | * `redux-forms` [![npm](https://img.shields.io/npm/v/redux-forms.svg)](https://www.npmjs.com/package/redux-forms) 11 | * `redux-forms-react` [![npm](https://img.shields.io/npm/v/redux-forms-react.svg)](https://www.npmjs.com/package/redux-forms-react) 12 | 13 | ## Size 14 | 15 | * `redux-forms` alone is **7kb** gzipped. 16 | * `redux-forms-react` is **10kb** with `redux-forms` included! 17 | 18 | **Dependencies** 19 | 20 | * Ramda 21 | 22 | The build process includes `babel-plugin-ramda`, so no unnecessary functions get into your bundle! 23 | 24 | ## Installation 25 | 26 | Simply: 27 | 28 | `yarn add redux-forms` 29 | 30 | Or: 31 | 32 | `npm i redux-forms --save` 33 | 34 | Then just install bindings for any UI library you prefer. 35 | 36 | ## Quickstart 37 | 38 | Mount the `redux-forms` reducer to your root reducer as `reduxForms`. 39 | 40 | ```js 41 | import { createStore, combineReducers } from 'redux'; 42 | import reduxFormsReducer from 'redux-forms'; 43 | 44 | const rootReducer = combineReducers({ 45 | // ... your other reducers 46 | reduxForms: reduxFormsReducer, 47 | }); 48 | 49 | const store = createStore(rootReducer); 50 | ``` 51 | 52 | Create a component wrapped in the `field` decorator. 53 | 54 | ```js 55 | import { field } from 'redux-forms-react'; 56 | 57 | const Input = props => ( 58 | 59 | ); 60 | 61 | export default field(Input); 62 | ``` 63 | 64 | Then simply wrap your desired form with the `Form` component and you're ready to go! 65 | 66 | ```js 67 | import { Form } from 'redux-forms-react'; 68 | 69 | import Input from './Input'; 70 | 71 | const MyForm = props => ( 72 |
73 |
74 | 75 | 76 |
77 |
78 | 79 | 80 |
81 | 82 |
83 | ); 84 | 85 | export default MyForm; 86 | ``` 87 | 88 | That's it! This is how you mount the most basic form. For more advanced usage, check out the API docs below. 89 | 90 | ## Documentation 91 | 92 | * [reducer](https://oreqizer.gitbooks.io/redux-forms/content/reducer.html) 93 | * [Form](https://oreqizer.gitbooks.io/redux-forms/content/form.html) 94 | * [field](https://oreqizer.gitbooks.io/redux-forms/content/field.html) 95 | * [fieldArray](https://oreqizer.gitbooks.io/redux-forms/content/fieldarray.html) 96 | * [selectors](https://oreqizer.gitbooks.io/redux-forms/content/selectors.html) 97 | * [actions](https://oreqizer.gitbooks.io/redux-forms/content/actions.html) 98 | 99 | ## Migrating from 0.11.x 100 | 101 | The API in `redux-forms-react` for `Field` and `FieldArray` changed from the `0.11.x` version. The reasons are: 102 | 103 | * less magic with supplied props 104 | * better type support in both _TypeScript_ and _Flow_ 105 | * easier unit testing 106 | * less overhead with imports 107 | 108 | ### Field -> field 109 | 110 | The `Field` higher order component changed to a `field` decorator. 111 | 112 | > Note: native components are no longer supported, you have to provide a regular component. 113 | 114 | This is how you upgrade your fields: 115 | 116 | **Before:** 117 | ```js 118 | // Input.js 119 | const Input = props => ( 120 | 121 | ); 122 | 123 | export default Input; 124 | 125 | // MyForm.js 126 | const MyForm = props => ( 127 |
128 |
129 | 130 | 131 | 132 | 133 |
134 | 135 |
136 | ); 137 | ``` 138 | 139 | **After:** 140 | ```js 141 | // Input.js 142 | const Input = props => ( 143 | 144 | ); 145 | 146 | export default field(Input); 147 | 148 | // MyForm.js 149 | const MyForm = props => ( 150 |
151 |
152 | 153 | 154 |
155 | 156 |
157 | ); 158 | ``` 159 | 160 | ### FieldArray -> fieldArray 161 | 162 | The `FieldArray` higher order component changed to a `fieldArray` decorator. 163 | 164 | This is how you upgrade your field arrays: 165 | 166 | **Before:** 167 | ```js 168 | // Inputs.js 169 | const Inputs = props => ( 170 |
171 | {props.fields.map(id => ( 172 | 173 | 174 | 175 | ))} 176 | 179 |
180 | ); 181 | 182 | export default Inputs; 183 | 184 | // MyForm.js 185 | const MyForm = props => ( 186 |
187 | 188 | 189 | 190 | 191 |
192 | ); 193 | ``` 194 | 195 | **After:** 196 | ```js 197 | // Inputs.js 198 | const Inputs = props => ( 199 |
200 | {props.fields.map(id => ( 201 | 202 | ))} 203 | 206 |
207 | ); 208 | 209 | export default fieldArray(Inputs); 210 | 211 | // MyForm.js 212 | const MyForm = props => ( 213 |
214 | 215 | 216 | 217 | ); 218 | ``` 219 | 220 | ## License 221 | 222 | MIT 223 | -------------------------------------------------------------------------------- /etc/enzymeSetup.js: -------------------------------------------------------------------------------- 1 | import { configure } from "enzyme"; 2 | import Adapter from "enzyme-adapter-react-16"; 3 | 4 | configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | redux-forms 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, combineReducers, applyMiddleware } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import reduxForms from 'redux-forms'; 6 | import { createLogger } from 'redux-logger'; 7 | 8 | import MyForm from './src/MyForm'; 9 | 10 | 11 | const logger = createLogger({ collapsed: true }); 12 | const store = createStore(combineReducers({ 13 | reduxForms, 14 | }), {}, applyMiddleware(logger)); 15 | 16 | 17 | const onSubmit = (values) => console.log(values); 18 | 19 | const Root = () => ( 20 | 21 | 22 | 23 | ); 24 | 25 | const node = document.getElementById('root'); // eslint-disable-line no-undef 26 | 27 | ReactDOM.render(, node); 28 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-forms-example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "description": "An example of redux-forms.", 6 | "main": "index.jsx", 7 | "scripts": { 8 | "build": "webpack index.jsx bundle.js" 9 | }, 10 | "author": "oreqizer", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "babel-core": "^6.26.0", 14 | "babel-loader": "^7.1.2", 15 | "babel-preset-es2015": "^6.24.1", 16 | "babel-preset-react": "^6.24.1", 17 | "webpack": "3.8.1" 18 | }, 19 | "dependencies": { 20 | "react": "^16.0.0", 21 | "react-redux": "^5.0.6", 22 | "redux": "^3.7.2", 23 | "redux-forms": "^1.0.0-2", 24 | "redux-forms-react": "^1.0.0-7", 25 | "redux-logger": "^3.0.6" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/src/Input.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { field } from 'redux-forms-react'; 3 | 4 | const Input = props => ( 5 |
6 |

{props.input.name}

7 |
Error: {String(props.meta.error)}
8 |
Dirty: {String(props.meta.dirty)}
9 |
Touched: {String(props.meta.touched)}
10 |
Visited: {String(props.meta.visited)}
11 |
Active: {String(props.meta.active)}
12 | 17 |
18 | ); 19 | 20 | export default field(Input); 21 | -------------------------------------------------------------------------------- /example/src/MyForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Form, fieldArray } from 'redux-forms-react'; 4 | import { getValues } from 'redux-forms/selectors'; 5 | 6 | import Input from './Input'; 7 | 8 | const InputArray = fieldArray(props => ( 9 |
10 | 13 | 16 | 19 | 22 | {props.fields.map((id, index) => 23 |
24 | 25 | 28 | 31 | 34 | 37 |
38 | )} 39 |
40 | )); 41 | 42 | const DeepArray = fieldArray(props => ( 43 |
44 | 47 | 50 | {props.fields.map(id => 51 |
52 | 53 |
54 | 55 |
56 | )} 57 |
58 | )); 59 | 60 | const validate = value => value.length < 5 ? 'too short' : null; 61 | 62 | const MyForm = props => ( 63 |
64 |

My form:

65 | 70 | 75 |
76 | 77 |
78 | 79 |
---
80 |
81 | Values: 82 |
{JSON.stringify(props.values, null, 2)}
83 |
84 | 87 | 88 | ); 89 | 90 | export default connect(state => ({ 91 | values: getValues('first', state), 92 | }))(MyForm); 93 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const webpack = require('webpack'); 3 | 4 | const config = { 5 | module: { 6 | loaders: [ 7 | { test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/ }, 8 | ], 9 | }, 10 | resolve: { 11 | extensions: ['.js', '.jsx'], 12 | }, 13 | plugins: [ 14 | new webpack.optimize.OccurrenceOrderPlugin(), 15 | new webpack.DefinePlugin({ 16 | 'process.env.NODE_ENV': JSON.stringify('development'), 17 | }), 18 | ], 19 | }; 20 | 21 | module.exports = config; 22 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const gutil = require('gulp-util'); 3 | const plumber = require('gulp-plumber'); 4 | const ts = require('gulp-typescript'); 5 | const babel = require('gulp-babel'); 6 | 7 | const chalk = require('chalk'); 8 | const through = require('through2'); 9 | 10 | 11 | const base = [ 12 | './types/*', 13 | './packages/redux-forms/src/**/*.{ts,tsx}' 14 | ]; 15 | 16 | const srcts = [ 17 | './types/*', 18 | './packages/*/src/**/*.{ts,tsx}', 19 | '!./packages/redux-forms/src/**/*.{ts,tsx}', 20 | '!**/__tests__/**', 21 | ]; 22 | 23 | const srcbabel = './packages/*/lib/**/*.js'; 24 | 25 | const dest = './packages'; 26 | 27 | const tsOptions = { 28 | module: 'es6', 29 | target: 'es6', 30 | jsx: 'react', 31 | declaration: true, 32 | noImplicitAny: true, 33 | strictNullChecks: true, 34 | allowSyntheticDefaultImports: true, 35 | }; 36 | 37 | const mapDest = (path) => path.replace(/(packages\/[^/]+)\/src\//, '$1/lib/'); 38 | 39 | 40 | gulp.task('default', ['babel']); 41 | 42 | gulp.task('ts:base', () => 43 | gulp.src(base) 44 | .pipe(plumber()) 45 | .pipe(through.obj((file, enc, callback) => { 46 | gutil.log(`Compiling ${chalk.blue(file.path)}...`); 47 | callback(null, file); 48 | })) 49 | .pipe(ts(tsOptions)) 50 | .pipe(through.obj((file, enc, callback) => { 51 | file.path = mapDest(file.path); 52 | callback(null, file); 53 | })) 54 | .pipe(gulp.dest('./packages/redux-forms/lib/'))); 55 | 56 | gulp.task('ts', ['ts:base'], () => 57 | gulp.src(srcts) 58 | .pipe(plumber()) 59 | .pipe(through.obj((file, enc, callback) => { 60 | gutil.log(`Compiling ${chalk.blue(file.path)}...`); 61 | callback(null, file); 62 | })) 63 | .pipe(ts(tsOptions)) 64 | .pipe(through.obj((file, enc, callback) => { 65 | file.path = mapDest(file.path); 66 | callback(null, file); 67 | })) 68 | .pipe(gulp.dest(dest))); 69 | 70 | gulp.task('babel', ['ts'], () => 71 | gulp.src(srcbabel) 72 | .pipe(plumber()) 73 | .pipe(through.obj((file, enc, callback) => { 74 | gutil.log(`Transpiling ${chalk.yellow(file.path)}...`); 75 | callback(null, file); 76 | })) 77 | .pipe(babel()) 78 | .pipe(gulp.dest(dest))); 79 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.0.0-rc.5", 3 | "version": "independent", 4 | "publishConfig": { 5 | "ignore": [ 6 | "*.md", 7 | "*.spec.*" 8 | ] 9 | }, 10 | "packages": [ 11 | "packages/*" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "jest", 5 | "test:watch": "jest --watch", 6 | "test:coverage": "jest --coverage", 7 | "lint": "npm run tslint", 8 | "tslint": "tslint -e '**/lib/**' 'packages/**/*.{ts,tsx}'", 9 | "build": "npm run build:lib && npm run build:umd && npm run build:umd:min", 10 | "build:lib": "gulp", 11 | "build:umd": "webpack", 12 | "build:umd:min": "cross-env NODE_ENV=production webpack", 13 | "clean": "npm run clean:deps && npm run clean:build", 14 | "clean:build": "rm -rf packages/*/lib", 15 | "clean:deps": "rm -rf node_modules packages/*/node_modules", 16 | "bootstrap": "npm run clean && yarn && lerna bootstrap && npm run build:lib", 17 | "release": "npm run clean:build && npm run build && npm test && npm run lint && lerna publish" 18 | }, 19 | "jest": { 20 | "moduleFileExtensions": [ 21 | "ts", 22 | "tsx", 23 | "js", 24 | "jsx", 25 | "json" 26 | ], 27 | "transform": { 28 | ".jsx?": "babel-jest", 29 | ".tsx?": "/node_modules/ts-jest/preprocessor.js" 30 | }, 31 | "setupFiles": [ 32 | "raf/polyfill", 33 | "./etc/enzymeSetup" 34 | ], 35 | "testRegex": "/__tests__/.*\\.spec\\.(ts|tsx)$", 36 | "coverageDirectory": "./coverage/", 37 | "collectCoverage": true 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/oreqizer/redux-forms.git" 42 | }, 43 | "author": "oreqizer", 44 | "license": "MIT", 45 | "devDependencies": { 46 | "@types/jest": "~21.1.5", 47 | "@types/ramda": "~0.24.18", 48 | "@types/react-redux": "~5.0.11", 49 | "@types/redux": "~3.6.0", 50 | "babel-cli": "~6.26.0", 51 | "babel-jest": "~21.2.0", 52 | "babel-loader": "~7.1.2", 53 | "babel-plugin-ramda": "~1.4.3", 54 | "babel-preset-es2015": "~6.24.1", 55 | "babel-preset-react": "~6.24.1", 56 | "babel-preset-stage-3": "~6.24.1", 57 | "chalk": "~2.3.0", 58 | "cross-env": "~5.1.0", 59 | "enzyme": "~3.1.0", 60 | "enzyme-adapter-react-16": "~1.0.2", 61 | "gulp": "~3.9.1", 62 | "gulp-babel": "~7.0.0", 63 | "gulp-plumber": "~1.1.0", 64 | "gulp-typescript": "~3.2.3", 65 | "gulp-util": "~3.0.8", 66 | "jest": "~21.2.1", 67 | "lerna": "2.4.0", 68 | "prop-types": "~15.6.0", 69 | "raf": "~3.4.0", 70 | "ramda": "~0.25.0", 71 | "react": "~16.0.0", 72 | "react-dom": "~16.0.0", 73 | "react-redux": "~5.0.6", 74 | "redux": "~3.7.2", 75 | "rimraf": "~2.6.2", 76 | "through2": "~2.0.3", 77 | "ts-jest": "~21.1.3", 78 | "ts-loader": "~3.1.0", 79 | "tslint": "~5.8.0", 80 | "tslint-react": "~3.2.0", 81 | "typescript": "~2.5.3", 82 | "webpack": "~3.8.1" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/redux-forms-react/README.md: -------------------------------------------------------------------------------- 1 | # redux-forms-react 2 | 3 | [![npm](https://img.shields.io/npm/v/redux-forms-react.svg)](https://www.npmjs.com/package/redux-forms-react) 4 | 5 | **React** bindings for `redux-forms`. 6 | 7 | Contains: 8 | * **form** 9 | * **field** 10 | * **FieldArray** 11 | 12 | Check out the [docs](https://oreqizer.gitbooks.io/redux-forms/content) for details. 13 | -------------------------------------------------------------------------------- /packages/redux-forms-react/__tests__/integration.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { createStore, combineReducers } from 'redux'; 3 | import { Provider } from 'react-redux'; 4 | import { mount } from "enzyme"; 5 | 6 | import reducer from 'redux-forms/lib/index'; 7 | import { form, field } from 'redux-forms/lib/containers'; 8 | import { Form, field as fieldDecorator, fieldArray } from '../src/'; 9 | 10 | const Field = fieldDecorator((props: any) => ( 11 | 12 | )); 13 | 14 | const FlatFieldsComponent = (props: any) => ( 15 |
16 | {props.fields.map((id: string) => ( 17 | 18 | ))} 19 |
20 | ); 21 | const FlatFields = fieldArray(FlatFieldsComponent); 22 | 23 | const DeepFieldsComponent = (props: any) => ( 24 |
25 | {props.fields.map((id: string) => ( 26 |
27 | 28 | 29 |
30 | ))} 31 |
32 | ); 33 | const DeepFields = fieldArray(DeepFieldsComponent); 34 | 35 | const MyForm = () => ( 36 |
37 | 38 | 39 | 40 | 41 | ); 42 | 43 | // Any to allow nested property dot notation 44 | const newStore = () => createStore(combineReducers({ 45 | reduxForms: reducer, 46 | })); 47 | 48 | const getForm = (store: any) => store.getState().reduxForms.test; 49 | 50 | 51 | describe('#integration', () => { 52 | it('should initialize properly', () => { 53 | const store = newStore(); 54 | const wrapper = mount(( 55 | 56 | 57 | 58 | )); 59 | 60 | const f = getForm(store); 61 | expect(f.fields).toEqual({ title: field }); 62 | expect(f.arrays).toEqual({ flatarray: 0, deeparray: 0 }); 63 | }); 64 | 65 | it('should add a field to a flat array', () => { 66 | const store = newStore(); 67 | const wrapper = mount(( 68 | 69 | 70 | 71 | )); 72 | 73 | wrapper.find(FlatFieldsComponent).prop('fields').push(); 74 | 75 | const f = getForm(store); 76 | expect(f.fields).toEqual({ 'title': field, 'flatarray.0': field }); 77 | expect(f.arrays).toEqual({ flatarray: 1, deeparray: 0 }); 78 | }); 79 | 80 | it('should add a field to a deep array', () => { 81 | const store = newStore(); 82 | const wrapper = mount(( 83 | 84 | 85 | 86 | )); 87 | 88 | wrapper.find(DeepFieldsComponent).prop('fields').push(); 89 | 90 | const f = getForm(store); 91 | expect(f.arrays).toEqual({ flatarray: 0, deeparray: 1 }); 92 | expect(f.fields).toEqual({ 93 | 'title': field, 94 | 'deeparray.0.name': field, 95 | 'deeparray.0.surname': field, 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /packages/redux-forms-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-forms-react", 3 | "version": "1.0.0-8", 4 | "description": "A simple form management for React & Redux.", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "repository": "https://github.com/oreqizer/redux-forms/tree/master/packages/redux-forms-react", 8 | "keywords": [ 9 | "form", 10 | "forms", 11 | "redux", 12 | "react" 13 | ], 14 | "author": "oreqizer", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/oreqizer/redux-forms/issues" 18 | }, 19 | "peerDependencies": { 20 | "prop-types": "^15.6.0", 21 | "react": "^16.0.0", 22 | "react-redux": "^5.0.6", 23 | "redux": "^3.7.2" 24 | }, 25 | "dependencies": { 26 | "ramda": "^0.25.0", 27 | "redux-forms": "^1.0.0-3" 28 | }, 29 | "files": [ 30 | "dist", 31 | "lib", 32 | "README.md" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/redux-forms-react/src/Form.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { 5 | identity, 6 | merge, 7 | prop, 8 | } from 'ramda'; 9 | 10 | import { isString, isPromise, isFunction, shallowCompare } from 'redux-forms/lib/shared/helpers'; 11 | import formProps, { toUpdate } from 'redux-forms/lib/shared/formProps'; 12 | import * as containers from 'redux-forms/lib/containers'; 13 | import * as actions from 'redux-forms/actions'; 14 | import * as selectors from 'redux-forms/selectors'; 15 | 16 | 17 | // FIXME don't use 'values: any'. TS doesn't understand I have my own onSubmit 18 | export interface IFormProps extends React.HTMLProps { 19 | name: string; 20 | persistent?: boolean; 21 | onSubmit?: (values: any) => Promise | null | undefined; 22 | withRef?: (el: HTMLFormElement) => void; 23 | } 24 | 25 | export type Context = { 26 | reduxForms: string; 27 | }; 28 | 29 | export type StateProps = { 30 | _form: boolean, 31 | _values: any, 32 | _valid: boolean, 33 | _submitting: boolean, 34 | }; 35 | 36 | export type ActionProps = { 37 | _addForm: typeof actions.addForm, 38 | _removeForm: typeof actions.removeForm, 39 | _touchAll: typeof actions.touchAll, 40 | _submitStart: typeof actions.submitStart, 41 | _submitStop: typeof actions.submitStop, 42 | }; 43 | 44 | export type Props = StateProps & ActionProps & IFormProps; 45 | 46 | 47 | class Form extends React.Component implements React.ChildContextProvider { 48 | static defaultProps = { 49 | persistent: false, 50 | onSubmit: () => null, 51 | withRef: () => null, 52 | // state 53 | _form: false, 54 | _values: {}, 55 | _valid: false, 56 | _submitting: false, 57 | // actions 58 | _addForm: identity, 59 | _removeForm: identity, 60 | _touchAll: identity, 61 | _submitStart: identity, 62 | _submitStop: identity, 63 | }; 64 | 65 | static childContextTypes = { 66 | reduxForms: PropTypes.string.isRequired, 67 | }; 68 | 69 | static propTypes = { 70 | name: PropTypes.string.isRequired, 71 | persistent: PropTypes.bool.isRequired, 72 | onSubmit: PropTypes.func.isRequired, 73 | withRef: PropTypes.func.isRequired, 74 | }; 75 | 76 | props: Props; 77 | 78 | constructor(props: Props) { 79 | super(props); 80 | 81 | this.handleSubmit = this.handleSubmit.bind(this); 82 | } 83 | 84 | shouldComponentUpdate(nextProps: Props) { 85 | return !shallowCompare(toUpdate(this.props), toUpdate(nextProps)); 86 | } 87 | 88 | componentWillMount() { 89 | const { name, _form, _addForm } = this.props; 90 | 91 | if (!_form) { 92 | _addForm(name); 93 | } 94 | } 95 | 96 | componentWillUnmount() { 97 | const { name, persistent, _removeForm } = this.props; 98 | 99 | if (!persistent) { 100 | _removeForm(name); 101 | } 102 | } 103 | 104 | getChildContext() { 105 | const { name } = this.props; 106 | 107 | return { 108 | reduxForms: name, 109 | }; 110 | } 111 | 112 | handleSubmit(ev: React.SyntheticEvent) { 113 | const { 114 | name, 115 | onSubmit, 116 | _valid, 117 | _values, 118 | _touchAll, 119 | _submitting, 120 | _submitStart, 121 | _submitStop, 122 | } = this.props; 123 | 124 | ev.preventDefault(); 125 | 126 | _touchAll(name); 127 | if (_submitting) { 128 | return; 129 | } 130 | 131 | if (!_valid || !isFunction(onSubmit)) { 132 | return; 133 | } 134 | 135 | const maybePromise = onSubmit(_values); 136 | if (isPromise(maybePromise)) { // TODO test this 137 | _submitStart(name); 138 | 139 | maybePromise.then(() => _submitStop(name)); 140 | } 141 | } 142 | 143 | render() { 144 | const { children, withRef, _form } = this.props; 145 | 146 | // Wait until form is initialized 147 | if (!_form) { 148 | return null; 149 | } 150 | 151 | return React.createElement('form', formProps(merge(this.props, { 152 | ref: withRef, 153 | onSubmit: this.handleSubmit, 154 | })), children); 155 | } 156 | } 157 | 158 | 159 | const Connected = connect((state, props: IFormProps) => ({ 160 | _form: Boolean(prop(props.name, state.reduxForms)), 161 | _values: selectors.getValues(props.name, state), 162 | _valid: selectors.isValid(props.name, state), 163 | _submitting: selectors.isSubmitting(props.name, state), 164 | }), { 165 | _addForm: actions.addForm, 166 | _removeForm: actions.removeForm, 167 | _touchAll: actions.touchAll, 168 | _submitStart: actions.submitStart, 169 | _submitStop: actions.submitStop, 170 | })(Form as any); 171 | 172 | Connected.displayName = 'Form'; 173 | 174 | export default Connected; 175 | -------------------------------------------------------------------------------- /packages/redux-forms-react/src/__tests__/Form.spec.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import * as React from 'react'; 3 | import { createStore, combineReducers } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import { mount } from 'enzyme'; 6 | import * as R from "ramda"; 7 | 8 | import reducer from 'redux-forms/lib/index'; 9 | import { form, field } from 'redux-forms/lib/containers'; 10 | import ConnectedForm from '../Form'; 11 | 12 | 13 | // NOTE: 14 | // We're unwrapping 'Form' from 'connect'. 15 | // Props needed mocking: 16 | // state: 17 | // _form: Form 18 | // _values: Object 19 | // _valid: boolean 20 | // _submitting: boolean 21 | // actions: 22 | // _addForm: AddFormCreator 23 | // _removeForm: RemoveFormCreator 24 | // _touchAll: TouchAllCreator 25 | // _submitStart: SubmitStartCreator 26 | // _submitStop: SubmitStopCreator 27 | const Form = (ConnectedForm as any).WrappedComponent; 28 | 29 | const MyComp = () => ( 30 |
31 | ); 32 | 33 | // Any to allow nested property dot notation 34 | const newStore = () => createStore(combineReducers({ 35 | reduxForms: reducer, 36 | }), { 37 | reduxForms: { test: form }, 38 | }); 39 | 40 | const getForm = (state: any) => state.getState().reduxForms.test; 41 | 42 | type Fn = Function; // tslint:disable-line ban-types 43 | const event = (pd: Fn) => ({ 44 | preventDefault: pd, 45 | }); 46 | 47 | 48 | describe('#Form', () => { 49 | it('should add a form', () => { 50 | const addForm = jest.fn(); 51 | const wrapper = mount(( 52 |
63 | 64 | 65 | )); 66 | 67 | expect(addForm).toBeCalledWith('test'); 68 | }); 69 | 70 | it('should not add a form if already present', () => { 71 | const addForm = jest.fn(); 72 | const wrapper = mount(( 73 |
84 | 85 | 86 | )); 87 | 88 | expect(addForm).not.toBeCalled(); 89 | }); 90 | 91 | it('should remove a form', () => { 92 | const removeForm = jest.fn(); 93 | const wrapper = mount(( 94 |
105 | 106 | 107 | )); 108 | 109 | expect(removeForm).not.toBeCalled(); 110 | 111 | wrapper.unmount(); 112 | 113 | expect(removeForm).toBeCalledWith('test'); 114 | }); 115 | 116 | it('should not remove a form if persistent', () => { 117 | const removeForm = jest.fn(); 118 | const wrapper = mount(( 119 |
131 | 132 | 133 | )); 134 | 135 | expect(removeForm).not.toBeCalled(); 136 | 137 | wrapper.unmount(); 138 | 139 | expect(removeForm).not.toBeCalled(); 140 | }); 141 | 142 | it('should not render without form', () => { 143 | const wrapper = mount(( 144 |
155 | 156 | 157 | )); 158 | 159 | expect(wrapper.isEmptyRender()).toBe(true); 160 | }); 161 | 162 | it('should not pass any private props', () => { 163 | const onSubmit = jest.fn(); 164 | const wrapper = mount(( 165 |
179 | 180 | 181 | )); 182 | 183 | expect(wrapper.find('form').prop('onSubmit')).not.toBe(onSubmit); 184 | 185 | expect(wrapper.find('form').prop('name')).toBeUndefined(); 186 | expect(wrapper.find('form').prop('persistent')).toBeUndefined(); 187 | expect(wrapper.find('form').prop('withRef')).toBeUndefined(); 188 | expect(wrapper.find('form').prop('_form')).toBeUndefined(); 189 | expect(wrapper.find('form').prop('_values')).toBeUndefined(); 190 | expect(wrapper.find('form').prop('_valid')).toBeUndefined(); 191 | expect(wrapper.find('form').prop('_submitting')).toBeUndefined(); 192 | expect(wrapper.find('form').prop('_addForm')).toBeUndefined(); 193 | expect(wrapper.find('form').prop('_removeForm')).toBeUndefined(); 194 | expect(wrapper.find('form').prop('_touchAll')).toBeUndefined(); 195 | expect(wrapper.find('form').prop('_submitStart')).toBeUndefined(); 196 | expect(wrapper.find('form').prop('_submitStop')).toBeUndefined(); 197 | }); 198 | 199 | it('should provide context', () => { 200 | const wrapper = mount(( 201 |
212 | 213 | 214 | )); 215 | 216 | expect((wrapper.instance() as any).getChildContext()).toEqual({ 217 | reduxForms: 'test', 218 | }); 219 | }); 220 | 221 | it('should prevent default and touch all with onSubmit', () => { 222 | const pd = jest.fn(); 223 | const touchAll = jest.fn(); 224 | const wrapper = mount(( 225 |
233 | 234 | 235 | )); 236 | 237 | wrapper.find('form').simulate('submit', event(pd)); 238 | 239 | expect(pd).toBeCalled(); 240 | expect(touchAll).toBeCalled(); 241 | }); 242 | 243 | it('should not fire onSubmit if invalid', () => { 244 | const pd = jest.fn(); 245 | const onSubmit = jest.fn(); 246 | const wrapper = mount(( 247 |
256 | 257 | 258 | )); 259 | 260 | wrapper.find('form').simulate('submit', event(pd)); 261 | 262 | expect(pd).toBeCalled(); 263 | expect(onSubmit).not.toBeCalled(); 264 | }); 265 | 266 | it('should not fire onSubmit if submitting', () => { 267 | const pd = jest.fn(); 268 | const touchAll = jest.fn(); 269 | const onSubmit = jest.fn(); 270 | const wrapper = mount(( 271 |
281 | 282 | 283 | )); 284 | 285 | wrapper.find('form').simulate('submit', event(pd)); 286 | 287 | expect(pd).toBeCalled(); 288 | expect(touchAll).toBeCalled(); 289 | expect(onSubmit).not.toBeCalled(); 290 | }); 291 | 292 | it('should fire onSubmit', () => { 293 | const touchAll = jest.fn(); 294 | const onSubmit = jest.fn(); 295 | const wrapper = mount(( 296 |
305 | 306 | 307 | )); 308 | 309 | wrapper.find('form').simulate('submit', event(jest.fn())); 310 | 311 | expect(touchAll).toBeCalled(); 312 | expect(onSubmit).toBeCalledWith({ test: 'yo' }); 313 | }); 314 | 315 | it('should fire submit start and stop', () => { 316 | let cb: Fn = (id: any) => id; 317 | const then = (fn: Fn) => { cb = fn; }; 318 | 319 | const pd = jest.fn(); 320 | const onSubmit: any = () => ({ then }); 321 | const submitStart = jest.fn(); 322 | const submitStop = jest.fn(); 323 | const wrapper = mount(( 324 |
335 | 336 | 337 | )); 338 | 339 | expect(submitStart).not.toBeCalled(); 340 | 341 | wrapper.find('form').simulate('submit', event(pd)); 342 | 343 | expect(pd).toBeCalled(); 344 | expect(submitStart).toBeCalled(); 345 | expect(submitStop).not.toBeCalled(); 346 | 347 | cb(); 348 | 349 | expect(submitStop).toBeCalled(); 350 | }); 351 | 352 | it('should fire ref callback on mount', () => { 353 | const withRef = jest.fn(); 354 | const wrapper = mount(( 355 |
366 | 367 | 368 | )); 369 | 370 | expect(withRef).toBeCalled(); 371 | }); 372 | 373 | it('should add a connected form', () => { 374 | const store = newStore(); 375 | const wrapper = mount(( 376 | 377 | 378 | 379 | 380 | 381 | )); 382 | 383 | expect(store.getState().reduxForms).toEqual({ test: form }); 384 | }); 385 | 386 | it('should remove a connected form', () => { 387 | const store = newStore(); 388 | const wrapper = mount(( 389 | 390 | 391 | 392 | 393 | 394 | )); 395 | 396 | wrapper.unmount(); 397 | 398 | expect(store.getState().reduxForms).toEqual({}); 399 | }); 400 | 401 | it('should not remove a form if persistent', () => { 402 | const store = newStore(); 403 | const wrapper = mount(( 404 | 405 | 406 | 407 | 408 | 409 | )); 410 | 411 | wrapper.unmount(); 412 | 413 | expect(store.getState().reduxForms).toEqual({ test: form }); 414 | }); 415 | }); 416 | -------------------------------------------------------------------------------- /packages/redux-forms-react/src/__tests__/connectField.spec.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import * as React from 'react'; 3 | import * as PropTypes from 'prop-types'; 4 | import { mount } from 'enzyme'; 5 | 6 | import connectField from '../connectField'; 7 | 8 | 9 | const MyComp: any = () => ( 10 |
11 | ); 12 | 13 | MyComp.displayName = 'MyComp'; 14 | 15 | const Decorated: any = connectField(MyComp); 16 | 17 | const context = { 18 | context: { 19 | reduxForms: 'test', 20 | }, 21 | childContextTypes: { 22 | reduxForms: PropTypes.string, 23 | }, 24 | }; 25 | 26 | 27 | describe('#connectField', () => { 28 | it('should not mount', () => { 29 | const mountFn = () => mount(); 30 | 31 | expect(mountFn).toThrowError(/Form/); 32 | }); 33 | 34 | it('should mount with prop', () => { 35 | const mountFn = () => mount(); 36 | 37 | expect(mountFn).not.toThrowError(/Form/); 38 | }); 39 | 40 | it('should keep the original name', () => { 41 | const wrapper = mount(, context); 42 | 43 | expect(wrapper.name()).toBe('MyComp'); 44 | }); 45 | 46 | it('should provide a reference to the original', () => { 47 | expect(Decorated.WrappedComponent).toBe(MyComp); 48 | }); 49 | 50 | it('should provide form name', () => { 51 | const wrapper = mount(, context); 52 | 53 | expect(wrapper.find(MyComp).prop('_form')).toBe('test'); 54 | }); 55 | 56 | it('should provide form name from prop', () => { 57 | const wrapper = mount(); 58 | 59 | expect(wrapper.find(MyComp).prop('_form')).toBe('prop'); 60 | expect(wrapper.find(MyComp).prop('form')).toBe('prop'); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /packages/redux-forms-react/src/__tests__/field.spec.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import * as React from 'react'; 3 | import * as PropTypes from 'prop-types'; 4 | import { shallow, mount } from 'enzyme'; 5 | import { createStore, combineReducers } from 'redux'; 6 | import { Provider } from 'react-redux'; 7 | import * as R from 'ramda'; 8 | 9 | import reducer from 'redux-forms/lib/index'; 10 | import { form, field } from 'redux-forms/lib/containers'; 11 | import * as actions from 'redux-forms/actions'; 12 | import fieldDecorator from '../field'; 13 | 14 | 15 | const Component = (props: any) => ( 16 | 23 | ); 24 | 25 | const ConnectedField = fieldDecorator(Component); 26 | const Field = (ConnectedField as any).WrappedComponent.WrappedComponent; 27 | 28 | const pattern = '__val__ km'; 29 | const validate = (value: string) => R.contains('always error', value) ? null : 'bad format'; 30 | const normalize = (value: string) => pattern.replace('__val__', value); 31 | 32 | const event = (value: string) => ({ 33 | preventDefault: R.identity, 34 | stopPropagation: R.identity, 35 | target: { value }, 36 | }); 37 | 38 | const options = { 39 | context: { 40 | reduxForms: 'test', 41 | }, 42 | childContextTypes: { 43 | reduxForms: PropTypes.string, 44 | }, 45 | }; 46 | 47 | // Any to allow nested property dot notation 48 | const newStore = () => createStore(combineReducers({ 49 | reduxForms: reducer, 50 | }), { 51 | reduxForms: { test: form }, 52 | }); 53 | 54 | const getForm = (state: any) => state.getState().reduxForms.test; 55 | 56 | 57 | describe('#field', () => { 58 | it('should not add a field', () => { 59 | const addField = jest.fn(); 60 | const wrapper = shallow(( 61 | 67 | )); 68 | 69 | expect(addField).not.toBeCalled(); 70 | }); 71 | 72 | it('should add a clean field', () => { 73 | const addField = jest.fn(); 74 | const wrapper = shallow(( 75 | 82 | )); 83 | 84 | expect(addField).toBeCalledWith('form', 'test', field); 85 | }); 86 | 87 | it('should add a field with a default value', () => { 88 | const addField = jest.fn(); 89 | const wrapper = shallow(( 90 | 98 | )); 99 | 100 | expect(addField).toBeCalledWith('form', 'test', { 101 | ...field, 102 | value: 'doge', 103 | }); 104 | }); 105 | 106 | it('should add a field with a validator', () => { 107 | const addField = jest.fn(); 108 | const wrapper = shallow(( 109 | 117 | )); 118 | 119 | expect(addField).toBeCalledWith('form', 'test', { 120 | ...field, 121 | error: 'bad format', 122 | }); 123 | }); 124 | 125 | it('should add a field with a normalizer', () => { 126 | const addField = jest.fn(); 127 | const wrapper = shallow(( 128 | 136 | )); 137 | 138 | expect(addField).toBeCalledWith('form', 'test', { 139 | ...field, 140 | value: ' km', 141 | }); 142 | }); 143 | 144 | it('should add a field with a validator and a normalizer', () => { 145 | const addField = jest.fn(); 146 | const wrapper = shallow(( 147 | 156 | )); 157 | 158 | expect(addField).toBeCalledWith('form', 'test', { 159 | ...field, 160 | value: ' km', 161 | error: 'bad format', 162 | }); 163 | }); 164 | 165 | it('should add a field with a default value, a validator and a normalizer', () => { 166 | const addField = jest.fn(); 167 | const wrapper = shallow(( 168 | 178 | )); 179 | 180 | expect(addField).toBeCalledWith('form', 'test', { 181 | ...field, 182 | value: 'asdf km', 183 | error: 'bad format', 184 | }); 185 | }); 186 | 187 | it('should re-mount when no field', () => { 188 | const wrapper = shallow(( 189 | 196 | )); 197 | 198 | const addField = jest.fn(); 199 | 200 | wrapper.setProps({ _addField: addField }); 201 | 202 | expect(addField).toBeCalled(); 203 | }); 204 | 205 | it('should re-mount a clean field', () => { 206 | const wrapper = shallow(( 207 | 214 | )); 215 | 216 | const addField = jest.fn(); 217 | 218 | wrapper.setProps({ _addField: addField, _form: "form2", name: "test2" }); 219 | 220 | expect(addField).toBeCalledWith('form2', 'test2', field); 221 | }); 222 | 223 | it('should re-mount a field with a default value', () => { 224 | const wrapper = shallow(( 225 | 232 | )); 233 | 234 | const addField = jest.fn(); 235 | 236 | wrapper.setProps({ _addField: addField, name: "test2", defaultValue: 'doge' }); 237 | 238 | expect(addField).toBeCalledWith('form', 'test2', { 239 | ...field, 240 | value: 'doge', 241 | }); 242 | }); 243 | 244 | it('should re-mount a field with a validator', () => { 245 | const wrapper = shallow(( 246 | 253 | )); 254 | 255 | const addField = jest.fn(); 256 | 257 | wrapper.setProps({ _addField: addField, name: "test2", validate }); 258 | 259 | expect(addField).toBeCalledWith('form', 'test2', { 260 | ...field, 261 | error: 'bad format', 262 | }); 263 | }); 264 | 265 | it('should re-mount a field with a normalizer', () => { 266 | const wrapper = shallow(( 267 | 274 | )); 275 | 276 | const addField = jest.fn(); 277 | 278 | wrapper.setProps({ _addField: addField, name: "test2", normalize }); 279 | 280 | expect(addField).toBeCalledWith('form', 'test2', { 281 | ...field, 282 | value: ' km', 283 | }); 284 | }); 285 | 286 | it('should re-mount a field with a validator and a normalizer', () => { 287 | const wrapper = shallow(( 288 | 295 | )); 296 | 297 | const addField = jest.fn(); 298 | 299 | wrapper.setProps({ _addField: addField, name: "test2", validate, normalize }); 300 | 301 | expect(addField).toBeCalledWith('form', 'test2', { 302 | ...field, 303 | value: ' km', 304 | error: 'bad format', 305 | }); 306 | }); 307 | 308 | it('should re-mount a field with a default value, a validator and a normalizer', () => { 309 | const wrapper = shallow(( 310 | 317 | )); 318 | 319 | const addField = jest.fn(); 320 | 321 | wrapper.setProps({ 322 | _addField: addField, 323 | name: "test2", 324 | validate, 325 | normalize, 326 | defaultValue: 'asdf', 327 | }); 328 | 329 | expect(addField).toBeCalledWith('form', 'test2', { 330 | ...field, 331 | value: 'asdf km', 332 | error: 'bad format', 333 | }); 334 | }); 335 | 336 | it('should not remove field on unmount', () => { 337 | const removeField = jest.fn(); 338 | const wrapper = shallow(( 339 | 347 | )); 348 | 349 | wrapper.unmount(); 350 | 351 | expect(removeField).not.toBeCalledWith('form', 'test'); 352 | }); 353 | 354 | it('should remove field on unmount', () => { 355 | const removeField = jest.fn(); 356 | const wrapper = shallow(( 357 | 366 | )); 367 | 368 | wrapper.unmount(); 369 | 370 | expect(removeField).toBeCalledWith('form', 'test'); 371 | }); 372 | 373 | it('should change a field when default value changes', () => { 374 | const fieldChange = jest.fn(); 375 | const wrapper = shallow(( 376 | 382 | )); 383 | 384 | wrapper.setProps({ defaultValue: '250' }); 385 | 386 | expect(fieldChange).toBeCalledWith('form', 'test', '', null, true); 387 | }); 388 | 389 | it('should change a field when validator changes', () => { 390 | const fieldChange = jest.fn(); 391 | const wrapper = shallow(( 392 | 398 | )); 399 | 400 | wrapper.setProps({ validate: () => 'error' }); 401 | 402 | expect(fieldChange).toBeCalledWith('form', 'test', '', 'error', false); 403 | }); 404 | 405 | it('should change a field when normalizer changes', () => { 406 | const fieldChange = jest.fn(); 407 | const wrapper = shallow(( 408 | 414 | )); 415 | 416 | wrapper.setProps({ normalize: (str) => str.toUpperCase() }); 417 | 418 | expect(fieldChange).toBeCalledWith('form', 'test', 'KEK', null, true); 419 | }); 420 | 421 | it('should fire a change action', () => { 422 | const fieldChange = jest.fn(); 423 | const wrapper = shallow(( 424 | 430 | )); 431 | 432 | expect(fieldChange).not.toBeCalled(); 433 | 434 | const instance: any = wrapper.instance(); 435 | instance.handleChange(event('doge')); 436 | 437 | expect(fieldChange).toBeCalledWith('form', 'test', 'doge', null, true); 438 | }); 439 | 440 | it('should not fire a change action without a field', () => { 441 | const fieldChange = jest.fn(); 442 | const wrapper = shallow(( 443 | 450 | )); 451 | 452 | expect(fieldChange).not.toBeCalled(); 453 | 454 | const instance: any = wrapper.instance(); 455 | instance.handleChange(event('doge')); 456 | 457 | expect(fieldChange).not.toBeCalled(); 458 | }); 459 | 460 | it('should fire a change action with default value', () => { 461 | const fieldChange = jest.fn(); 462 | const wrapper = shallow(( 463 | 470 | )); 471 | 472 | expect(fieldChange).not.toBeCalled(); 473 | 474 | const instance: any = wrapper.instance(); 475 | instance.handleChange(event('doge')); 476 | 477 | expect(fieldChange).toBeCalledWith('form', 'test', 'doge', null, false); 478 | }); 479 | 480 | it('should fire a validated change action', () => { 481 | const fieldChange = jest.fn(); 482 | const wrapper = shallow(( 483 | 490 | )); 491 | 492 | expect(fieldChange).not.toBeCalled(); 493 | 494 | const instance: any = wrapper.instance(); 495 | instance.handleChange(event('doge')); 496 | 497 | expect(fieldChange).toBeCalledWith('form', 'test', 'doge', 'bad format', true); 498 | }); 499 | 500 | it('should fire a normalized change action', () => { 501 | const fieldChange = jest.fn(); 502 | const wrapper = shallow(( 503 | 510 | )); 511 | 512 | expect(fieldChange).not.toBeCalled(); 513 | 514 | const instance: any = wrapper.instance(); 515 | instance.handleChange(event('doge')); 516 | 517 | expect(fieldChange).toBeCalledWith('form', 'test', 'doge km', null, true); 518 | }); 519 | 520 | it('should fire a validated and normalized change action with default value', () => { 521 | const fieldChange = jest.fn(); 522 | const wrapper = shallow(( 523 | 532 | )); 533 | 534 | expect(fieldChange).not.toBeCalled(); 535 | 536 | const instance: any = wrapper.instance(); 537 | instance.handleChange(event('doge')); 538 | 539 | expect(fieldChange).toBeCalledWith('form', 'test', 'doge km', 'bad format', false); 540 | }); 541 | 542 | it('should fire a focus action', () => { 543 | const fieldFocus = jest.fn(); 544 | const wrapper = shallow(( 545 | 551 | )); 552 | 553 | expect(fieldFocus).not.toBeCalled(); 554 | 555 | const instance: any = wrapper.instance(); 556 | instance.handleFocus(); 557 | 558 | expect(fieldFocus).toBeCalledWith('form', 'test'); 559 | }); 560 | 561 | it('should fire a blur action', () => { 562 | const fieldBlur = jest.fn(); 563 | const wrapper = shallow(( 564 | 570 | )); 571 | 572 | expect(fieldBlur).not.toBeCalled(); 573 | 574 | const instance: any = wrapper.instance(); 575 | instance.handleBlur(event('doge')); 576 | 577 | expect(fieldBlur).toBeCalledWith('form', 'test', 'doge', null, true); 578 | }); 579 | 580 | it('should fire a blur action with default value', () => { 581 | const fieldBlur = jest.fn(); 582 | const wrapper = shallow(( 583 | 590 | )); 591 | 592 | expect(fieldBlur).not.toBeCalled(); 593 | 594 | const instance: any = wrapper.instance(); 595 | instance.handleBlur(event('doge')); 596 | 597 | expect(fieldBlur).toBeCalledWith('form', 'test', 'doge', null, false); 598 | }); 599 | 600 | it('should fire a validated blur action', () => { 601 | const fieldBlur = jest.fn(); 602 | const wrapper = shallow(( 603 | 610 | )); 611 | 612 | expect(fieldBlur).not.toBeCalled(); 613 | 614 | const instance: any = wrapper.instance(); 615 | instance.handleBlur(event('doge')); 616 | 617 | expect(fieldBlur).toBeCalledWith('form', 'test', 'doge', 'bad format', true); 618 | }); 619 | 620 | it('should fire a normalized blur action', () => { 621 | const fieldBlur = jest.fn(); 622 | const wrapper = shallow(( 623 | 630 | )); 631 | 632 | expect(fieldBlur).not.toBeCalled(); 633 | 634 | const instance: any = wrapper.instance(); 635 | instance.handleBlur(event('doge')); 636 | 637 | expect(fieldBlur).toBeCalledWith('form', 'test', 'doge km', null, true); 638 | }); 639 | 640 | it('should fire a validated and normalized blur action with default value', () => { 641 | const fieldBlur = jest.fn(); 642 | const wrapper = shallow(( 643 | 652 | )); 653 | 654 | expect(fieldBlur).not.toBeCalled(); 655 | 656 | const instance: any = wrapper.instance(); 657 | instance.handleBlur(event('doge')); 658 | 659 | expect(fieldBlur).toBeCalledWith('form', 'test', 'doge km', 'bad format', false); 660 | }); 661 | 662 | it('should not fire a blur action without a field', () => { 663 | const fieldBlur = jest.fn(); 664 | const wrapper = shallow(( 665 | 672 | )); 673 | 674 | expect(fieldBlur).not.toBeCalled(); 675 | 676 | const instance: any = wrapper.instance(); 677 | instance.handleBlur(event('doge')); 678 | 679 | expect(fieldBlur).not.toBeCalled(); 680 | }); 681 | 682 | it('should not render without a field', () => { 683 | const wrapper = shallow(( 684 | 690 | )); 691 | 692 | expect(wrapper.isEmptyRender()).toBe(true); 693 | }); 694 | 695 | it('should pass props', () => { 696 | const wrapper = shallow(( 697 | 704 | )); 705 | 706 | expect(wrapper.prop('input').value).toBe(''); 707 | expect(wrapper.prop('input').onChange).toBeDefined(); 708 | expect(wrapper.prop('input').onFocus).toBeDefined(); 709 | expect(wrapper.prop('input').onBlur).toBeDefined(); 710 | 711 | expect(wrapper.prop('meta')).toEqual({ 712 | active: false, 713 | dirty: false, 714 | error: null, 715 | touched: false, 716 | visited: false, 717 | }); 718 | 719 | expect(wrapper.prop('kek')).toBe('bur'); 720 | }); 721 | 722 | it('should overwrite props', () => { 723 | const wrapper = shallow(( 724 | 731 | )); 732 | 733 | expect(wrapper.prop('input').value).toBe('lmao'); 734 | }); 735 | 736 | it('should not mount without context', () => { 737 | const store = newStore(); 738 | const wrapperFn = () => mount(( 739 | 740 | 741 | 742 | )); 743 | 744 | expect(wrapperFn).toThrowError(/Form/); 745 | }); 746 | 747 | it('should have a correct name', () => { 748 | const Component2: any = (props: any) => ( 749 | 756 | ); 757 | 758 | Component2.displayName = 'Input'; 759 | 760 | const ConnectedField2 = fieldDecorator(Component2); 761 | 762 | const store = newStore(); 763 | const wrapper = mount(( 764 | 765 | 766 | 767 | ), options); 768 | 769 | expect(wrapper.find(ConnectedField2).name()).toBe('field(Input)'); 770 | }); 771 | 772 | it('should have a default name', () => { 773 | const store = newStore(); 774 | const wrapper = mount(( 775 | 776 | 777 | 778 | ), options); 779 | 780 | expect(wrapper.find(ConnectedField).name()).toBe('field(Component)'); 781 | }); 782 | 783 | it('should rerender on form add', () => { 784 | const store = newStore(); 785 | const wrapper = mount(( 786 | 787 | 788 | 789 | )); 790 | 791 | expect(wrapper.find(Field).isEmptyRender()).toBe(true); 792 | 793 | store.dispatch(actions.addForm('nope')); 794 | wrapper.update(); 795 | 796 | expect(wrapper.find(Field).isEmptyRender()).toBe(false); 797 | }); 798 | 799 | it('should add a field', () => { 800 | const store = newStore(); 801 | const wrapper = mount(( 802 | 803 | 804 | 805 | ), options); 806 | 807 | expect(getForm(store).fields).toEqual({ test: field }); 808 | expect(wrapper.find('input').length).toBe(1); 809 | }); 810 | 811 | it('should handle a change event', () => { 812 | const store = newStore(); 813 | const wrapper = mount(( 814 | 815 | 816 | 817 | ), options); 818 | 819 | wrapper.find('input').simulate('change', event('doge')); 820 | 821 | expect(getForm(store).fields).toEqual({ test: { 822 | ...field, 823 | value: 'doge', 824 | dirty: true, 825 | } }); 826 | }); 827 | 828 | it('should handle a focus event', () => { 829 | const store = newStore(); 830 | const wrapper = mount(( 831 | 832 | 833 | 834 | ), options); 835 | 836 | wrapper.find('input').simulate('focus'); 837 | 838 | expect(getForm(store).fields).toEqual({ test: { 839 | ...field, 840 | visited: true, 841 | active: true, 842 | } }); 843 | }); 844 | 845 | it('should handle a blur event', () => { 846 | const store = newStore(); 847 | const wrapper = mount(( 848 | 849 | 850 | 851 | ), options); 852 | 853 | wrapper.find('input').simulate('blur', event('doge')); 854 | 855 | expect(getForm(store).fields).toEqual({ test: { 856 | ...field, 857 | value: 'doge', 858 | touched: true, 859 | dirty: true, 860 | } }); 861 | }); 862 | }); 863 | -------------------------------------------------------------------------------- /packages/redux-forms-react/src/__tests__/fieldArray.spec.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import * as React from 'react'; 3 | import * as PropTypes from 'prop-types'; 4 | import { createStore, combineReducers } from 'redux'; 5 | import { Provider } from 'react-redux'; 6 | import { shallow, mount } from 'enzyme'; 7 | import * as R from 'ramda'; 8 | 9 | import reducer from 'redux-forms/lib/index'; 10 | import { form, field } from 'redux-forms/lib/containers'; 11 | import * as actions from 'redux-forms/actions'; 12 | import fieldArray from '../fieldArray'; 13 | 14 | const Component = (props: any) => ( 15 |
16 | ); 17 | 18 | const ConnectedFieldArray = fieldArray(Component); 19 | const FieldArray = (ConnectedFieldArray as any).WrappedComponent.WrappedComponent; 20 | 21 | const options = { 22 | context: { 23 | reduxForms: 'test', 24 | }, 25 | childContextTypes: { 26 | reduxForms: PropTypes.string, 27 | }, 28 | }; 29 | 30 | const event = { target: { value: 'doge' } }; 31 | 32 | // Any to allow nested property dot notation 33 | const newStore = () => createStore(combineReducers({ 34 | reduxForms: reducer, 35 | }), { 36 | reduxForms: { test: form }, 37 | }); 38 | 39 | const getForm = (state: any) => state.getState().reduxForms.test; 40 | 41 | 42 | describe('#fieldArray', () => { 43 | it('should not add an array', () => { 44 | const addArray = jest.fn(); 45 | const wrapper = mount(( 46 | 51 | )); 52 | 53 | expect(addArray).not.toBeCalled(); 54 | }); 55 | 56 | it('should add an array', () => { 57 | const addArray = jest.fn(); 58 | const wrapper = mount(( 59 | 65 | )); 66 | 67 | expect(addArray).toBeCalledWith('form', 'array'); 68 | }); 69 | 70 | it('should provide array length', () => { 71 | const wrapper = shallow(( 72 | 79 | )); 80 | 81 | expect(wrapper.prop('fields').length).toBe(1); 82 | }); 83 | 84 | it('should handle map', () => { 85 | const wrapper = shallow(( 86 | 91 | )); 92 | 93 | expect(wrapper.prop('fields').map(R.identity)).toEqual(['array.0', 'array.1']); 94 | }); 95 | 96 | it('should handle map without array', () => { 97 | const wrapper = shallow(( 98 | 103 | )); 104 | 105 | expect(wrapper.instance().handleMap(R.identity)).toEqual([]); 106 | }); 107 | 108 | it('should handle map of indexes', () => { 109 | const wrapper = shallow(( 110 | 115 | )); 116 | 117 | expect(wrapper.prop('fields').map((_: any, i: number) => i)).toEqual([0, 1]); 118 | }); 119 | 120 | it('should handle push', () => { 121 | const push = jest.fn(); 122 | const wrapper = shallow(( 123 | 130 | )); 131 | 132 | wrapper.prop('fields').push(); 133 | 134 | expect(push).toBeCalledWith('form', 'array'); 135 | }); 136 | 137 | it('should not handle pop', () => { 138 | const pop = jest.fn(); 139 | const wrapper = shallow(( 140 | 147 | )); 148 | 149 | wrapper.prop('fields').pop(); 150 | 151 | expect(pop).not.toBeCalled(); 152 | }); 153 | 154 | it('should handle pop', () => { 155 | const pop = jest.fn(); 156 | const wrapper = shallow(( 157 | 164 | )); 165 | 166 | wrapper.prop('fields').pop(); 167 | 168 | expect(pop).toBeCalledWith('form', 'array'); 169 | }); 170 | 171 | it('should handle unshift', () => { 172 | const unshift = jest.fn(); 173 | const wrapper = shallow(( 174 | 181 | )); 182 | 183 | wrapper.prop('fields').unshift(); 184 | 185 | expect(unshift).toBeCalledWith('form', 'array'); 186 | }); 187 | 188 | it('should not handle shift', () => { 189 | const shift = jest.fn(); 190 | const wrapper = shallow(( 191 | 198 | )); 199 | 200 | wrapper.prop('fields').shift(); 201 | 202 | expect(shift).not.toBeCalled(); 203 | }); 204 | 205 | it('should handle shift', () => { 206 | const shift = jest.fn(); 207 | const wrapper = shallow(( 208 | 215 | )); 216 | 217 | wrapper.prop('fields').shift(); 218 | 219 | expect(shift).toBeCalledWith('form', 'array'); 220 | }); 221 | 222 | it('should handle insert', () => { 223 | const insert = jest.fn(); 224 | const wrapper = shallow(( 225 | 232 | )); 233 | 234 | wrapper.prop('fields').insert(1); 235 | 236 | expect(insert).toBeCalledWith('form', 'array', 1); 237 | }); 238 | 239 | it('should handle remove', () => { 240 | const remove = jest.fn(); 241 | const wrapper = shallow(( 242 | 249 | )); 250 | 251 | wrapper.prop('fields').remove(1); 252 | 253 | expect(remove).toBeCalledWith('form', 'array', 1); 254 | }); 255 | 256 | it('should handle swap', () => { 257 | const swap = jest.fn(); 258 | const wrapper = shallow(( 259 | 266 | )); 267 | 268 | wrapper.prop('fields').swap(0, 1); 269 | 270 | expect(swap).toBeCalledWith('form', 'array', 0, 1); 271 | }); 272 | 273 | it('should handle move', () => { 274 | const move = jest.fn(); 275 | const wrapper = shallow(( 276 | 283 | )); 284 | 285 | wrapper.prop('fields').move(0, 1); 286 | 287 | expect(move).toBeCalledWith('form', 'array', 0, 1); 288 | }); 289 | 290 | it('should not render without an array', () => { 291 | const wrapper = mount(( 292 | 296 | )); 297 | 298 | expect(wrapper.isEmptyRender()).toBe(true); 299 | }); 300 | 301 | it('should render a component', () => { 302 | const wrapper = mount(( 303 | 308 | )); 309 | 310 | expect(wrapper.find('.Component').length).toBe(1); 311 | }); 312 | 313 | it('should not mount without context', () => { 314 | const store = newStore(); 315 | const wrapperFn = () => mount(( 316 | 317 | 318 | 319 | )); 320 | 321 | expect(wrapperFn).toThrowError(/Form/); 322 | }); 323 | 324 | it('should have a correct name', () => { 325 | const Component2: any = (props: any) => ( 326 |
327 | ); 328 | 329 | Component2.displayName = 'Array'; 330 | 331 | const ConnectedFieldArray2 = fieldArray(Component2); 332 | 333 | const store = newStore(); 334 | const wrapper = mount(( 335 | 336 | 337 | 338 | ), 339 | options, 340 | ); 341 | 342 | expect(wrapper.find(ConnectedFieldArray2).name()).toBe('fieldArray(Array)'); 343 | }); 344 | 345 | it('should rerender on form add', () => { 346 | const store = newStore(); 347 | const wrapper = mount(( 348 | 349 | 350 | 351 | )); 352 | 353 | expect(wrapper.find(FieldArray).isEmptyRender()).toBe(true); 354 | 355 | store.dispatch(actions.addForm('nope')); 356 | wrapper.update(); 357 | 358 | expect(wrapper.find(FieldArray).isEmptyRender()).toBe(false); 359 | }); 360 | 361 | it('should have a correct default name', () => { 362 | const store = newStore(); 363 | const wrapper = mount(( 364 | 365 | 366 | 367 | ), 368 | options, 369 | ); 370 | 371 | expect(wrapper.find(ConnectedFieldArray).name()).toBe('fieldArray(Component)'); 372 | }); 373 | 374 | it('should actually add an array', () => { 375 | const store = newStore(); 376 | const wrapper = mount(( 377 | 378 | 379 | 380 | ), 381 | options, 382 | ); 383 | 384 | expect(getForm(store).arrays).toEqual({ test: 0 }); 385 | }); 386 | 387 | it('should push a field', () => { 388 | const store = newStore(); 389 | const wrapper = mount(( 390 | 391 | 392 | 393 | ), 394 | options, 395 | ).find(Component); 396 | 397 | wrapper.prop('fields').push(); 398 | 399 | expect(getForm(store).arrays).toEqual({ test: 1 }); 400 | }); 401 | 402 | it('should pop a field', () => { 403 | const store = newStore(); 404 | const wrapper = mount(( 405 | 406 | 407 | 408 | ), 409 | options, 410 | ).find(Component); 411 | 412 | wrapper.prop('fields').push(); 413 | wrapper.prop('fields').pop(); 414 | 415 | expect(getForm(store).arrays).toEqual({ test: 0 }); 416 | }); 417 | 418 | it('should unshift a field', () => { 419 | const store = newStore(); 420 | const wrapper = mount(( 421 | 422 | 423 | 424 | ), 425 | options, 426 | ).find(Component); 427 | 428 | wrapper.prop('fields').unshift(); 429 | 430 | expect(getForm(store).arrays).toEqual({ test: 1 }); 431 | }); 432 | 433 | it('should shift a field', () => { 434 | const store = newStore(); 435 | const wrapper = mount(( 436 | 437 | 438 | 439 | ), 440 | options, 441 | ).find(Component); 442 | 443 | wrapper.prop('fields').unshift(); 444 | wrapper.prop('fields').shift(); 445 | 446 | expect(getForm(store).arrays).toEqual({ test: 0 }); 447 | }); 448 | 449 | it('should insert a field', () => { 450 | const store = newStore(); 451 | const wrapper = mount(( 452 | 453 | 454 | 455 | ), 456 | options, 457 | ).find(Component); 458 | 459 | wrapper.prop('fields').insert(0); 460 | 461 | expect(getForm(store).arrays).toEqual({ test: 1 }); 462 | }); 463 | 464 | it('should remove a field', () => { 465 | const store = newStore(); 466 | const wrapper = mount(( 467 | 468 | 469 | 470 | ), 471 | options, 472 | ).find(Component); 473 | 474 | wrapper.prop('fields').insert(0); 475 | wrapper.prop('fields').remove(0); 476 | 477 | expect(getForm(store).arrays).toEqual({ test: 0 }); 478 | }); 479 | 480 | it('should map fields', () => { 481 | const store = newStore(); 482 | const wrapper = mount(( 483 | 484 | 485 | 486 | ), 487 | options, 488 | ).find(Component); 489 | 490 | wrapper.prop('fields').push(); 491 | wrapper.prop('fields').push(); 492 | 493 | expect(wrapper.prop('fields').map(R.identity)).toEqual(['test.0', 'test.1']); 494 | }); 495 | }); 496 | -------------------------------------------------------------------------------- /packages/redux-forms-react/src/connectField.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as PropTypes from 'prop-types'; 3 | import { 4 | merge, 5 | } from 'ramda'; 6 | 7 | import { invariant, isString } from 'redux-forms/lib/shared/helpers'; 8 | import { Context } from './Form'; 9 | 10 | 11 | export type SuppliedProps = { 12 | _form: string, 13 | }; 14 | 15 | export type InputProps = { 16 | form?: string, 17 | }; 18 | 19 | export type WrappedField = React.ComponentClass; 20 | 21 | export type Connected = React.SFC & { 22 | WrappedComponent?: React.ComponentClass, 23 | }; 24 | 25 | 26 | export default function connectField( 27 | Wrapped: React.ComponentClass, 28 | ): Connected { 29 | const ConnectedField: Connected = (props: T & InputProps, { reduxForms }: Context) => { 30 | const contextForm = isString(reduxForms) ? reduxForms : null; 31 | const form = isString(props.form) ? props.form : contextForm; 32 | invariant( 33 | isString(form), 34 | '[redux-forms] "field(...)" and "fieldArray(...)" must be a child of the Form ' + 35 | 'component or an explicit "form" prop must be supplied.', 36 | ); 37 | 38 | return React.createElement(Wrapped, merge(props, { 39 | _form: (form as string), 40 | })); 41 | }; 42 | 43 | 44 | ConnectedField.contextTypes = { 45 | reduxForms: PropTypes.string, 46 | }; 47 | 48 | ConnectedField.displayName = Wrapped.displayName; 49 | 50 | ConnectedField.WrappedComponent = Wrapped; 51 | 52 | return ConnectedField; 53 | } 54 | -------------------------------------------------------------------------------- /packages/redux-forms-react/src/field.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { 5 | identity, 6 | not, 7 | compose, 8 | set, 9 | lensProp, 10 | merge, 11 | path, 12 | prop, 13 | } from 'ramda'; 14 | 15 | import { IReduxFormsState } from 'redux-forms/lib/index'; 16 | import * as containers from 'redux-forms/lib/containers'; 17 | import fieldProps, { boolField, InputProps, MetaProps } from 'redux-forms/lib/shared/fieldProps'; 18 | import getValue, { Target } from 'redux-forms/lib/shared/getValue'; 19 | import { shallowCompare } from 'redux-forms/lib/shared/helpers'; 20 | import * as actions from 'redux-forms/actions'; 21 | import connectField, { SuppliedProps } from './connectField'; 22 | 23 | 24 | export type SuppliedProps = { 25 | input: InputProps, 26 | meta: MetaProps, 27 | }; 28 | 29 | export type Validate = (value: any) => string | null; 30 | export type Normalize = (value: any) => any; 31 | 32 | export type FieldProps = { 33 | name: string, 34 | normalize?: Normalize, 35 | defaultValue?: any, 36 | validate?: Validate, 37 | cleanup?: boolean, 38 | }; 39 | 40 | type ConnectedProps = FieldProps & SuppliedProps; 41 | 42 | type StateProps = { 43 | _hasForm: boolean, 44 | _field: containers.Field | null, 45 | }; 46 | 47 | type ActionProps = { 48 | _addField: typeof actions.addField, 49 | _removeField: typeof actions.removeField, 50 | _fieldChange: typeof actions.fieldChange, 51 | _fieldFocus: typeof actions.fieldFocus, 52 | _fieldBlur: typeof actions.fieldBlur, 53 | }; 54 | 55 | type Props = T & ConnectedProps & StateProps & ActionProps; 56 | 57 | 58 | function field(Component: React.ComponentType): React.ComponentType { 59 | class Field extends React.Component { 60 | static defaultProps = { 61 | normalize: identity, 62 | defaultValue: '', 63 | cleanup: false, 64 | }; 65 | 66 | static propTypes = { 67 | name: PropTypes.string.isRequired, 68 | normalize: PropTypes.func.isRequired, 69 | defaultValue: PropTypes.any.isRequired, 70 | validate: PropTypes.func, 71 | }; 72 | 73 | props: Props; 74 | 75 | constructor(props: Props) { 76 | super(props); 77 | 78 | this.handleChange = this.handleChange.bind(this); 79 | this.handleFocus = this.handleFocus.bind(this); 80 | this.handleBlur = this.handleBlur.bind(this); 81 | } 82 | 83 | shouldComponentUpdate(nextProps: Props) { 84 | const { _field } = this.props; 85 | 86 | if (!shallowCompare(boolField(this.props), boolField(nextProps))) { 87 | return true; 88 | } 89 | 90 | return not(_field && nextProps._field && shallowCompare(_field, nextProps._field)); 91 | } 92 | 93 | componentWillMount() { 94 | const { _hasForm, _field } = this.props; 95 | 96 | if (_hasForm && !_field) { 97 | this.addField(this.props); 98 | } 99 | } 100 | 101 | componentWillUnmount() { 102 | const { _form, name, cleanup, _removeField } = this.props; 103 | 104 | if (cleanup) { 105 | _removeField(_form, name); 106 | } 107 | } 108 | 109 | componentWillReceiveProps(next: Props) { 110 | const { defaultValue, validate, normalize } = this.props; 111 | 112 | if (next._hasForm && !next._field) { 113 | this.addField(next); 114 | return; 115 | } 116 | 117 | if (defaultValue !== next.defaultValue) { 118 | this.updateField(next); 119 | return; 120 | } 121 | 122 | if (validate !== next.validate) { 123 | this.updateField(next); 124 | return; 125 | } 126 | 127 | if (normalize !== next.normalize) { 128 | this.updateField(next); 129 | return; 130 | } 131 | } 132 | 133 | addField(props: Props) { 134 | const value = (props.normalize as Normalize)(props.defaultValue); 135 | const newField = compose( 136 | set(lensProp('value'), value), 137 | set(lensProp('error'), props.validate ? props.validate(value) : null), 138 | )(containers.field); 139 | 140 | props._addField(props._form, props.name, newField); 141 | } 142 | 143 | updateField(props: Props) { 144 | if (props._field) { 145 | const value = (props.normalize as Normalize)(props._field.value); 146 | const error = props.validate ? props.validate(value) : props._field.error; 147 | const dirty = props.defaultValue !== value; 148 | 149 | props._fieldChange(props._form, props.name, value, error, dirty); 150 | } 151 | } 152 | 153 | handleChange(ev: React.SyntheticEvent | any) { 154 | const { _fieldChange, _form, _field, name, normalize, validate, defaultValue } = this.props; 155 | 156 | if (!_field) { 157 | return; 158 | } 159 | 160 | const value = (normalize as Normalize)(getValue(ev)); 161 | const error = validate ? validate(value) : _field.error; 162 | const dirty = value !== defaultValue; 163 | 164 | _fieldChange(_form, name, value, error, dirty); 165 | } 166 | 167 | handleFocus() { 168 | const { _fieldFocus, _form, name } = this.props; 169 | 170 | _fieldFocus(_form, name); 171 | } 172 | 173 | handleBlur(ev: React.SyntheticEvent | any) { 174 | const { _fieldBlur, _form, _field, name, normalize, validate, defaultValue } = this.props; 175 | 176 | if (!_field) { 177 | return; 178 | } 179 | 180 | const value = (normalize as Normalize)(getValue(ev)); 181 | const error = validate ? validate(value) : _field.error; 182 | const dirty = value !== defaultValue; 183 | 184 | _fieldBlur(_form, name, value, error, dirty); 185 | } 186 | 187 | render() { 188 | const { name, _field } = this.props; 189 | 190 | // Wait until field is initialized 191 | if (!_field) { 192 | return null; 193 | } 194 | 195 | const inputProps = merge(_field, { 196 | name, 197 | onChange: this.handleChange, 198 | onFocus: this.handleFocus, 199 | onBlur: this.handleBlur, 200 | }); 201 | 202 | const { input, meta, rest } = fieldProps(merge(inputProps, this.props)); 203 | 204 | // TODO SFC not compatibile with class... wtf TS 205 | return React.createElement(Component as any, merge(rest, { 206 | input, 207 | meta, 208 | })); 209 | } 210 | } 211 | 212 | const connector = connect( 213 | (state: IReduxFormsState, props: ConnectedProps & T) => ({ 214 | _hasForm: Boolean(prop(props._form, state.reduxForms)), 215 | _field: path([props._form, 'fields', props.name], state.reduxForms), 216 | }), 217 | { 218 | _addField: actions.addField, 219 | _removeField: actions.removeField, 220 | _fieldChange: actions.fieldChange, 221 | _fieldFocus: actions.fieldFocus, 222 | _fieldBlur: actions.fieldBlur, 223 | }, 224 | ); 225 | 226 | // TODO SFC not compatibile with class... wtf TS 227 | const Connected = connector(Field as any); 228 | 229 | const Contexted = connectField(Connected); 230 | 231 | Contexted.displayName = `field(${Component.displayName || 'Component'})`; 232 | 233 | return Contexted; 234 | } 235 | 236 | export default field; 237 | -------------------------------------------------------------------------------- /packages/redux-forms-react/src/fieldArray.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { 5 | addIndex, 6 | map, 7 | path, 8 | repeat, 9 | merge, 10 | prop, 11 | } from 'ramda'; 12 | 13 | import { IReduxFormsState } from 'redux-forms/lib/index'; 14 | import fieldArrayProps from 'redux-forms/lib/shared/fieldArrayProps'; 15 | import { Target } from 'redux-forms/lib/shared/getValue'; 16 | import { isNumber, isEvent } from "redux-forms/lib/shared/helpers"; 17 | import * as actions from 'redux-forms/actions'; 18 | import { Context } from './Form'; 19 | import connectField, { SuppliedProps } from './connectField'; 20 | 21 | 22 | export type SuppliedProps = { 23 | fields: { 24 | length: number, 25 | map: (fn: (el: string, index: number) => T) => T[], 26 | push: () => void, 27 | pop: () => void, 28 | unshift: () => void, 29 | shift: () => void, 30 | insert: (index: number) => void, 31 | remove: (index: number) => void, 32 | swap: (index1: number, index2: number) => void, 33 | move: (from: number, to: number) => void, 34 | }, 35 | }; 36 | 37 | export type FieldArrayProps = { 38 | name: string, 39 | }; 40 | 41 | type ConnectedProps = FieldArrayProps & SuppliedProps; 42 | 43 | type StateProps = { 44 | _hasForm: boolean, 45 | _array?: number, 46 | }; 47 | 48 | type ActionProps = { 49 | _addArray: typeof actions.addArray, 50 | _arrayPush: typeof actions.arrayPush, 51 | _arrayPop: typeof actions.arrayPop, 52 | _arrayUnshift: typeof actions.arrayUnshift, 53 | _arrayShift: typeof actions.arrayShift, 54 | _arrayInsert: typeof actions.arrayInsert, 55 | _arrayRemove: typeof actions.arrayRemove, 56 | _arraySwap: typeof actions.arraySwap, 57 | _arrayMove: typeof actions.arrayMove, 58 | }; 59 | 60 | type Props = T & StateProps & ActionProps & ConnectedProps; 61 | 62 | 63 | const RindexMap = addIndex(map); 64 | 65 | function fieldArray(Component: React.ComponentType): React.ComponentType { 66 | class FieldArray extends React.PureComponent { 67 | static propTypes = { 68 | name: PropTypes.string.isRequired, 69 | }; 70 | 71 | props: Props; 72 | 73 | constructor(props: Props) { 74 | super(props); 75 | 76 | this.handleMap = this.handleMap.bind(this); 77 | this.handlePush = this.handlePush.bind(this); 78 | this.handlePop = this.handlePop.bind(this); 79 | this.handleUnshift = this.handleUnshift.bind(this); 80 | this.handleShift = this.handleShift.bind(this); 81 | this.handleInsert = this.handleInsert.bind(this); 82 | this.handleRemove = this.handleRemove.bind(this); 83 | this.handleSwap = this.handleSwap.bind(this); 84 | this.handleMove = this.handleMove.bind(this); 85 | } 86 | 87 | componentWillMount() { 88 | this.maybeAddArray(this.props); 89 | } 90 | 91 | componentWillReceiveProps(nextProps: Props) { 92 | this.maybeAddArray(nextProps); 93 | } 94 | 95 | maybeAddArray(props: Props) { 96 | const { _form, _hasForm, name, _array, _addArray } = props; 97 | 98 | if (_hasForm && !isNumber(_array)) { 99 | _addArray(_form, name); 100 | } 101 | } 102 | 103 | handleMap(fn: (el: string, index: number) => U): U[] { 104 | const { name, _array } = this.props; 105 | 106 | if (!isNumber(_array)) { 107 | return []; 108 | } 109 | 110 | const array = repeat(null, _array); 111 | return RindexMap(fn, RindexMap((_, i) => `${name}.${i}`, array)); 112 | } 113 | 114 | handlePush() { 115 | const { _form, name, _arrayPush } = this.props; 116 | 117 | _arrayPush(_form, name); 118 | } 119 | 120 | handlePop() { 121 | const { _form, name, _array, _arrayPop } = this.props; 122 | 123 | if (isNumber(_array) && _array > 0) { 124 | _arrayPop(_form, name); 125 | } 126 | } 127 | 128 | handleUnshift() { 129 | const { _form, name, _arrayUnshift } = this.props; 130 | 131 | _arrayUnshift(_form, name); 132 | } 133 | 134 | handleShift() { 135 | const { _form, name, _array, _arrayShift } = this.props; 136 | 137 | if (isNumber(_array) && _array > 0) { 138 | _arrayShift(_form, name); 139 | } 140 | } 141 | 142 | handleInsert(index: number) { 143 | const { _form, name, _arrayInsert } = this.props; 144 | 145 | _arrayInsert(_form, name, index); 146 | } 147 | 148 | handleRemove(index: number) { 149 | const { _form, name, _arrayRemove } = this.props; 150 | 151 | _arrayRemove(_form, name, index); 152 | } 153 | 154 | handleSwap(index1: number, index2: number) { 155 | const { _form, name, _arraySwap } = this.props; 156 | 157 | _arraySwap(_form, name, index1, index2); 158 | } 159 | 160 | handleMove(from: number, to: number) { 161 | const { _form, name, _arrayMove } = this.props; 162 | 163 | _arrayMove(_form, name, from, to); 164 | } 165 | 166 | render() { 167 | const { _array } = this.props; 168 | 169 | if (!isNumber(_array)) { 170 | return null; 171 | } 172 | 173 | // TODO SFC not compatibile with class... wtf TS 174 | return React.createElement(Component as any, merge(fieldArrayProps(this.props), { 175 | fields: { 176 | length: _array, 177 | map: this.handleMap, 178 | push: this.handlePush, 179 | pop: this.handlePop, 180 | unshift: this.handleUnshift, 181 | shift: this.handleShift, 182 | insert: this.handleInsert, 183 | remove: this.handleRemove, 184 | swap: this.handleSwap, 185 | move: this.handleMove, 186 | }, 187 | })); 188 | } 189 | } 190 | 191 | 192 | const connector = connect( 193 | (state: IReduxFormsState, props: ConnectedProps & T) => ({ 194 | _hasForm: Boolean(prop(props._form, state.reduxForms)), 195 | _array: path([props._form, 'arrays', props.name], state.reduxForms), 196 | }), 197 | { 198 | _addArray: actions.addArray, 199 | _arrayPush: actions.arrayPush, 200 | _arrayPop: actions.arrayPop, 201 | _arrayUnshift: actions.arrayUnshift, 202 | _arrayShift: actions.arrayShift, 203 | _arrayInsert: actions.arrayInsert, 204 | _arrayRemove: actions.arrayRemove, 205 | _arraySwap: actions.arraySwap, 206 | _arrayMove: actions.arrayMove, 207 | }, 208 | ); 209 | 210 | // TODO SFC not compatibile with class... wtf TS 211 | const Connected = connector(FieldArray as any); 212 | 213 | const Contexted = connectField(Connected); 214 | 215 | Contexted.displayName = `fieldArray(${Component.displayName || 'Component'})`; 216 | 217 | return Contexted; 218 | } 219 | 220 | export default fieldArray; 221 | -------------------------------------------------------------------------------- /packages/redux-forms-react/src/index.ts: -------------------------------------------------------------------------------- 1 | import Form from './Form'; 2 | import field from './field'; 3 | import fieldArray from './fieldArray'; 4 | 5 | 6 | export { 7 | Form, 8 | field, 9 | fieldArray, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/redux-forms/README.md: -------------------------------------------------------------------------------- 1 | # redux-forms 2 | 3 | [![npm](https://img.shields.io/npm/v/redux-forms.svg)](https://www.npmjs.com/package/redux-forms) 4 | 5 | The core `redux-forms` package. 6 | 7 | Contains: 8 | * **reducer** 9 | * **actions** 10 | * **selectors** 11 | * things other packages depend on 12 | 13 | Check out the [docs](https://oreqizer.gitbooks.io/redux-forms/content) for details. 14 | -------------------------------------------------------------------------------- /packages/redux-forms/actions.d.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/actions'; 2 | -------------------------------------------------------------------------------- /packages/redux-forms/actions.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/actions'); 2 | -------------------------------------------------------------------------------- /packages/redux-forms/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-forms", 3 | "version": "1.0.0-3", 4 | "description": "A simple form management for Redux.", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "repository": "https://github.com/oreqizer/redux-forms/tree/master/packages/redux-forms", 8 | "keywords": [ 9 | "form", 10 | "forms", 11 | "redux" 12 | ], 13 | "author": "oreqizer", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/oreqizer/redux-forms/issues" 17 | }, 18 | "dependencies": { 19 | "ramda": "^0.25.0" 20 | }, 21 | "files": [ 22 | "dist", 23 | "lib", 24 | "actions.js", 25 | "actions.d.ts", 26 | "selectors.d.ts", 27 | "selectors.js", 28 | "README.md" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /packages/redux-forms/selectors.d.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/selectors'; 2 | -------------------------------------------------------------------------------- /packages/redux-forms/selectors.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/selectors'); 2 | -------------------------------------------------------------------------------- /packages/redux-forms/src/__tests__/actions.spec.ts: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | import { field } from "../containers"; 4 | 5 | 6 | describe('#actions', () => { 7 | it('should create an ADD_FORM action', () => { 8 | expect(actions.addForm('form')).toEqual({ 9 | type: actions.ADD_FORM, 10 | payload: { name: 'form' }, 11 | }); 12 | }); 13 | 14 | it('should create a REMOVE_FORM action', () => { 15 | expect(actions.removeForm('form')).toEqual({ 16 | type: actions.REMOVE_FORM, 17 | payload: { name: 'form' }, 18 | }); 19 | }); 20 | 21 | it('should create an ADD_FIELD action', () => { 22 | expect(actions.addField('form', 'field', field)).toEqual({ 23 | type: actions.ADD_FIELD, 24 | payload: { form: 'form', id: 'field', field }, 25 | }); 26 | }); 27 | 28 | it('should create an TOUCH_ALL action', () => { 29 | expect(actions.touchAll('form')).toEqual({ 30 | type: actions.TOUCH_ALL, 31 | payload: { form: 'form' }, 32 | }); 33 | }); 34 | 35 | it('should create an SUBMIT_START action', () => { 36 | expect(actions.submitStart('form')).toEqual({ 37 | type: actions.SUBMIT_START, 38 | payload: { form: 'form' }, 39 | }); 40 | }); 41 | 42 | it('should create an SUBMIT_STOP action', () => { 43 | expect(actions.submitStop('form')).toEqual({ 44 | type: actions.SUBMIT_STOP, 45 | payload: { form: 'form' }, 46 | }); 47 | }); 48 | 49 | it('should create a REMOVE_FIELD action', () => { 50 | expect(actions.removeField('form', 'field')).toEqual({ 51 | type: actions.REMOVE_FIELD, 52 | payload: { form: 'form', id: 'field' }, 53 | }); 54 | }); 55 | 56 | it('should create an ADD_ARRAY action', () => { 57 | expect(actions.addArray('form', 'field')).toEqual({ 58 | type: actions.ADD_ARRAY, 59 | payload: { form: 'form', id: 'field' }, 60 | }); 61 | }); 62 | 63 | it('should create a REMOVE_ARRAY action', () => { 64 | expect(actions.removeArray('form', 'field')).toEqual({ 65 | type: actions.REMOVE_ARRAY, 66 | payload: { form: 'form', id: 'field' }, 67 | }); 68 | }); 69 | 70 | it('should create an ARRAY_PUSH action', () => { 71 | expect(actions.arrayPush('form', 'field')).toEqual({ 72 | type: actions.ARRAY_PUSH, 73 | payload: { form: 'form', id: 'field' }, 74 | }); 75 | }); 76 | 77 | it('should create an ARRAY_POP action', () => { 78 | expect(actions.arrayPop('form', 'field')).toEqual({ 79 | type: actions.ARRAY_POP, 80 | payload: { form: 'form', id: 'field' }, 81 | }); 82 | }); 83 | 84 | it('should create an ARRAY_UNSHIFT action', () => { 85 | expect(actions.arrayUnshift('form', 'field')).toEqual({ 86 | type: actions.ARRAY_UNSHIFT, 87 | payload: { form: 'form', id: 'field' }, 88 | }); 89 | }); 90 | 91 | it('should create an ARRAY_SHIFT action', () => { 92 | expect(actions.arrayShift('form', 'field')).toEqual({ 93 | type: actions.ARRAY_SHIFT, 94 | payload: { form: 'form', id: 'field' }, 95 | }); 96 | }); 97 | 98 | it('should create an ARRAY_INSERT action', () => { 99 | expect(actions.arrayInsert('form', 'field', 1)).toEqual({ 100 | type: actions.ARRAY_INSERT, 101 | payload: { form: 'form', id: 'field', index: 1 }, 102 | }); 103 | }); 104 | 105 | it('should create an ARRAY_REMOVE action', () => { 106 | expect(actions.arrayRemove('form', 'field', 1)).toEqual({ 107 | type: actions.ARRAY_REMOVE, 108 | payload: { form: 'form', id: 'field', index: 1 }, 109 | }); 110 | }); 111 | 112 | it('should create an ARRAY_SWAP action', () => { 113 | expect(actions.arraySwap('form', 'arr', 1, 2)).toEqual({ 114 | type: actions.ARRAY_SWAP, 115 | payload: { form: 'form', id: 'arr', index1: 1, index2: 2 }, 116 | }); 117 | }); 118 | 119 | it('should create an ARRAY_MOVE action', () => { 120 | expect(actions.arrayMove('form', 'arr', 1, 2)).toEqual({ 121 | type: actions.ARRAY_MOVE, 122 | payload: { form: 'form', id: 'arr', from: 1, to: 2 }, 123 | }); 124 | }); 125 | 126 | it('should create a FIELD_CHANGE action', () => { 127 | expect(actions.fieldChange('form', 'field', 'value', 'error', true)).toEqual({ 128 | type: actions.FIELD_CHANGE, 129 | payload: { form: 'form', field: 'field', value: 'value', error: 'error', dirty: true }, 130 | }); 131 | }); 132 | 133 | it('should create a FIELD_FOCUS action', () => { 134 | expect(actions.fieldFocus('form', 'field')).toEqual({ 135 | type: actions.FIELD_FOCUS, 136 | payload: { form: 'form', field: 'field' }, 137 | }); 138 | }); 139 | 140 | it('should create a FIELD_BLUR action', () => { 141 | expect(actions.fieldBlur('form', 'field', 'value', 'error', true)).toEqual({ 142 | type: actions.FIELD_BLUR, 143 | payload: { form: 'form', field: 'field', value: 'value', error: 'error', dirty: true }, 144 | }); 145 | }); 146 | 147 | it('should create a FIELD_VALUE action', () => { 148 | expect(actions.fieldValue('form', 'field', 'value')).toEqual({ 149 | type: actions.FIELD_VALUE, 150 | payload: { form: 'form', field: 'field', value: 'value' }, 151 | }); 152 | }); 153 | 154 | it('should create a FIELD_ERROR action', () => { 155 | expect(actions.fieldError('form', 'field', 'error')).toEqual({ 156 | type: actions.FIELD_ERROR, 157 | payload: { form: 'form', field: 'field', error: 'error' }, 158 | }); 159 | }); 160 | 161 | it('should create a FIELD_DIRTY action', () => { 162 | expect(actions.fieldDirty('form', 'field', true)).toEqual({ 163 | type: actions.FIELD_DIRTY, 164 | payload: { form: 'form', field: 'field', dirty: true }, 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /packages/redux-forms/src/__tests__/arrays.spec.ts: -------------------------------------------------------------------------------- 1 | import { arrayUnshift, arrayShift, arraySwap, arrayMove, arrayCleanup } from '../arrays'; 2 | 3 | import { field } from '../containers'; 4 | 5 | 6 | const field0 = { ...field, value: '0' }; 7 | const field1 = { ...field, value: '1' }; 8 | const field2 = { ...field, value: '2' }; 9 | const field3 = { ...field, value: '3' }; 10 | const field4 = { ...field, value: '4' }; 11 | 12 | const fields = { 13 | 'flat.0': field0, 14 | 'flat.1': field1, 15 | 'flat.2': field2, 16 | 'flat.3': field3, 17 | 'flat.4': field4, 18 | 'medium.0.nest.0': field0, 19 | 'medium.0.nest.1': field1, 20 | 'medium.0.nest.2': field2, 21 | 'medium.0.nest.3': field3, 22 | 'medium.1.nest.0': field3, 23 | 'medium.1.nest.1': field2, 24 | 'medium.1.nest.2': field1, 25 | 'medium.1.nest.3': field0, 26 | 'rec.0.rec.0.rec.0': field0, 27 | 'rec.0.rec.0.rec.1': field1, 28 | 'rec.0.rec.0.rec.2': field2, 29 | }; 30 | 31 | 32 | describe('#arrays', () => { 33 | it('should not shift to negative index', () => { 34 | const res = arrayShift('flat', 0)(fields); 35 | 36 | expect(res['flat.-1']).toBeUndefined(); 37 | 38 | expect(res['flat.0']).toBe(field1); 39 | expect(res['flat.1']).toBe(field2); 40 | expect(res['flat.2']).toBe(field3); 41 | expect(res['flat.3']).toBe(field4); 42 | }); 43 | 44 | it('should shift flat array', () => { 45 | const res = arrayUnshift('flat', 1)(fields); 46 | 47 | expect(res['flat.1']).toBeUndefined(); 48 | 49 | expect(res['flat.0']).toBe(field0); 50 | expect(res['flat.2']).toBe(field1); 51 | expect(res['flat.3']).toBe(field2); 52 | expect(res['flat.4']).toBe(field3); 53 | expect(res['flat.5']).toBe(field4); 54 | }); 55 | 56 | it('should shift flat array - negative', () => { 57 | const res = arrayShift('flat', 1)(fields); 58 | 59 | expect(res['flat.4']).toBeUndefined(); 60 | 61 | expect(res['flat.0']).toBe(field0); 62 | expect(res['flat.1']).toBe(field2); 63 | expect(res['flat.2']).toBe(field3); 64 | expect(res['flat.3']).toBe(field4); 65 | }); 66 | 67 | it('should shift nested array', () => { 68 | const res = arrayUnshift('medium.0.nest', 2)(fields); 69 | 70 | expect(res['medium.0.nest.2']).toBeUndefined(); 71 | 72 | expect(res['medium.0.nest.0']).toBe(field0); 73 | expect(res['medium.0.nest.1']).toBe(field1); 74 | expect(res['medium.0.nest.3']).toBe(field2); 75 | expect(res['medium.0.nest.4']).toBe(field3); 76 | }); 77 | 78 | it('should shift nested array - negative', () => { 79 | const res = arrayShift('medium.0.nest', 2)(fields); 80 | 81 | expect(res['medium.0.nest.3']).toBeUndefined(); 82 | 83 | expect(res['medium.0.nest.0']).toBe(field0); 84 | expect(res['medium.0.nest.1']).toBe(field1); 85 | expect(res['medium.0.nest.2']).toBe(field3); 86 | }); 87 | 88 | it('should shift recursive array - head', () => { 89 | const res = arrayUnshift('rec', 0)(fields); 90 | 91 | expect(res['rec.0.rec.0.rec.0']).toBeUndefined(); 92 | expect(res['rec.0.rec.0.rec.1']).toBeUndefined(); 93 | expect(res['rec.0.rec.0.rec.2']).toBeUndefined(); 94 | 95 | expect(res['rec.1.rec.0.rec.0']).toBe(field0); 96 | expect(res['rec.1.rec.0.rec.1']).toBe(field1); 97 | expect(res['rec.1.rec.0.rec.2']).toBe(field2); 98 | }); 99 | 100 | it('should shift recursive array - mid', () => { 101 | const res = arrayUnshift('rec.0.rec', 0)(fields); 102 | 103 | expect(res['rec.0.rec.0.rec.0']).toBeUndefined(); 104 | expect(res['rec.0.rec.0.rec.1']).toBeUndefined(); 105 | expect(res['rec.0.rec.0.rec.2']).toBeUndefined(); 106 | 107 | expect(res['rec.0.rec.1.rec.0']).toBe(field0); 108 | expect(res['rec.0.rec.1.rec.1']).toBe(field1); 109 | expect(res['rec.0.rec.1.rec.2']).toBe(field2); 110 | }); 111 | 112 | it('should shift recursive array - last', () => { 113 | const res = arrayUnshift('rec.0.rec.0.rec', 1)(fields); 114 | 115 | expect(res['rec.0.rec.0.rec.1']).toBeUndefined(); 116 | 117 | expect(res['rec.0.rec.0.rec.0']).toBe(field0); 118 | expect(res['rec.0.rec.0.rec.2']).toBe(field1); 119 | expect(res['rec.0.rec.0.rec.3']).toBe(field2); 120 | }); 121 | 122 | it('should not swap nonexistent fields', () => { 123 | const res = arraySwap('medium.0.nest', 1, 8)(fields); 124 | 125 | expect(res).toBe(fields); 126 | }); 127 | 128 | it('should swap two fields', () => { 129 | const res = arraySwap('medium.0.nest', 1, 3)(fields); 130 | 131 | expect(res).toEqual({ 132 | ...fields, 133 | 'medium.0.nest.1': field3, 134 | 'medium.0.nest.3': field1, 135 | }); 136 | }); 137 | 138 | it('should swap nested fields', () => { 139 | const res = arraySwap('medium', 0, 1)(fields); 140 | 141 | expect(res['medium.0.nest.0']).toBe(fields['medium.1.nest.0']); 142 | expect(res['medium.0.nest.1']).toBe(fields['medium.1.nest.1']); 143 | expect(res['medium.0.nest.2']).toBe(fields['medium.1.nest.2']); 144 | expect(res['medium.0.nest.3']).toBe(fields['medium.1.nest.3']); 145 | 146 | expect(res['medium.1.nest.0']).toBe(fields['medium.0.nest.0']); 147 | expect(res['medium.1.nest.1']).toBe(fields['medium.0.nest.1']); 148 | expect(res['medium.1.nest.2']).toBe(fields['medium.0.nest.2']); 149 | expect(res['medium.1.nest.3']).toBe(fields['medium.0.nest.3']); 150 | }); 151 | 152 | it('should not move nonexistent fields', () => { 153 | const res = arrayMove('medium.0.nest', 1, 8)(fields); 154 | 155 | expect(res).toBe(fields); 156 | }); 157 | 158 | it('should move a field - start', () => { 159 | const res = arrayMove('flat', 0, 2)(fields); 160 | 161 | expect(res['flat.0']).toBe(field1); 162 | expect(res['flat.1']).toBe(field2); 163 | expect(res['flat.2']).toBe(field0); 164 | expect(res['flat.3']).toBe(field3); 165 | expect(res['flat.4']).toBe(field4); 166 | }); 167 | 168 | it('should move a field - end', () => { 169 | const res = arrayMove('flat', 1, 4)(fields); 170 | 171 | expect(res['flat.0']).toBe(field0); 172 | expect(res['flat.1']).toBe(field2); 173 | expect(res['flat.2']).toBe(field3); 174 | expect(res['flat.3']).toBe(field4); 175 | expect(res['flat.4']).toBe(field1); 176 | }); 177 | 178 | it('should move a field - less', () => { 179 | const res = arrayMove('flat', 3, 1)(fields); 180 | 181 | expect(res['flat.0']).toBe(field0); 182 | expect(res['flat.1']).toBe(field3); 183 | expect(res['flat.2']).toBe(field1); 184 | expect(res['flat.3']).toBe(field2); 185 | expect(res['flat.4']).toBe(field4); 186 | }); 187 | 188 | it('should cleanup fields', () => { 189 | const res = arrayCleanup('medium')(fields); 190 | 191 | expect(res['flat.0']).toBe(field0); 192 | 193 | expect(res['medium.0.nest.0']).toBeUndefined(); 194 | expect(res['medium.0.nest.1']).toBeUndefined(); 195 | expect(res['medium.0.nest.2']).toBeUndefined(); 196 | expect(res['medium.0.nest.3']).toBeUndefined(); 197 | expect(res['medium.1.nest.0']).toBeUndefined(); 198 | expect(res['medium.1.nest.1']).toBeUndefined(); 199 | expect(res['medium.1.nest.2']).toBeUndefined(); 200 | expect(res['medium.1.nest.3']).toBeUndefined(); 201 | 202 | expect(res['rec.0.rec.0.rec.0']).toBe(field0); 203 | }); 204 | }); 205 | -------------------------------------------------------------------------------- /packages/redux-forms/src/__tests__/reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import reducer from '../reducer'; 2 | import * as actions from '../actions'; 3 | import { form, field } from '../containers'; 4 | 5 | 6 | describe('#formsReducer', () => { 7 | it('should return initial state', () => { 8 | const state = reducer(undefined, {} as any); 9 | 10 | expect(state).toEqual({}); 11 | }); 12 | 13 | it('should add a form', () => { 14 | const state = reducer({}, actions.addForm('form')); 15 | 16 | expect(state).toEqual({ form }); 17 | }); 18 | 19 | it('should remove a form', () => { 20 | const state = reducer({ form }, actions.removeForm('form')); 21 | 22 | expect(state).toEqual({}); 23 | }); 24 | 25 | it('should add a field', () => { 26 | const state: any = reducer({ 27 | form: { ...form, fields: {} }, 28 | }, actions.addField('form', 'field', field)); 29 | 30 | expect(state.form.fields.field).toEqual(field); 31 | }); 32 | 33 | it('should not add a field without form', () => { 34 | const state: any = reducer({}, actions.addField('form', 'field', field)); 35 | 36 | expect(state.form).toBeUndefined(); 37 | }); 38 | 39 | it('should remove a field', () => { 40 | const state: any = reducer({ 41 | form: { ...form, fields: { field } }, 42 | }, actions.removeField('form', 'field')); 43 | 44 | expect(state.form.fields).toEqual({}); 45 | }); 46 | 47 | it('should touch all fields', () => { 48 | const state: any = reducer({ 49 | form: { 50 | ...form, 51 | fields: { field1: field, field2: field }, 52 | }, 53 | }, actions.touchAll('form')); 54 | 55 | expect(state.form.fields.field1.touched).toBe(true); 56 | expect(state.form.fields.field2.touched).toBe(true); 57 | }); 58 | 59 | it('should not touch all fields without form', () => { 60 | const state: any = reducer({}, actions.touchAll('form')); 61 | 62 | expect(state).toEqual({}); 63 | }); 64 | 65 | it('should start submit', () => { 66 | const state: any = reducer({ form }, actions.submitStart('form')); 67 | 68 | expect(state.form.submitting).toBe(true); 69 | }); 70 | 71 | it('should not start submit without form', () => { 72 | const state: any = reducer({}, actions.submitStart('form')); 73 | 74 | expect(state.form).toBeUndefined(); 75 | }); 76 | 77 | it('should stop submit', () => { 78 | const state: any = reducer({ 79 | form: { ...form, submitting: true }, 80 | }, actions.submitStop('form')); 81 | 82 | expect(state.form.submitting).toBe(false); 83 | }); 84 | 85 | it('should not stop submit without form', () => { 86 | const state: any = reducer({}, actions.submitStop('form')); 87 | 88 | expect(state.form).toBeUndefined(); 89 | }); 90 | 91 | it('should add an array', () => { 92 | const state: any = reducer({ 93 | form, 94 | }, actions.addArray('form', 'array')); 95 | 96 | expect(state.form.arrays.array).toBe(0); 97 | }); 98 | 99 | it('should not add an array without form', () => { 100 | const state: any = reducer({}, actions.addArray('form', 'array')); 101 | 102 | expect(state.form).toBeUndefined(); 103 | }); 104 | 105 | it('should remove an array', () => { 106 | const state: any = reducer({ 107 | form: { 108 | ...form, 109 | fields: { 'array.0': field }, 110 | arrays: { array: 1 }, 111 | }, 112 | }, actions.removeArray('form', 'array')); 113 | 114 | expect(state.form).toEqual(form); 115 | }); 116 | 117 | it('should push to an array', () => { 118 | const state: any = reducer({ 119 | form: { 120 | ...form, 121 | arrays: { array: 1 }, 122 | }, 123 | }, actions.arrayPush('form', 'array')); 124 | 125 | expect(state.form.arrays.array).toBe(2); 126 | }); 127 | 128 | it('should not push to an array without form', () => { 129 | const state: any = reducer({}, actions.arrayPush('form', 'array')); 130 | 131 | expect(state.form).toBeUndefined(); 132 | }); 133 | 134 | it('should pop from an array', () => { 135 | const state: any = reducer({ 136 | form: { ...form, arrays: { array: 2 } }, 137 | }, actions.arrayPop('form', 'array')); 138 | 139 | expect(state.form.arrays.array).toBe(1); 140 | }); 141 | 142 | it('should not pop from an array without form', () => { 143 | const state: any = reducer({}, actions.arrayPop('form', 'array')); 144 | 145 | expect(state.form).toBeUndefined(); 146 | }); 147 | 148 | it('should unshift an array', () => { 149 | const state: any = reducer({ 150 | form: { 151 | ...form, 152 | fields: { 'array.0': field }, 153 | arrays: { array: 1 }, 154 | }, 155 | }, actions.arrayUnshift('form', 'array')); 156 | 157 | expect(state.form.fields['array.0']).toBeUndefined(); 158 | expect(state.form.fields['array.1']).toBeDefined(); 159 | expect(state.form.arrays.array).toBe(2); 160 | }); 161 | 162 | it('should not unshift an array without form', () => { 163 | const state: any = reducer({}, actions.arrayUnshift('form', 'array')); 164 | 165 | expect(state.form).toBeUndefined(); 166 | }); 167 | 168 | it('should shift an array', () => { 169 | const state: any = reducer({ 170 | form: { 171 | ...form, 172 | fields: { 'array.0': field, 'array.1': field }, 173 | arrays: { array: 2 }, 174 | }, 175 | }, actions.arrayShift('form', 'array')); 176 | 177 | expect(state.form.fields['array.0']).toBeDefined(); 178 | expect(state.form.fields['array.1']).toBeUndefined(); 179 | expect(state.form.arrays.array).toBe(1); 180 | }); 181 | 182 | it('should not shift an array without form', () => { 183 | const state: any = reducer({ }, actions.arrayShift('form', 'array')); 184 | 185 | expect(state.form).toBeUndefined(); 186 | }); 187 | 188 | it('should insert to an array', () => { 189 | const state: any = reducer({ 190 | form: { 191 | ...form, 192 | fields: { 'array.0': field, 'array.1': field }, 193 | arrays: { array: 2 }, 194 | }, 195 | }, actions.arrayInsert('form', 'array', 0)); 196 | 197 | expect(state.form.fields['array.0']).toBeDefined(); 198 | expect(state.form.fields['array.1']).toBeUndefined(); 199 | expect(state.form.fields['array.2']).toBeDefined(); 200 | expect(state.form.arrays.array).toBe(3); 201 | }); 202 | 203 | it('should not insert to an array without form', () => { 204 | const state: any = reducer({}, actions.arrayInsert('form', 'array', 0)); 205 | 206 | expect(state.form).toBeUndefined(); 207 | }); 208 | 209 | it('should remove from an array', () => { 210 | const field0 = { ...field, value: '0' }; 211 | const field2 = { ...field, value: '2' }; 212 | 213 | const state: any = reducer({ 214 | form: { 215 | ...form, 216 | fields: { 'array.0': field0, 'array.1': field, 'array.2': field2 }, 217 | arrays: { array: 3 }, 218 | }, 219 | }, actions.arrayRemove('form', 'array', 1)); 220 | 221 | expect(state.form.fields['array.0']).toBe(field0); 222 | expect(state.form.fields['array.1']).toBe(field2); 223 | expect(state.form.fields['array.2']).toBeUndefined(); 224 | expect(state.form.arrays.array).toBe(2); 225 | }); 226 | 227 | it('should not remove from an array without form', () => { 228 | const state: any = reducer({}, actions.arrayRemove('form', 'array', 1)); 229 | 230 | expect(state.form).toBeUndefined(); 231 | }); 232 | 233 | it('should swap fields in an array', () => { 234 | const field0 = { ...field, value: '0' }; 235 | const field1 = { ...field, value: '1' }; 236 | 237 | const state: any = reducer({ 238 | form: { 239 | ...form, 240 | fields: { 'array.0': field0, 'array.1': field1 }, 241 | arrays: { array: 2 }, 242 | }, 243 | }, actions.arraySwap('form', 'array', 0, 1)); 244 | 245 | expect(state.form.fields['array.0']).toBe(field1); 246 | expect(state.form.fields['array.1']).toBe(field0); 247 | }); 248 | 249 | it('should not swap fields in an array without form', () => { 250 | const state: any = reducer({}, actions.arraySwap('form', 'array', 0, 1)); 251 | 252 | expect(state.form).toBeUndefined(); 253 | }); 254 | 255 | it('should move a field in an array', () => { 256 | const field0 = { ...field, value: '0' }; 257 | const field1 = { ...field, value: '1' }; 258 | const field2 = { ...field, value: '2' }; 259 | 260 | const state: any = reducer({ 261 | form: { 262 | ...form, 263 | fields: { 'array.0': field0, 'array.1': field1, 'array.2': field2 }, 264 | arrays: { array: 2 }, 265 | }, 266 | }, actions.arrayMove('form', 'array', 0, 2)); 267 | 268 | expect(state.form.fields['array.0']).toBe(field1); 269 | expect(state.form.fields['array.1']).toBe(field2); 270 | expect(state.form.fields['array.2']).toBe(field0); 271 | }); 272 | 273 | it('should not move a field in an array without form', () => { 274 | const state: any = reducer({}, actions.arrayMove('form', 'array', 0, 2)); 275 | 276 | expect(state.form).toBeUndefined(); 277 | }); 278 | 279 | it('should change a field', () => { 280 | const state: any = reducer({ 281 | form: { ...form, fields: { field } }, 282 | }, actions.fieldChange('form', 'field', 'doge', 'error', true)); 283 | 284 | expect(state.form.fields.field).toEqual({ 285 | value: 'doge', 286 | error: 'error', 287 | dirty: true, 288 | touched: false, 289 | visited: false, 290 | active: false, 291 | }); 292 | }); 293 | 294 | it('should not change a field without form', () => { 295 | const state: any = reducer({}, actions.fieldChange('form', 'field', 'doge', 'error', true)); 296 | 297 | expect(state.form).toBeUndefined(); 298 | }); 299 | 300 | it('should not change unwanted props', () => { 301 | const newField = { 302 | ...field, 303 | touched: true, 304 | visited: true, 305 | active: true, 306 | }; 307 | 308 | const state: any = reducer({ 309 | form: { ...form, fields: { field: newField } }, 310 | }, actions.fieldChange('form', 'field', 'doge', 'error', true)); 311 | 312 | expect(state.form.fields.field.touched).toBe(true); 313 | expect(state.form.fields.field.visited).toBe(true); 314 | expect(state.form.fields.field.active).toBe(true); 315 | }); 316 | 317 | it('should focus a field', () => { 318 | const state: any = reducer({ 319 | form: { ...form, fields: { field } }, 320 | }, actions.fieldFocus('form', 'field')); 321 | 322 | expect(state.form.fields.field).toEqual({ 323 | value: '', 324 | error: null, 325 | dirty: false, 326 | touched: false, 327 | visited: true, 328 | active: true, 329 | }); 330 | }); 331 | 332 | it('should not focus a field without form', () => { 333 | const state: any = reducer({}, actions.fieldFocus('form', 'field')); 334 | 335 | expect(state.form).toBeUndefined(); 336 | }); 337 | 338 | it('should not focus unwanted props', () => { 339 | const newField = { 340 | ...field, 341 | value: 'doge', 342 | error: 'error', 343 | dirty: true, 344 | touched: true, 345 | }; 346 | 347 | const state: any = reducer({ 348 | form: { ...form, fields: { field: newField } }, 349 | }, actions.fieldFocus('form', 'field')); 350 | 351 | expect(state.form.fields.field.value).toBe('doge'); 352 | expect(state.form.fields.field.error).toBe('error'); 353 | expect(state.form.fields.field.dirty).toBe(true); 354 | expect(state.form.fields.field.touched).toBe(true); 355 | }); 356 | 357 | it('should blur a field', () => { 358 | const newField = { 359 | ...field, 360 | active: true, 361 | }; 362 | 363 | const state: any = reducer({ 364 | form: { ...form, fields: { field: newField } }, 365 | }, actions.fieldBlur('form', 'field', 'value', 'error', true)); 366 | 367 | expect(state.form.fields.field).toEqual({ 368 | value: 'value', 369 | error: 'error', 370 | dirty: true, 371 | touched: true, 372 | visited: false, 373 | active: false, 374 | }); 375 | }); 376 | 377 | it('should not blur a field without form', () => { 378 | const state: any = reducer({}, actions.fieldBlur('form', 'field', 'value', 'error', true)); 379 | 380 | expect(state.form).toBeUndefined(); 381 | }); 382 | 383 | it('should not blur unwanted props', () => { 384 | const newField = { 385 | ...field, 386 | visited: true, 387 | }; 388 | 389 | const state: any = reducer({ 390 | form: { ...form, fields: { field: newField } }, 391 | }, actions.fieldBlur('form', 'field', 'value', 'error', true)); 392 | 393 | expect(state.form.fields.field.visited).toBe(true); 394 | }); 395 | 396 | it('should change field value', () => { 397 | const state: any = reducer({ 398 | form: { ...form, fields: { field } }, 399 | }, actions.fieldValue('form', 'field', 'value')); 400 | 401 | expect(state.form.fields.field.value).toBe('value'); 402 | }); 403 | 404 | it('should not change field value without form', () => { 405 | const state: any = reducer({}, actions.fieldValue('form', 'field', 'value')); 406 | 407 | expect(state.form).toBeUndefined(); 408 | }); 409 | 410 | it('should change field error', () => { 411 | const state: any = reducer({ 412 | form: { ...form, fields: { field } }, 413 | }, actions.fieldError('form', 'field', 'error')); 414 | 415 | expect(state.form.fields.field.error).toBe('error'); 416 | }); 417 | 418 | it('should not change field error without form', () => { 419 | const state: any = reducer({}, actions.fieldError('form', 'field', 'error')); 420 | 421 | expect(state.form).toBeUndefined(); 422 | }); 423 | 424 | it('should change field dirty', () => { 425 | const state: any = reducer({ 426 | form: { ...form, fields: { field } }, 427 | }, actions.fieldDirty('form', 'field', true)); 428 | 429 | expect(state.form.fields.field.dirty).toBe(true); 430 | }); 431 | 432 | it('should not change field dirty without form', () => { 433 | const state: any = reducer({}, actions.fieldDirty('form', 'field', true)); 434 | 435 | expect(state.form).toBeUndefined(); 436 | }); 437 | }); 438 | -------------------------------------------------------------------------------- /packages/redux-forms/src/__tests__/selectors.spec.ts: -------------------------------------------------------------------------------- 1 | import * as selectors from '../selectors'; 2 | 3 | import { form, field } from "../containers"; 4 | 5 | 6 | const demoform = { 7 | ...form, 8 | fields: { 9 | 'flat': field, 10 | 'array.0': field, 11 | 'array.1': field, 12 | 'deep.0.array.0.name': field, 13 | 'deep.0.array.1.name': field, 14 | }, 15 | }; 16 | 17 | const demoform2 = { 18 | ...form, 19 | fields: { 20 | 'flat': field, 21 | 'array.0': field, 22 | 'array.1': field, 23 | 'deep.0.array.0.name': field, 24 | 'deep.0.array.1.name': field, 25 | }, 26 | }; 27 | 28 | const errform = { 29 | ...form, 30 | fields: { 31 | 'flat': { ...field, error: 'error' }, 32 | 'array.0': field, 33 | }, 34 | }; 35 | 36 | const touchform = { 37 | ...form, 38 | fields: { 39 | flat: { ...field, touched: true }, 40 | flat2: { ...field, touched: false }, 41 | }, 42 | }; 43 | 44 | const dirtyform = { 45 | ...form, 46 | fields: { 47 | flat: { ...field, dirty: true }, 48 | flat2: { ...field, dirty: false }, 49 | }, 50 | }; 51 | 52 | const submitform = { 53 | ...form, 54 | submitting: true, 55 | }; 56 | 57 | const emptystate: any = {}; 58 | 59 | const state = { 60 | reduxForms: { test: demoform }, 61 | }; 62 | 63 | const state2 = { 64 | reduxForms: { test: demoform2 }, 65 | }; 66 | 67 | const errstate = { 68 | reduxForms: { test: errform }, 69 | }; 70 | 71 | const touchstate = { 72 | reduxForms: { test: touchform }, 73 | }; 74 | 75 | const dirtystate = { 76 | reduxForms: { test: dirtyform }, 77 | }; 78 | 79 | const submitstate = { 80 | reduxForms: { test: submitform }, 81 | }; 82 | 83 | 84 | describe('#selectors', () => { 85 | it('should return empty if no reducer - value', () => { 86 | expect(selectors.getValues('nonexistent', emptystate)).toEqual({}); 87 | }); 88 | 89 | it('should return empty if no reducer - error', () => { 90 | expect(selectors.getErrors('nonexistent', emptystate)).toEqual({}); 91 | }); 92 | 93 | it('should return the same empty - value', () => { 94 | const res1 = selectors.getValues('nonexistent', emptystate); 95 | const res2 = selectors.getValues('nonexistent', emptystate); 96 | 97 | expect(res1).toBe(res2); 98 | }); 99 | 100 | it('should return the same empty - error', () => { 101 | const res1 = selectors.getErrors('nonexistent', emptystate); 102 | const res2 = selectors.getErrors('nonexistent', emptystate); 103 | 104 | expect(res1).toBe(res2); 105 | }); 106 | 107 | it('should return empty if no reducer - valid', () => { 108 | expect(selectors.isValid('nonexistent', emptystate)).toBe(false); 109 | }); 110 | 111 | it('should return empty if no reducer - touched', () => { 112 | expect(selectors.isTouched('nonexistent', emptystate)).toBe(false); 113 | }); 114 | 115 | it('should return empty if no reducer - dirty', () => { 116 | expect(selectors.isDirty('nonexistent', emptystate)).toBe(false); 117 | }); 118 | 119 | it('should return empty if no reducer - submitting', () => { 120 | expect(selectors.isSubmitting('nonexistent', emptystate)).toBe(false); 121 | }); 122 | 123 | it('should return empty if no form - value', () => { 124 | expect(selectors.getValues('nonexistent', state)).toEqual({}); 125 | }); 126 | 127 | it('should return empty if no form - error', () => { 128 | expect(selectors.getErrors('nonexistent', state)).toEqual({}); 129 | }); 130 | 131 | it('should return empty if no form - valid', () => { 132 | expect(selectors.isValid('nonexistent', state)).toBe(false); 133 | }); 134 | 135 | it('should return empty if no form - touched', () => { 136 | expect(selectors.isTouched('nonexistent', state)).toBe(false); 137 | }); 138 | 139 | it('should return empty if no form - valid', () => { 140 | expect(selectors.isDirty('nonexistent', state)).toBe(false); 141 | }); 142 | 143 | it('should return empty if no form - touched', () => { 144 | expect(selectors.isSubmitting('nonexistent', state)).toBe(false); 145 | }); 146 | 147 | it('should produce an id memoized value form', () => { 148 | const res = selectors.getValues('test', state); 149 | const res2 = selectors.getValues('test', state); 150 | 151 | expect(res).toBe(res2); 152 | }); 153 | 154 | it('should produce a value memoized form', () => { 155 | const res = selectors.getValues('test', state); 156 | const res2 = selectors.getValues('test', state2); 157 | 158 | expect(res).toBe(res2); 159 | }); 160 | 161 | it('should produce an id memoized error form', () => { 162 | const res = selectors.getErrors('test', state); 163 | const res2 = selectors.getErrors('test', state); 164 | 165 | expect(res).toBe(res2); 166 | }); 167 | 168 | it('should produce an error memoized form', () => { 169 | const res = selectors.getErrors('test', state); 170 | const res2 = selectors.getErrors('test', state2); 171 | 172 | expect(res).toBe(res2); 173 | }); 174 | 175 | it('should produce nested values', () => { 176 | const res = selectors.getValues('test', state); 177 | 178 | expect(res).toEqual({ 179 | flat: '', 180 | array: ['', ''], 181 | deep: [{ 182 | array: [{ name: '' }, { name: '' }], 183 | }], 184 | }); 185 | }); 186 | 187 | it('should produce nested errors', () => { 188 | const res = selectors.getErrors('test', state); 189 | 190 | expect(res).toEqual({ 191 | flat: null, 192 | array: [null, null], 193 | deep: [{ 194 | array: [{ name: null }, { name: null }], 195 | }], 196 | }); 197 | }); 198 | 199 | it('should reduce valid - true', () => { 200 | const res = selectors.isValid('test', state); 201 | 202 | expect(res).toBe(true); 203 | }); 204 | 205 | it('should reduce valid - false', () => { 206 | const res = selectors.isValid('test', errstate); 207 | 208 | expect(res).toBe(false); 209 | }); 210 | 211 | it('should reduce touched - false', () => { 212 | const res = selectors.isTouched('test', state); 213 | 214 | expect(res).toBe(false); 215 | }); 216 | 217 | it('should reduce touched - true', () => { 218 | const res = selectors.isTouched('test', touchstate); 219 | 220 | expect(res).toBe(true); 221 | }); 222 | 223 | it('should reduce dirty - false', () => { 224 | const res = selectors.isDirty('test', state); 225 | 226 | expect(res).toBe(false); 227 | }); 228 | 229 | it('should reduce dirty - true', () => { 230 | const res = selectors.isDirty('test', dirtystate); 231 | 232 | expect(res).toBe(true); 233 | }); 234 | 235 | it('should determine submitting - false', () => { 236 | const res = selectors.isSubmitting('test', state); 237 | 238 | expect(res).toBe(false); 239 | }); 240 | 241 | it('should determine submitting - true', () => { 242 | const res = selectors.isSubmitting('test', submitstate); 243 | 244 | expect(res).toBe(true); 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /packages/redux-forms/src/actions.ts: -------------------------------------------------------------------------------- 1 | import { Field } from './containers'; 2 | 3 | 4 | export const ADD_FORM = '@@redux-forms/ADD_FORM'; 5 | export const REMOVE_FORM = '@@redux-forms/REMOVE_FORM'; 6 | export const ADD_FIELD = '@@redux-forms/ADD_FIELD'; 7 | export const REMOVE_FIELD = '@@redux-forms/REMOVE_FIELD'; 8 | export const TOUCH_ALL = '@@redux-forms/TOUCH_ALL'; 9 | export const SUBMIT_START = '@@redux-forms/SUBMIT_START'; 10 | export const SUBMIT_STOP = '@@redux-forms/SUBMIT_STOP'; 11 | 12 | export const ADD_ARRAY = '@@redux-forms/ADD_ARRAY'; 13 | export const REMOVE_ARRAY = '@@redux-forms/REMOVE_ARRAY'; 14 | export const ARRAY_PUSH = '@@redux-forms/ARRAY_PUSH'; 15 | export const ARRAY_POP = '@@redux-forms/ARRAY_POP'; 16 | export const ARRAY_UNSHIFT = '@@redux-forms/ARRAY_UNSHIFT'; 17 | export const ARRAY_SHIFT = '@@redux-forms/ARRAY_SHIFT'; 18 | export const ARRAY_INSERT = '@@redux-forms/ARRAY_INSERT'; 19 | export const ARRAY_REMOVE = '@@redux-forms/ARRAY_REMOVE'; 20 | export const ARRAY_SWAP = '@@redux-forms/ARRAY_SWAP'; 21 | export const ARRAY_MOVE = '@@redux-forms/ARRAY_MOVE'; 22 | 23 | export const FIELD_CHANGE = '@@redux-forms/FIELD_CHANGE'; 24 | export const FIELD_FOCUS = '@@redux-forms/FIELD_FOCUS'; 25 | export const FIELD_BLUR = '@@redux-forms/FIELD_BLUR'; 26 | export const FIELD_VALUE = '@@redux-forms/FIELD_VALUE'; 27 | export const FIELD_ERROR = '@@redux-forms/FIELD_ERROR'; 28 | export const FIELD_DIRTY = '@@redux-forms/FIELD_DIRTY'; 29 | 30 | 31 | export type AddFormAction = { type: '@@redux-forms/ADD_FORM', payload: { 32 | name: string, 33 | } }; 34 | 35 | export const addForm = (name: string): AddFormAction => ({ 36 | type: ADD_FORM, 37 | payload: { name }, 38 | }); 39 | 40 | 41 | export type RemoveFormAction = { type: '@@redux-forms/REMOVE_FORM', payload: { 42 | name: string, 43 | } }; 44 | 45 | export const removeForm = (name: string): RemoveFormAction => ({ 46 | type: REMOVE_FORM, 47 | payload: { name }, 48 | }); 49 | 50 | 51 | export type AddFieldAction = { type: '@@redux-forms/ADD_FIELD', payload: { 52 | form: string, 53 | id: string, 54 | field: Field, 55 | } }; 56 | 57 | export const addField = (form: string, id: string, field: Field): AddFieldAction => ({ 58 | type: ADD_FIELD, 59 | payload: { form, id, field }, 60 | }); 61 | 62 | 63 | export type RemoveFieldAction = { type: '@@redux-forms/REMOVE_FIELD', payload: { 64 | form: string, 65 | id: string, 66 | } }; 67 | 68 | export const removeField = (form: string, id: string): RemoveFieldAction => ({ 69 | type: REMOVE_FIELD, 70 | payload: { form, id }, 71 | }); 72 | 73 | 74 | export type TouchAllAction = { type: '@@redux-forms/TOUCH_ALL', payload: { 75 | form: string, 76 | } }; 77 | 78 | export const touchAll = (form: string): TouchAllAction => ({ 79 | type: TOUCH_ALL, 80 | payload: { form }, 81 | }); 82 | 83 | 84 | export type SubmitStartAction = { type: '@@redux-forms/SUBMIT_START', payload: { 85 | form: string, 86 | } }; 87 | 88 | export const submitStart = (form: string): SubmitStartAction => ({ 89 | type: SUBMIT_START, 90 | payload: { form }, 91 | }); 92 | 93 | 94 | export type SubmitStopAction = { type: '@@redux-forms/SUBMIT_STOP', payload: { 95 | form: string, 96 | } }; 97 | 98 | export const submitStop = (form: string): SubmitStopAction => ({ 99 | type: SUBMIT_STOP, 100 | payload: { form }, 101 | }); 102 | 103 | 104 | export type AddArrayAction = { type: '@@redux-forms/ADD_ARRAY', payload: { 105 | form: string, 106 | id: string, 107 | } }; 108 | 109 | export const addArray = (form: string, id: string): AddArrayAction => ({ 110 | type: ADD_ARRAY, 111 | payload: { form, id }, 112 | }); 113 | 114 | 115 | export type RemoveArrayAction = { type: '@@redux-forms/REMOVE_ARRAY', payload: { 116 | form: string, 117 | id: string, 118 | } }; 119 | 120 | export const removeArray = (form: string, id: string): RemoveArrayAction => ({ 121 | type: REMOVE_ARRAY, 122 | payload: { form, id }, 123 | }); 124 | 125 | 126 | export type ArrayPushAction = { type: '@@redux-forms/ARRAY_PUSH', payload: { 127 | form: string, 128 | id: string, 129 | } }; 130 | 131 | export const arrayPush = (form: string, id: string): ArrayPushAction => ({ 132 | type: ARRAY_PUSH, 133 | payload: { form, id }, 134 | }); 135 | 136 | 137 | export type ArrayPopAction = { type: '@@redux-forms/ARRAY_POP', payload: { 138 | form: string, 139 | id: string, 140 | } }; 141 | 142 | export const arrayPop = (form: string, id: string): ArrayPopAction => ({ 143 | type: ARRAY_POP, 144 | payload: { form, id }, 145 | }); 146 | 147 | 148 | export type ArrayUnshiftAction = { type: '@@redux-forms/ARRAY_UNSHIFT', payload: { 149 | form: string, 150 | id: string, 151 | } }; 152 | 153 | export const arrayUnshift = (form: string, id: string): ArrayUnshiftAction => ({ 154 | type: ARRAY_UNSHIFT, 155 | payload: { form, id }, 156 | }); 157 | 158 | 159 | export type ArrayShiftAction = { type: '@@redux-forms/ARRAY_SHIFT', payload: { 160 | form: string, 161 | id: string, 162 | } }; 163 | 164 | export const arrayShift = (form: string, id: string): ArrayShiftAction => ({ 165 | type: ARRAY_SHIFT, 166 | payload: { form, id }, 167 | }); 168 | 169 | 170 | export type ArrayInsertAction = { type: '@@redux-forms/ARRAY_INSERT', payload: { 171 | form: string, 172 | id: string, 173 | index: number, 174 | } }; 175 | 176 | export const arrayInsert = (form: string, id: string, index: number): ArrayInsertAction => ({ 177 | type: ARRAY_INSERT, 178 | payload: { form, id, index }, 179 | }); 180 | 181 | 182 | export type ArrayRemoveAction = { type: '@@redux-forms/ARRAY_REMOVE', payload: { 183 | form: string, 184 | id: string, 185 | index: number, 186 | } }; 187 | 188 | export const arrayRemove = (form: string, id: string, index: number): ArrayRemoveAction => ({ 189 | type: ARRAY_REMOVE, 190 | payload: { form, id, index }, 191 | }); 192 | 193 | 194 | export type ArraySwapAction = { type: '@@redux-forms/ARRAY_SWAP', payload: { 195 | form: string, 196 | id: string, 197 | index1: number, 198 | index2: number, 199 | } }; 200 | 201 | export const arraySwap = (form: string, id: string, index1: number, index2: number): ArraySwapAction => ({ 202 | type: ARRAY_SWAP, 203 | payload: { form, id, index1, index2 }, 204 | }); 205 | 206 | 207 | export type ArrayMoveAction = { type: '@@redux-forms/ARRAY_MOVE', payload: { 208 | form: string, 209 | id: string, 210 | from: number, 211 | to: number, 212 | } }; 213 | 214 | export const arrayMove = (form: string, id: string, from: number, to: number): ArrayMoveAction => ({ 215 | type: ARRAY_MOVE, 216 | payload: { form, id, from, to }, 217 | }); 218 | 219 | 220 | export type FieldChangeAction = { type: '@@redux-forms/FIELD_CHANGE', payload: { 221 | form: string, 222 | field: string, 223 | value: any, 224 | error: string | null, 225 | dirty: boolean, 226 | } }; 227 | 228 | export const fieldChange = ( 229 | form: string, field: string, value: any, error: string | null, dirty: boolean, 230 | ): FieldChangeAction => ({ 231 | type: FIELD_CHANGE, 232 | payload: { form, field, value, error, dirty }, 233 | }); 234 | 235 | 236 | export type FieldFocusAction = { type: '@@redux-forms/FIELD_FOCUS', payload: { 237 | form: string, 238 | field: string, 239 | } }; 240 | 241 | export const fieldFocus = (form: string, field: string): FieldFocusAction => ({ 242 | type: FIELD_FOCUS, 243 | payload: { form, field }, 244 | }); 245 | 246 | 247 | export type FieldBlurAction = { type: '@@redux-forms/FIELD_BLUR', payload: { 248 | form: string, 249 | field: string, 250 | value: any, 251 | error: string | null, 252 | dirty: boolean, 253 | } }; 254 | 255 | export const fieldBlur = ( 256 | form: string, field: string, value: any, error: string | null, dirty: boolean, 257 | ): FieldBlurAction => ({ 258 | type: FIELD_BLUR, 259 | payload: { form, field, value, error, dirty }, 260 | }); 261 | 262 | 263 | export type FieldValueAction = { type: '@@redux-forms/FIELD_VALUE', payload: { 264 | form: string, 265 | field: string, 266 | value: any, 267 | } }; 268 | 269 | export const fieldValue = (form: string, field: string, value: any): FieldValueAction => ({ 270 | type: FIELD_VALUE, 271 | payload: { form, field, value }, 272 | }); 273 | 274 | 275 | export type FieldErrorAction = { type: '@@redux-forms/FIELD_ERROR', payload: { 276 | form: string, 277 | field: string, 278 | error: string | null, 279 | } }; 280 | 281 | export const fieldError = (form: string, field: string, error: string | null): FieldErrorAction => ({ 282 | type: FIELD_ERROR, 283 | payload: { form, field, error }, 284 | }); 285 | 286 | 287 | export type FieldDirtyAction = { type: '@@redux-forms/FIELD_DIRTY', payload: { 288 | form: string, 289 | field: string, 290 | dirty: boolean, 291 | } }; 292 | 293 | export const fieldDirty = (form: string, field: string, dirty: boolean): FieldDirtyAction => ({ 294 | type: FIELD_DIRTY, 295 | payload: { form, field, dirty }, 296 | }); 297 | 298 | 299 | export type Action = 300 | | AddFormAction 301 | | RemoveFormAction 302 | | AddFieldAction 303 | | RemoveFieldAction 304 | | TouchAllAction 305 | | SubmitStartAction 306 | | SubmitStopAction 307 | | AddArrayAction 308 | | RemoveArrayAction 309 | | ArrayPushAction 310 | | ArrayPopAction 311 | | ArrayUnshiftAction 312 | | ArrayShiftAction 313 | | ArrayInsertAction 314 | | ArrayRemoveAction 315 | | ArraySwapAction 316 | | ArrayMoveAction 317 | | FieldChangeAction 318 | | FieldFocusAction 319 | | FieldBlurAction 320 | | FieldValueAction 321 | | FieldErrorAction 322 | | FieldDirtyAction; 323 | -------------------------------------------------------------------------------- /packages/redux-forms/src/arrays.ts: -------------------------------------------------------------------------------- 1 | import { 2 | compose, 3 | tail, 4 | split, 5 | replace, 6 | reduce, 7 | assoc, 8 | prop, 9 | head, 10 | prepend, 11 | keys, 12 | any, 13 | map, 14 | identity, 15 | not, 16 | startsWith, 17 | pickBy, 18 | } from 'ramda'; 19 | 20 | import { Field } from "./containers"; 21 | 22 | 23 | export type Fields = { [key: string]: Field }; 24 | 25 | 26 | export function arrayUnshift(path: string, start: number) { 27 | const toParts = compose( 28 | tail, 29 | split('.'), 30 | replace(path, ''), 31 | ); 32 | 33 | return (fields: Fields): Fields => reduce((acc, key) => { 34 | if (key.indexOf(path) !== 0) { 35 | return assoc(key, prop(key, fields), acc); 36 | } 37 | 38 | const parts = toParts(key); 39 | const index = Number(head(parts)); 40 | 41 | if (isNaN(index) || index < start) { 42 | return assoc(key, prop(key, fields), acc); 43 | } 44 | 45 | const lead = `${path}.${index + 1}`; 46 | const newkey = prepend(lead, tail(parts)).join('.'); 47 | return assoc(newkey, prop(key, fields), acc); 48 | }, {}, keys(fields)); 49 | } 50 | 51 | export function arrayShift(path: string, start: number) { 52 | const toParts = compose( 53 | tail, 54 | split('.'), 55 | replace(path, ''), 56 | ); 57 | 58 | return (fields: Fields): Fields => reduce((acc, key) => { 59 | if (key.indexOf(path) !== 0) { 60 | return assoc(key, prop(key, fields), acc); 61 | } 62 | 63 | const parts = toParts(key); 64 | const index = Number(head(parts)); 65 | 66 | if (isNaN(index) || index < start) { 67 | return assoc(key, prop(key, fields), acc); 68 | } 69 | 70 | const newindex = index - 1; 71 | if (newindex < 0 || index === start) { 72 | return acc; 73 | } 74 | 75 | const lead = `${path}.${newindex}`; 76 | const newkey = prepend(lead, tail(parts)).join('.'); 77 | return assoc(newkey, prop(key, fields), acc); 78 | }, {}, keys(fields)); 79 | } 80 | 81 | 82 | function hasPaths(pos1: string, pos2: string, fields: Fields) { 83 | const keyz = keys(fields); 84 | const ok1 = compose( 85 | any(Boolean), 86 | map((key) => key.indexOf(pos1) === 0), 87 | )(keyz); 88 | 89 | const ok2 = compose( 90 | any(Boolean), 91 | map((key) => key.indexOf(pos2) === 0), 92 | )(keyz); 93 | 94 | return ok1 && ok2; 95 | } 96 | 97 | export function arraySwap(path: string, index1: number, index2: number) { 98 | return (fields: Fields): Fields => { 99 | const pos1 = `${path}.${index1}`; 100 | const pos2 = `${path}.${index2}`; 101 | 102 | if (!hasPaths(pos1, pos2, fields)) { 103 | return fields; 104 | } 105 | 106 | return reduce((acc, key) => { 107 | if (key.indexOf(pos1) === 0) { 108 | return assoc(replace(pos1, pos2, key), prop(key, fields), acc); 109 | } 110 | 111 | if (key.indexOf(pos2) === 0) { 112 | return assoc(replace(pos2, pos1, key), prop(key, fields), acc); 113 | } 114 | 115 | return assoc(key, prop(key, fields), acc); 116 | }, {}, keys(fields)); 117 | }; 118 | } 119 | 120 | export function arrayMove(path: string, index1: number, index2: number) { 121 | return (fields: Fields): Fields => { 122 | const pos1 = `${path}.${index1}`; 123 | const pos2 = `${path}.${index2}`; 124 | 125 | if (!hasPaths(pos1, pos2, fields)) { 126 | return fields; 127 | } 128 | 129 | return compose( 130 | assoc(pos2, prop(pos1, fields)), 131 | arrayUnshift(path, index2), 132 | arrayShift(path, index1), 133 | )(fields); 134 | }; 135 | } 136 | 137 | export function arrayCleanup(path: string): (fields: Fields) => Fields { 138 | return pickBy(compose(not, (_: Field, key: string) => startsWith(path, key))); 139 | } 140 | -------------------------------------------------------------------------------- /packages/redux-forms/src/containers.ts: -------------------------------------------------------------------------------- 1 | export const form = { 2 | fields: {}, 3 | arrays: {}, 4 | submitting: false, 5 | }; 6 | 7 | export const field = { 8 | value: '', 9 | error: null, 10 | visited: false, 11 | touched: false, 12 | active: false, 13 | dirty: false, 14 | }; 15 | 16 | 17 | export type Form = { 18 | // key - value pairs of field id and the field object 19 | fields: { [key: string]: Field }, 20 | // a map of array names and its lengths 21 | arrays: { [key: string]: number }, 22 | submitting: boolean, 23 | }; 24 | 25 | export type Field = { 26 | value: any; 27 | visited: boolean; 28 | touched: boolean; 29 | active: boolean; 30 | error: string | null; 31 | dirty: boolean; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/redux-forms/src/index.ts: -------------------------------------------------------------------------------- 1 | import reducer, { State } from './reducer'; 2 | 3 | export default reducer; 4 | 5 | export interface IReduxFormsState { 6 | reduxForms: State; 7 | } 8 | -------------------------------------------------------------------------------- /packages/redux-forms/src/reducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assoc, 3 | assocPath, 4 | dissoc, 5 | dissocPath, 6 | over, 7 | lensPath, 8 | ifElse, 9 | identity, 10 | has, 11 | hasIn, 12 | map, 13 | set, 14 | lensProp, 15 | inc, 16 | dec, 17 | lt, 18 | defaultTo, 19 | always, 20 | pathSatisfies, 21 | compose, 22 | } from 'ramda'; 23 | 24 | import { form, field, Form, Field } from './containers'; 25 | import { arrayUnshift, arrayShift, arraySwap, arrayMove, arrayCleanup } from './arrays'; 26 | import { isNumber } from './shared/helpers'; 27 | 28 | import { 29 | Action, 30 | ADD_FORM, 31 | REMOVE_FORM, 32 | ADD_FIELD, 33 | REMOVE_FIELD, 34 | TOUCH_ALL, 35 | SUBMIT_START, 36 | SUBMIT_STOP, 37 | 38 | ADD_ARRAY, 39 | REMOVE_ARRAY, 40 | ARRAY_PUSH, 41 | ARRAY_POP, 42 | ARRAY_UNSHIFT, 43 | ARRAY_SHIFT, 44 | ARRAY_INSERT, 45 | ARRAY_REMOVE, 46 | ARRAY_SWAP, 47 | ARRAY_MOVE, 48 | 49 | FIELD_CHANGE, 50 | FIELD_FOCUS, 51 | FIELD_BLUR, 52 | FIELD_VALUE, 53 | FIELD_ERROR, 54 | FIELD_DIRTY, 55 | } from './actions'; 56 | 57 | 58 | export type State = { 59 | [form: string]: Form, 60 | }; 61 | 62 | const RarrayInc = compose(inc, defaultTo(0)); 63 | const RarrayDec = compose(dec, defaultTo(0)); 64 | 65 | export default function formsReducer(state: State = {}, a: Action): State { 66 | switch (a.type) { 67 | // Form 68 | // --- 69 | case ADD_FORM: 70 | return assocPath( 71 | [a.payload.name], form, state, 72 | ); 73 | 74 | case REMOVE_FORM: 75 | return dissoc(a.payload.name, state); 76 | 77 | case ADD_FIELD: 78 | return ifElse( 79 | has(a.payload.form), 80 | assocPath([a.payload.form, 'fields', a.payload.id], a.payload.field), 81 | identity, 82 | )(state); 83 | 84 | case REMOVE_FIELD: 85 | return dissocPath( 86 | [a.payload.form, 'fields', a.payload.id], state, 87 | ); 88 | 89 | case TOUCH_ALL: 90 | return ifElse( 91 | has(a.payload.form), 92 | over( 93 | lensPath([a.payload.form, 'fields']), 94 | map(set(lensProp('touched'), true)), 95 | ), 96 | identity, 97 | )(state); 98 | 99 | case SUBMIT_START: 100 | return ifElse( 101 | has(a.payload.form), 102 | assocPath([a.payload.form, 'submitting'], true), 103 | identity, 104 | )(state); 105 | 106 | case SUBMIT_STOP: 107 | return ifElse( 108 | has(a.payload.form), 109 | assocPath([a.payload.form, 'submitting'], false), 110 | identity, 111 | )(state); 112 | 113 | // Array 114 | // --- 115 | case ADD_ARRAY: 116 | return ifElse( 117 | has(a.payload.form), 118 | assocPath([a.payload.form, 'arrays', a.payload.id], 0), 119 | identity, 120 | )(state); 121 | 122 | case REMOVE_ARRAY: 123 | return compose( 124 | over( 125 | lensPath([a.payload.form, 'arrays']), 126 | arrayCleanup(a.payload.id), 127 | ), 128 | over( 129 | lensPath([a.payload.form, 'fields']), 130 | arrayCleanup(a.payload.id), 131 | ), 132 | )(state); 133 | 134 | case ARRAY_PUSH: 135 | return ifElse( 136 | pathSatisfies(isNumber, [a.payload.form, 'arrays', a.payload.id]), 137 | over(lensPath([a.payload.form, 'arrays', a.payload.id]), RarrayInc), 138 | identity, 139 | )(state); 140 | 141 | case ARRAY_POP: 142 | return ifElse( 143 | pathSatisfies(isNumber, [a.payload.form, 'arrays', a.payload.id]), 144 | over( 145 | lensPath([a.payload.form, 'arrays', a.payload.id]), 146 | ifElse(lt(0), RarrayDec, always(0)), 147 | ), 148 | identity, 149 | )(state); 150 | 151 | case ARRAY_UNSHIFT: 152 | return ifElse( 153 | pathSatisfies(isNumber, [a.payload.form, 'arrays', a.payload.id]), 154 | compose( 155 | over( 156 | lensPath([a.payload.form, 'fields']), 157 | arrayUnshift(a.payload.id, 0), 158 | ), 159 | over( 160 | lensPath([a.payload.form, 'arrays', a.payload.id]), 161 | RarrayInc, 162 | ), 163 | ), 164 | identity, 165 | )(state); 166 | 167 | case ARRAY_SHIFT: 168 | return ifElse( 169 | pathSatisfies(isNumber, [a.payload.form, 'arrays', a.payload.id]), 170 | compose( 171 | over( 172 | lensPath([a.payload.form, 'fields']), 173 | arrayShift(a.payload.id, 0), 174 | ), 175 | over( 176 | lensPath([a.payload.form, 'arrays', a.payload.id]), 177 | ifElse(lt(0), RarrayDec, always(0)), 178 | ), 179 | ), 180 | identity, 181 | )(state); 182 | 183 | case ARRAY_INSERT: 184 | return ifElse( 185 | pathSatisfies(isNumber, [a.payload.form, 'arrays', a.payload.id]), 186 | compose( 187 | over( 188 | lensPath([a.payload.form, 'fields']), 189 | arrayUnshift(a.payload.id, a.payload.index + 1), 190 | ), 191 | over( 192 | lensPath([a.payload.form, 'arrays', a.payload.id]), 193 | RarrayInc, 194 | ), 195 | ), 196 | identity, 197 | )(state); 198 | 199 | case ARRAY_REMOVE: 200 | return ifElse( 201 | pathSatisfies(isNumber, [a.payload.form, 'arrays', a.payload.id]), 202 | compose( 203 | over( 204 | lensPath([a.payload.form, 'fields']), 205 | arrayShift(a.payload.id, a.payload.index), 206 | ), 207 | over( 208 | lensPath([a.payload.form, 'arrays', a.payload.id]), 209 | ifElse(lt(0), RarrayDec, always(0)), 210 | ), 211 | ), 212 | identity, 213 | )(state); 214 | 215 | case ARRAY_SWAP: 216 | return ifElse( 217 | pathSatisfies(isNumber, [a.payload.form, 'arrays', a.payload.id]), 218 | over( 219 | lensPath([a.payload.form, 'fields']), 220 | arraySwap(a.payload.id, a.payload.index1, a.payload.index2), 221 | ), 222 | identity, 223 | )(state); 224 | 225 | case ARRAY_MOVE: 226 | return ifElse( 227 | pathSatisfies(isNumber, [a.payload.form, 'arrays', a.payload.id]), 228 | over( 229 | lensPath([a.payload.form, 'fields']), 230 | arrayMove(a.payload.id, a.payload.from, a.payload.to), 231 | ), 232 | identity, 233 | )(state); 234 | 235 | // Field 236 | // --- 237 | case FIELD_CHANGE: 238 | return ifElse( 239 | pathSatisfies(Boolean, [a.payload.form, 'fields', a.payload.field]), 240 | compose( 241 | assocPath([a.payload.form, 'fields', a.payload.field, 'value'], a.payload.value), 242 | assocPath([a.payload.form, 'fields', a.payload.field, 'error'], a.payload.error), 243 | assocPath([a.payload.form, 'fields', a.payload.field, 'dirty'], a.payload.dirty), 244 | ), 245 | identity, 246 | )(state); 247 | 248 | case FIELD_FOCUS: 249 | return ifElse( 250 | pathSatisfies(Boolean, [a.payload.form, 'fields', a.payload.field]), 251 | compose( 252 | assocPath([a.payload.form, 'fields', a.payload.field, 'active'], true), 253 | assocPath([a.payload.form, 'fields', a.payload.field, 'visited'], true), 254 | ), 255 | identity, 256 | )(state); 257 | 258 | case FIELD_BLUR: 259 | return ifElse( 260 | pathSatisfies(Boolean, [a.payload.form, 'fields', a.payload.field]), 261 | compose( 262 | assocPath([a.payload.form, 'fields', a.payload.field, 'value'], a.payload.value), 263 | assocPath([a.payload.form, 'fields', a.payload.field, 'error'], a.payload.error), 264 | assocPath([a.payload.form, 'fields', a.payload.field, 'dirty'], a.payload.dirty), 265 | assocPath([a.payload.form, 'fields', a.payload.field, 'active'], false), 266 | assocPath([a.payload.form, 'fields', a.payload.field, 'touched'], true), 267 | ), 268 | identity, 269 | )(state); 270 | 271 | case FIELD_VALUE: 272 | return ifElse( 273 | pathSatisfies(Boolean, [a.payload.form, 'fields', a.payload.field]), 274 | assocPath([a.payload.form, 'fields', a.payload.field, 'value'], a.payload.value), 275 | identity, 276 | )(state); 277 | 278 | case FIELD_ERROR: 279 | return ifElse( 280 | pathSatisfies(Boolean, [a.payload.form, 'fields', a.payload.field]), 281 | assocPath([a.payload.form, 'fields', a.payload.field, 'error'], a.payload.error), 282 | identity, 283 | )(state); 284 | 285 | case FIELD_DIRTY: 286 | return ifElse( 287 | pathSatisfies(Boolean, [a.payload.form, 'fields', a.payload.field]), 288 | assocPath([a.payload.form, 'fields', a.payload.field, 'dirty'], a.payload.dirty), 289 | identity, 290 | )(state); 291 | 292 | default: 293 | return state; 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /packages/redux-forms/src/selectors.ts: -------------------------------------------------------------------------------- 1 | import { 2 | memoize, 3 | compose, 4 | map, 5 | prop, 6 | path, 7 | values, 8 | none, 9 | any, 10 | identity, 11 | } from 'ramda'; 12 | 13 | import { State } from './reducer'; 14 | import { Form } from './containers'; 15 | import { unflatten } from './shared/helpers'; 16 | 17 | 18 | export type Values = { [key: string]: any | any[] | Values[] }; 19 | export type Error = string | null; 20 | export type Errors = { [key: string]: Error | Error[] | Errors[] }; 21 | 22 | export interface IState { 23 | reduxForms: State; 24 | } 25 | 26 | type Memoize = (x: T[]) => T; 27 | 28 | 29 | const EMPTY = {}; 30 | 31 | const memUnflatten = memoize(unflatten) as Memoize<{}>; 32 | 33 | const memValue = memoize(compose( 34 | memUnflatten, 35 | map(prop('value')), 36 | )); 37 | 38 | export function getValues(name: string, state: IState): Values { 39 | const form = path
(['reduxForms', name], state); 40 | if (!form) { 41 | return EMPTY; 42 | } 43 | 44 | return memValue(form.fields); 45 | } 46 | 47 | 48 | const memError = memoize(compose( 49 | memUnflatten, 50 | map(prop('error')), 51 | )); 52 | 53 | export function getErrors(name: string, state: IState): Errors { 54 | const form = path(['reduxForms', name], state); 55 | if (!form) { 56 | return EMPTY; 57 | } 58 | 59 | return memError(form.fields); 60 | } 61 | 62 | 63 | const memValues = memoize(values) as Memoize<{}>; 64 | 65 | const memValid = memoize(compose( 66 | none(Boolean), 67 | memValues, 68 | map(prop('error')), 69 | )); 70 | 71 | export function isValid(name: string, state: IState): boolean { 72 | const form = path(['reduxForms', name], state); 73 | if (!form) { 74 | return false; 75 | } 76 | 77 | return memValid(form.fields); 78 | } 79 | 80 | 81 | const memTouched = memoize(compose( 82 | any(Boolean), 83 | memValues, 84 | map(prop('touched')), 85 | )); 86 | 87 | export function isTouched(name: string, state: IState): boolean { 88 | const form = path(['reduxForms', name], state); 89 | if (!form) { 90 | return false; 91 | } 92 | 93 | return memTouched(form.fields); 94 | } 95 | 96 | 97 | const memDirty = memoize(compose( 98 | any(Boolean), 99 | memValues, 100 | map(prop('dirty')), 101 | )); 102 | 103 | export function isDirty(name: string, state: IState): boolean { 104 | const form = path(['reduxForms', name], state); 105 | if (!form) { 106 | return false; 107 | } 108 | 109 | return memDirty(form.fields); 110 | } 111 | 112 | 113 | export function isSubmitting(name: string, state: IState): boolean { 114 | const form = path(['reduxForms', name], state); 115 | if (!form) { 116 | return false; 117 | } 118 | 119 | return form.submitting; 120 | } 121 | -------------------------------------------------------------------------------- /packages/redux-forms/src/shared/__tests__/fieldArrayProps.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as R from "ramda"; 3 | 4 | import fieldArrayProps from '../fieldArrayProps'; 5 | 6 | 7 | const props = { 8 | _form: null, 9 | _array: null, 10 | _addArray: null, 11 | _arrayPush: null, 12 | _arrayPop: null, 13 | _arrayUnshift: null, 14 | _arrayShift: null, 15 | _arrayInsert: null, 16 | _arrayRemove: null, 17 | _arraySwap: null, 18 | _arrayMove: null, 19 | lol: 'kek', 20 | fields: {}, 21 | }; 22 | 23 | describe('#fieldArrayProps', () => { 24 | it('should filter props', () => { 25 | const result: any = fieldArrayProps(props); 26 | 27 | expect(result).toEqual({ lol: 'kek', fields: {} }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/redux-forms/src/shared/__tests__/fieldProps.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as R from "ramda"; 3 | 4 | import fieldProps, { boolField } from '../fieldProps'; 5 | 6 | 7 | const onChange = R.identity; 8 | const onFocus = R.identity; 9 | const onBlur = R.identity; 10 | 11 | const props = { 12 | // input 13 | // --- 14 | value: '1337', 15 | checked: false, 16 | name: 'fieldz', 17 | onChange, 18 | onFocus, 19 | onBlur, 20 | 21 | // meta 22 | // --- 23 | error: 'not enough peanuts', 24 | dirty: false, 25 | visited: false, 26 | touched: true, 27 | active: false, 28 | 29 | // field 30 | // --- 31 | _field: {}, 32 | 33 | // custom 34 | // --- 35 | kek: 'bur', 36 | }; 37 | 38 | const props2 = { 39 | ...props, 40 | value: true, 41 | }; 42 | 43 | describe('#fieldProps', () => { 44 | it('should separate input props', () => { 45 | const result: any = fieldProps(props); 46 | 47 | expect(result.input.value).toBe('1337'); 48 | expect(result.input.checked).toBe(false); 49 | expect(result.input.name).toBe('fieldz'); 50 | expect(result.input.onChange).toBeDefined(); 51 | expect(result.input.onFocus).toBeDefined(); 52 | expect(result.input.onBlur).toBeDefined(); 53 | }); 54 | 55 | it('should separate meta props', () => { 56 | const result: any = fieldProps(props); 57 | 58 | expect(result.meta.error).toBe('not enough peanuts'); 59 | expect(result.meta.dirty).toBe(false); 60 | expect(result.meta.visited).toBe(false); 61 | expect(result.meta.touched).toBe(true); 62 | expect(result.meta.active).toBe(false); 63 | }); 64 | 65 | it('should add a "checked" prop for boolean value', () => { 66 | const result: any = fieldProps(props2); 67 | 68 | expect(result.input.checked).toBe(true); 69 | }); 70 | 71 | it('should turn "_field" prop to a boolean', () => { 72 | expect(boolField(props)).toEqual({ 73 | ...props, 74 | _field: true, 75 | }); 76 | }); 77 | 78 | it('should keep custom props', () => { 79 | const result: any = fieldProps(props); 80 | 81 | expect(result.rest.kek).toBe('bur'); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /packages/redux-forms/src/shared/__tests__/formProps.spec.ts: -------------------------------------------------------------------------------- 1 | import * as R from "ramda"; 2 | 3 | import formProps, { toUpdate } from '../formProps'; 4 | 5 | 6 | const props = { 7 | // to omit 8 | // --- 9 | name: 'form', 10 | persistent: true, 11 | withRef: R.identity, 12 | // state 13 | _form: {}, 14 | _values: {}, 15 | _valid: false, 16 | _submitting: false, 17 | // actions 18 | _addForm: R.identity, 19 | _removeForm: R.identity, 20 | _touchAll: R.identity, 21 | _submitStart: R.identity, 22 | _submitStop: R.identity, 23 | 24 | // custom 25 | // --- 26 | damage: 'tons of', 27 | wow: 'so test', 28 | }; 29 | 30 | const props2 = { 31 | ...props, 32 | value: true, 33 | }; 34 | 35 | describe('#fieldProps', () => { 36 | it('should omit props', () => { 37 | const result = formProps(props); 38 | 39 | expect(result.name).toBeUndefined(); 40 | expect(result.persistent).toBeUndefined(); 41 | expect(result.withRef).toBeUndefined(); 42 | expect(result._form).toBeUndefined(); 43 | expect(result._values).toBeUndefined(); 44 | expect(result._valid).toBeUndefined(); 45 | expect(result._submitting).toBeUndefined(); 46 | expect(result._addForm).toBeUndefined(); 47 | expect(result._removeForm).toBeUndefined(); 48 | expect(result._touchAll).toBeUndefined(); 49 | expect(result._submitStart).toBeUndefined(); 50 | expect(result._submitStop).toBeUndefined(); 51 | }); 52 | 53 | it('should keep custom props', () => { 54 | const result = formProps(props); 55 | 56 | expect(result.damage).toBe('tons of'); 57 | expect(result.wow).toBe('so test'); 58 | }); 59 | 60 | it('should omit props not to update for', () => { 61 | const result = toUpdate(props); 62 | 63 | expect(result._values).toBeUndefined(); 64 | expect(result._valid).toBeUndefined(); 65 | expect(result._submitting).toBeUndefined(); 66 | 67 | expect(result.name).toBeDefined(); 68 | expect(result.persistent).toBeDefined(); 69 | expect(result.withRef).toBeDefined(); 70 | expect(result._form).toBeDefined(); 71 | expect(result._addForm).toBeDefined(); 72 | expect(result._removeForm).toBeDefined(); 73 | expect(result._touchAll).toBeDefined(); 74 | expect(result._submitStart).toBeDefined(); 75 | expect(result._submitStop).toBeDefined(); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /packages/redux-forms/src/shared/__tests__/getValue.spec.ts: -------------------------------------------------------------------------------- 1 | import getValue from '../getValue'; 2 | 3 | const preventDefault = (id: any) => id; 4 | const stopPropagation = (id: any) => id; 5 | 6 | const evValue: any = (value: any) => ({ 7 | preventDefault, 8 | stopPropagation, 9 | target: { 10 | type: 'text', 11 | value, 12 | }, 13 | }); 14 | 15 | const evChecked: any = (checked: boolean) => ({ 16 | preventDefault, 17 | stopPropagation, 18 | target: { 19 | type: 'checkbox', 20 | checked, 21 | }, 22 | }); 23 | 24 | const evFiles: any = (files: string[]) => ({ 25 | preventDefault, 26 | stopPropagation, 27 | target: { 28 | type: 'file', 29 | files, 30 | }, 31 | }); 32 | 33 | type Option = { selected: boolean, value: string }; 34 | const evSelect: any = (options: Option[]) => ({ 35 | preventDefault, 36 | stopPropagation, 37 | target: { 38 | type: 'select-multiple', 39 | options, 40 | }, 41 | }); 42 | 43 | describe('#getValue', () => { 44 | it('should return value for non-event values', () => { 45 | expect(getValue(null)).toBeNull(); 46 | expect(getValue('kek')).toBe('kek'); 47 | expect(getValue(true)).toBe(true); 48 | expect(getValue(false)).toBe(false); 49 | }); 50 | 51 | it('should return value for value', () => { 52 | expect(getValue(evValue(null))).toBeNull(); 53 | expect(getValue(evValue(undefined))).toBeUndefined(); 54 | expect(getValue(evValue(1337))).toBe(1337); 55 | expect(getValue(evValue('y u do dis'))).toBe('y u do dis'); 56 | }); 57 | 58 | it('should return checked for checkbox', () => { 59 | expect(getValue(evChecked(true))).toBe(true); 60 | expect(getValue(evChecked(false))).toBe(false); 61 | }); 62 | 63 | it('should return files for files', () => { 64 | const files = ['lol', 'kek', 'bur']; 65 | expect(getValue(evFiles(files))).toEqual(files); 66 | }); 67 | 68 | it('should return options for select-multiple', () => { 69 | const options = [ 70 | { selected: false, value: 'lol' }, 71 | { selected: true, value: 'kek' }, 72 | { selected: false, value: 'bur' }, 73 | ]; 74 | expect(getValue(evSelect(options))).toEqual(['kek']); 75 | expect(getValue(evSelect([]))).toEqual([]); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /packages/redux-forms/src/shared/__tests__/helpers.spec.ts: -------------------------------------------------------------------------------- 1 | import * as R from 'ramda'; 2 | 3 | import * as helpers from '../helpers'; 4 | 5 | 6 | const event = { 7 | preventDefault: R.identity, 8 | stopPropagation: R.identity, 9 | }; 10 | 11 | const fields = { 12 | 'name': 'John', 13 | 'nicknames.0': 'johnny', 14 | 'nicknames.1': 'leet', 15 | 'pets.0.foods.0.calories': 133, 16 | 'pets.0.foods.1.calories': 337, 17 | }; 18 | 19 | 20 | describe('#helpers', () => { 21 | it('should recognize a string', () => { 22 | expect(helpers.isString('')).toBe(true); 23 | expect(helpers.isString('adsf')).toBe(true); 24 | expect(helpers.isString('1234')).toBe(true); 25 | }); 26 | 27 | it('should not recognize a string', () => { 28 | expect(helpers.isString(undefined)).toBe(false); 29 | expect(helpers.isString(null)).toBe(false); 30 | expect(helpers.isString(1234)).toBe(false); 31 | expect(helpers.isString({})).toBe(false); 32 | expect(helpers.isString([])).toBe(false); 33 | }); 34 | 35 | it('should recognize a number', () => { 36 | expect(helpers.isNumber(1234)).toBe(true); 37 | expect(helpers.isNumber(13.37)).toBe(true); 38 | }); 39 | 40 | it('should not recognize a number', () => { 41 | expect(helpers.isNumber(undefined)).toBe(false); 42 | expect(helpers.isNumber(null)).toBe(false); 43 | expect(helpers.isNumber('1234')).toBe(false); 44 | expect(helpers.isNumber({})).toBe(false); 45 | expect(helpers.isNumber([])).toBe(false); 46 | }); 47 | 48 | it('should recognize a promise', () => { 49 | expect(helpers.isPromise(new Promise((resolve) => resolve()))).toBe(true); 50 | expect(helpers.isPromise({ then: () => null })).toBe(true); 51 | }); 52 | 53 | it('should not recognize a promise', () => { 54 | expect(helpers.isPromise(undefined)).toBe(false); 55 | expect(helpers.isPromise(null)).toBe(false); 56 | expect(helpers.isPromise(1234)).toBe(false); 57 | expect(helpers.isPromise('asdf')).toBe(false); 58 | expect(helpers.isPromise({})).toBe(false); 59 | expect(helpers.isPromise([])).toBe(false); 60 | }); 61 | 62 | it('should recognize a function', () => { 63 | expect(helpers.isFunction(R.identity)).toBe(true); 64 | expect(helpers.isFunction(() => null)).toBe(true); 65 | }); 66 | 67 | it('should not recognize a function', () => { 68 | expect(helpers.isFunction(undefined)).toBe(false); 69 | expect(helpers.isFunction(null)).toBe(false); 70 | expect(helpers.isFunction(1234)).toBe(false); 71 | expect(helpers.isFunction('asdf')).toBe(false); 72 | expect(helpers.isFunction({})).toBe(false); 73 | expect(helpers.isFunction([])).toBe(false); 74 | }); 75 | 76 | it('should recognize an event', () => { 77 | expect(helpers.isEvent(event)).toBe(true); 78 | }); 79 | 80 | it('should not recognize an event', () => { 81 | expect(helpers.isEvent(undefined)).toBe(false); 82 | expect(helpers.isEvent(null)).toBe(false); 83 | expect(helpers.isEvent(1234)).toBe(false); 84 | expect(helpers.isEvent('asdf')).toBe(false); 85 | expect(helpers.isEvent({})).toBe(false); 86 | expect(helpers.isEvent([])).toBe(false); 87 | }); 88 | 89 | it('should compare objects', () => { 90 | const props1 = { lol: 'rofl', kek: 1337 }; 91 | const props2 = { lol: 'rofl', kek: 1337 }; 92 | 93 | expect(helpers.shallowCompare(props1, props1)).toBe(true); 94 | expect(helpers.shallowCompare(props1, props2)).toBe(true); 95 | }); 96 | 97 | it('should not compare objects', () => { 98 | const propsKeys1 = { lol: 'rofl', kek: 1337 }; 99 | const propsKeys2 = { lol: 'rofl' }; 100 | 101 | const propsValues1 = { kek: 1337, lol: 'rofl' }; 102 | const propsValues2 = { kek: 1336, lol: 'rofl' }; 103 | 104 | const propsId1 = { lol: 'rofl', kek: [] }; 105 | const propsId2 = { lol: 'rofl', kek: [] }; 106 | 107 | expect(helpers.shallowCompare(propsKeys1, propsKeys2)).toBe(false); 108 | expect(helpers.shallowCompare(propsValues1, propsValues2)).toBe(false); 109 | expect(helpers.shallowCompare(propsId1, propsId2)).toBe(false); 110 | }); 111 | 112 | it('should unflatten an object', () => { 113 | expect(helpers.unflatten(fields)).toEqual({ 114 | name: 'John', 115 | nicknames: ['johnny', 'leet'], 116 | pets: [{ 117 | foods: [{ 118 | calories: 133, 119 | }, { 120 | calories: 337, 121 | }], 122 | }], 123 | }); 124 | }); 125 | 126 | it('should not throw if ok', () => { 127 | expect(() => helpers.invariant(true, 'asdf')).not.toThrow(); 128 | }); 129 | 130 | it('should throw if not ok', () => { 131 | expect(() => helpers.invariant(false, 'asdf')).toThrowError(/asdf/); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /packages/redux-forms/src/shared/fieldArrayProps.ts: -------------------------------------------------------------------------------- 1 | import { 2 | omit, 3 | } from 'ramda'; 4 | 5 | const IGNORE_PROPS = [ 6 | '_form', 7 | '_array', 8 | '_addArray', 9 | '_arrayPush', 10 | '_arrayPop', 11 | '_arrayUnshift', 12 | '_arrayShift', 13 | '_arrayInsert', 14 | '_arrayRemove', 15 | '_arraySwap', 16 | '_arrayMove', 17 | ]; 18 | 19 | const clearProps = (all: T): T => omit(IGNORE_PROPS, all); 20 | 21 | export default clearProps; 22 | -------------------------------------------------------------------------------- /packages/redux-forms/src/shared/fieldProps.ts: -------------------------------------------------------------------------------- 1 | import { 2 | merge, 3 | pick, 4 | compose, 5 | over, 6 | lensProp, 7 | omit, 8 | } from 'ramda'; 9 | 10 | import { Target } from './getValue'; 11 | 12 | 13 | export type InputProps = { 14 | name: string, 15 | value: any, 16 | checked?: boolean, 17 | onChange: (ev: React.SyntheticEvent) => void, 18 | onFocus: (ev: React.SyntheticEvent) => void, 19 | onBlur: (ev: React.SyntheticEvent) => void, 20 | }; 21 | 22 | export type MetaProps = { 23 | error: string | null, 24 | dirty: boolean, 25 | touched: boolean, 26 | visited: boolean, 27 | active: boolean, 28 | }; 29 | 30 | export type All = T & InputProps & MetaProps; 31 | 32 | export type SeparatedProps = { 33 | input: InputProps, 34 | meta: MetaProps, 35 | rest: T, 36 | }; 37 | 38 | 39 | const INPUT_PROPS = [ 40 | 'checked', 41 | 'name', 42 | 'value', 43 | 'onChange', 44 | 'onFocus', 45 | 'onBlur', 46 | ]; 47 | 48 | export type InputProp = 49 | | 'checked' 50 | | 'name' 51 | | 'value' 52 | | 'onChange' 53 | | 'onFocus' 54 | | 'onBlur'; 55 | 56 | const META_PROPS = [ 57 | 'active', 58 | 'dirty', 59 | 'error', 60 | 'touched', 61 | 'visited', 62 | ]; 63 | 64 | export type MetaProp = 65 | | 'active' 66 | | 'dirty' 67 | | 'error' 68 | | 'touched' 69 | | 'visited'; 70 | 71 | const IGNORE_PROPS = [ 72 | ...INPUT_PROPS, 73 | ...META_PROPS, 74 | 'validate', 75 | 'normalize', 76 | 'defaultValue', 77 | '_form', 78 | '_addField', 79 | '_fieldChange', 80 | '_fieldFocus', 81 | '_fieldBlur', 82 | ]; 83 | 84 | 85 | const maybeCheckProps = (all: All): All => { 86 | if (typeof all.value === 'boolean') { 87 | return merge(all, { checked: all.value }); 88 | } 89 | return all; 90 | }; 91 | 92 | const separateProps = (all: All): SeparatedProps => ({ 93 | input: pick, InputProp>(INPUT_PROPS, all), 94 | meta: pick, MetaProp>(META_PROPS, all), 95 | rest: omit(IGNORE_PROPS, all), 96 | }); 97 | 98 | export default (all: All) => separateProps(maybeCheckProps(all)); 99 | 100 | 101 | export const boolField = over(lensProp('_field'), Boolean); 102 | -------------------------------------------------------------------------------- /packages/redux-forms/src/shared/formProps.ts: -------------------------------------------------------------------------------- 1 | import { 2 | omit, 3 | } from 'ramda'; 4 | 5 | 6 | export type Omitted = { 7 | name?: any, 8 | persistent?: any, 9 | withRef?: any, 10 | // state 11 | _form?: any, 12 | _values?: any, 13 | _valid?: any, 14 | _submitting?: any, 15 | // actions 16 | _addForm?: any, 17 | _removeForm?: any, 18 | _touchAll?: any, 19 | _submitStart?: any, 20 | _submitStop?: any, 21 | }; 22 | 23 | const FORM_PROPS = [ 24 | 'name', 25 | 'persistent', 26 | 'withRef', 27 | // state 28 | '_form', 29 | '_values', 30 | '_valid', 31 | '_submitting', 32 | // actions 33 | '_addForm', 34 | '_removeForm', 35 | '_touchAll', 36 | '_submitStart', 37 | '_submitStop', 38 | ]; 39 | 40 | const formProps = (props: Omitted & T): T => omit(FORM_PROPS, props); 41 | 42 | export default formProps; 43 | 44 | 45 | export type NotUpdated = { 46 | _values?: any, 47 | _valid?: any, 48 | _submitting?: any, 49 | }; 50 | 51 | const NOT_TO_UPDATE = [ 52 | '_values', 53 | '_valid', 54 | '_submitting', 55 | ]; 56 | 57 | export const toUpdate = (all: T & NotUpdated): T => omit(NOT_TO_UPDATE, all); 58 | -------------------------------------------------------------------------------- /packages/redux-forms/src/shared/getValue.ts: -------------------------------------------------------------------------------- 1 | import { isEvent } from './helpers'; 2 | 3 | 4 | export type Target = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; 5 | 6 | 7 | const getSelectedValues = (options: HTMLOptionsCollection): string[] => Array.from(options) 8 | .filter((option) => option.selected) 9 | .map((option) => option.value); 10 | 11 | const getValue = (ev: React.SyntheticEvent | any): any => { 12 | if (!isEvent(ev)) { 13 | return ev; 14 | } 15 | 16 | const target = ev.target as Target; 17 | 18 | switch (target.type) { 19 | case 'checkbox': 20 | return (target as HTMLInputElement).checked; 21 | case 'file': 22 | return (target as HTMLInputElement).files; 23 | case 'select-multiple': 24 | return getSelectedValues((target as HTMLSelectElement).options); 25 | default: 26 | return target.value; 27 | } 28 | }; 29 | 30 | export default getValue; 31 | -------------------------------------------------------------------------------- /packages/redux-forms/src/shared/helpers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | compose, 3 | keys, 4 | reduce, 5 | prop, 6 | length, 7 | } from 'ramda'; 8 | 9 | 10 | export function isString(cand: any): cand is string { 11 | return typeof cand === 'string'; 12 | } 13 | 14 | export function isNumber(cand: any): cand is number { 15 | return typeof cand === 'number'; 16 | } 17 | 18 | export function isPromise(cand: any): cand is PromiseLike { 19 | return Boolean(cand) && typeof cand === 'object' && typeof cand.then === 'function'; 20 | } 21 | 22 | export function isFunction(cand: any): cand is Function { // tslint:disable-line ban-types 23 | return typeof cand === 'function'; 24 | } 25 | 26 | export function isEvent(cand: any): cand is React.SyntheticEvent { 27 | return Boolean( 28 | cand && 29 | typeof cand === 'object' && 30 | isFunction(cand.preventDefault) && 31 | isFunction(cand.stopPropagation), 32 | ); 33 | } 34 | 35 | 36 | export type Props = { [key: string]: any }; 37 | 38 | const keyCount = compose(length, keys); 39 | 40 | export function shallowCompare(props1: Props, props2: Props): boolean { 41 | if (props1 === props2) { 42 | return true; 43 | } 44 | 45 | if (keyCount(props1) !== keyCount(props2)) { 46 | return false; 47 | } 48 | 49 | return reduce((acc, key) => 50 | acc && prop(key, props1) === prop(key, props2), true, keys(props1)); 51 | } 52 | 53 | 54 | export type Flat = { [key: string]: any }; 55 | 56 | // FIXME: ugly code 57 | // A rewrite would be welcome. 58 | export function unflatten(obj: Flat) { 59 | const result = {}; 60 | 61 | Object.keys(obj) 62 | .forEach((propp) => propp.split('.') 63 | .reduce((acc: any, key, index, array) => { 64 | const k = isNaN(Number(key)) ? key : Number(key); 65 | 66 | if (index === array.length - 1) { 67 | return acc[k] = obj[propp]; 68 | } 69 | 70 | if (acc[k]) { 71 | return acc[k] = acc[k]; 72 | } 73 | 74 | if (!isNaN(Number(array[index + 1]))) { 75 | return acc[k] = []; 76 | } 77 | 78 | return acc[k] = {}; 79 | }, result)); 80 | 81 | return result; 82 | } 83 | 84 | export function invariant(cond: boolean, msg: string) { 85 | if (cond) { 86 | return; 87 | } 88 | 89 | const error = new Error(msg); 90 | 91 | error.name = 'Invariant violation'; 92 | 93 | throw error; 94 | } 95 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib/", 4 | "module": "es6", 5 | "target": "es6", 6 | "jsx": "react", 7 | "declaration": false, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "allowSyntheticDefaultImports": true 11 | }, 12 | "include": [ 13 | "types/" 14 | ], 15 | "exclude": [ 16 | "example", 17 | "node_modules" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-react"], 3 | "rules": { 4 | "interface-over-type-literal": false, 5 | "jsx-boolean-value": false, 6 | "jsx-no-multiline-js": false, 7 | "member-access": false, 8 | "no-consecutive-blank-lines": false, 9 | "no-implicit-dependencies": false, 10 | "no-submodule-imports": false, 11 | "object-literal-sort-keys": false, 12 | "ordered-imports": false, 13 | "quotemark": ["single", "jsx-double"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /types/prop-types.d.ts: -------------------------------------------------------------------------------- 1 | // TODO install @types/prop-types once it's fixed 2 | declare module 'prop-types' { 3 | const PropTypes: any; 4 | 5 | export = PropTypes; 6 | } 7 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const webpack = require('webpack'); 3 | const R = require('ramda'); 4 | 5 | const packages = require('./webpack.packages.js'); 6 | 7 | 8 | const env = process.env.NODE_ENV; 9 | 10 | const config = { 11 | // package.entry 12 | // package.externals 13 | module: { 14 | rules: [ 15 | { test: /\.tsx?$/, use: ['babel-loader', 'ts-loader'], exclude: /node_modules/ }, 16 | ], 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.ts', '.tsx'], 20 | }, 21 | output: { 22 | // package.outputPath 23 | // package.outputFilename 24 | // package.outputLibrary 25 | libraryTarget: 'umd', 26 | }, 27 | plugins: [ 28 | new webpack.DefinePlugin({ 29 | 'process.env.NODE_ENV': JSON.stringify(env), 30 | }), 31 | ], 32 | }; 33 | 34 | if (env === 'production') { 35 | config.plugins.push( 36 | new webpack.optimize.UglifyJsPlugin({ 37 | compressor: { 38 | pure_getters: true, 39 | unsafe: true, 40 | unsafe_comps: true, 41 | warnings: false, 42 | }, 43 | }) // eslint-disable-line comma-dangle 44 | ); 45 | } 46 | 47 | module.exports = R.map((pkg) => R.compose( 48 | R.assoc('entry', pkg.entry), 49 | R.assoc('externals', pkg.externals), 50 | R.assocPath(['output', 'path'], pkg.outputPath), 51 | R.assocPath(['output', 'filename'], pkg.outputFilename), 52 | R.assocPath(['output', 'library'], pkg.outputLibrary) 53 | )(config), packages); 54 | -------------------------------------------------------------------------------- /webpack.packages.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | 4 | const production = process.env.NODE_ENV === 'production'; 5 | 6 | const reactExternal = { 7 | root: 'React', 8 | commonjs2: 'react', 9 | commonjs: 'react', 10 | amd: 'react', 11 | }; 12 | 13 | const reduxExternal = { 14 | root: 'Redux', 15 | commonjs2: 'redux', 16 | commonjs: 'redux', 17 | amd: 'redux' 18 | }; 19 | 20 | const reactReduxExternal = { 21 | root: 'ReactRedux', 22 | commonjs2: 'react-redux', 23 | commonjs: 'react-redux', 24 | amd: 'react-redux' 25 | }; 26 | 27 | 28 | const ext = production ? '.min.js' : '.js'; 29 | 30 | 31 | module.exports = [{ 32 | entry: path.join(__dirname, 'packages/redux-forms/src/index.ts'), 33 | outputPath: path.join(__dirname, 'packages/redux-forms/dist'), 34 | outputFilename: 'redux-forms' + ext, 35 | outputLibrary: 'ReduxForms', 36 | externals: {}, 37 | }, { 38 | entry: path.join(__dirname, 'packages/redux-forms-react/src/index.ts'), 39 | outputPath: path.join(__dirname, 'packages/redux-forms-react/dist'), 40 | outputFilename: 'redux-forms-react' + ext, 41 | outputLibrary: 'ReduxFormsReact', 42 | externals: { 43 | 'react': reactExternal, 44 | 'redux': reduxExternal, 45 | 'react-redux': reactReduxExternal, 46 | }, 47 | }]; 48 | --------------------------------------------------------------------------------