├── .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 | [](https://travis-ci.org/oreqizer/redux-forms)
4 | [](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` [](https://www.npmjs.com/package/redux-forms)
11 | * `redux-forms-react` [](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 |
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 |
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 |
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 |
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 |
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 |
58 | ));
59 |
60 | const validate = value => value.length < 5 ? 'too short' : null;
61 |
62 | const MyForm = props => (
63 |
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 | [](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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 | [](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