├── .npmrc
├── packages
├── redux-forms
│ ├── actions.d.ts
│ ├── selectors.d.ts
│ ├── actions.js
│ ├── selectors.js
│ ├── src
│ │ ├── index.ts
│ │ ├── shared
│ │ │ ├── fieldArrayProps.ts
│ │ │ ├── __tests__
│ │ │ │ ├── fieldArrayProps.spec.ts
│ │ │ │ ├── fieldProps.spec.ts
│ │ │ │ ├── getValue.spec.ts
│ │ │ │ ├── formProps.spec.ts
│ │ │ │ └── helpers.spec.ts
│ │ │ ├── getValue.ts
│ │ │ ├── formProps.ts
│ │ │ ├── fieldProps.ts
│ │ │ └── helpers.ts
│ │ ├── containers.ts
│ │ ├── selectors.ts
│ │ ├── arrays.ts
│ │ ├── __tests__
│ │ │ ├── actions.spec.ts
│ │ │ ├── selectors.spec.ts
│ │ │ ├── arrays.spec.ts
│ │ │ └── reducer.spec.ts
│ │ ├── reducer.ts
│ │ └── actions.ts
│ ├── README.md
│ └── package.json
└── redux-forms-react
│ ├── src
│ ├── index.ts
│ ├── connectField.ts
│ ├── __tests__
│ │ ├── connectField.spec.tsx
│ │ ├── Form.spec.tsx
│ │ ├── fieldArray.spec.tsx
│ │ └── field.spec.tsx
│ ├── Form.ts
│ ├── fieldArray.ts
│ └── field.ts
│ ├── README.md
│ ├── package.json
│ └── __tests__
│ └── integration.spec.tsx
├── .babelrc
├── .gitignore
├── etc
└── enzymeSetup.js
├── types
└── prop-types.d.ts
├── lerna.json
├── .travis.yml
├── example
├── index.html
├── webpack.config.js
├── src
│ ├── Input.jsx
│ └── MyForm.jsx
├── package.json
└── index.jsx
├── CONTRIBUTING.md
├── tsconfig.json
├── tslint.json
├── LICENSE
├── webpack.packages.js
├── webpack.config.js
├── gulpfile.js
├── package.json
└── README.md
/.npmrc:
--------------------------------------------------------------------------------
1 | git-tag-version = true
2 |
--------------------------------------------------------------------------------
/packages/redux-forms/actions.d.ts:
--------------------------------------------------------------------------------
1 | export * from './lib/actions';
2 |
--------------------------------------------------------------------------------
/packages/redux-forms/selectors.d.ts:
--------------------------------------------------------------------------------
1 | export * from './lib/selectors';
2 |
--------------------------------------------------------------------------------
/packages/redux-forms/actions.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/actions');
2 |
--------------------------------------------------------------------------------
/packages/redux-forms/selectors.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/selectors');
2 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/etc/enzymeSetup.js:
--------------------------------------------------------------------------------
1 | import { configure } from "enzyme";
2 | import Adapter from "enzyme-adapter-react-16";
3 |
4 | configure({ adapter: new Adapter() });
5 |
--------------------------------------------------------------------------------
/types/prop-types.d.ts:
--------------------------------------------------------------------------------
1 | // TODO install @types/prop-types once it's fixed
2 | declare module 'prop-types' {
3 | const PropTypes: any;
4 |
5 | export = PropTypes;
6 | }
7 |
--------------------------------------------------------------------------------
/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-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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | redux-forms
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "lib/",
4 | "module": "es6",
5 | "target": "es6",
6 | "jsx": "react",
7 | "declaration": false,
8 | "noImplicitAny": true,
9 | "strictNullChecks": true,
10 | "allowSyntheticDefaultImports": true
11 | },
12 | "include": [
13 | "types/"
14 | ],
15 | "exclude": [
16 | "example",
17 | "node_modules"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/packages/redux-forms/src/shared/fieldArrayProps.ts:
--------------------------------------------------------------------------------
1 | import {
2 | omit,
3 | } from 'ramda';
4 |
5 | const IGNORE_PROPS = [
6 | '_form',
7 | '_array',
8 | '_addArray',
9 | '_arrayPush',
10 | '_arrayPop',
11 | '_arrayUnshift',
12 | '_arrayShift',
13 | '_arrayInsert',
14 | '_arrayRemove',
15 | '_arraySwap',
16 | '_arrayMove',
17 | ];
18 |
19 | const clearProps = (all: T): T => omit(IGNORE_PROPS, all);
20 |
21 | export default clearProps;
22 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:latest", "tslint-react"],
3 | "rules": {
4 | "interface-over-type-literal": false,
5 | "jsx-boolean-value": false,
6 | "jsx-no-multiline-js": false,
7 | "member-access": false,
8 | "no-consecutive-blank-lines": false,
9 | "no-implicit-dependencies": false,
10 | "no-submodule-imports": false,
11 | "object-literal-sort-keys": false,
12 | "ordered-imports": false,
13 | "quotemark": ["single", "jsx-double"]
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/shared/__tests__/fieldArrayProps.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import * as R from "ramda";
3 |
4 | import fieldArrayProps from '../fieldArrayProps';
5 |
6 |
7 | const props = {
8 | _form: null,
9 | _array: null,
10 | _addArray: null,
11 | _arrayPush: null,
12 | _arrayPop: null,
13 | _arrayUnshift: null,
14 | _arrayShift: null,
15 | _arrayInsert: null,
16 | _arrayRemove: null,
17 | _arraySwap: null,
18 | _arrayMove: null,
19 | lol: 'kek',
20 | fields: {},
21 | };
22 |
23 | describe('#fieldArrayProps', () => {
24 | it('should filter props', () => {
25 | const result: any = fieldArrayProps(props);
26 |
27 | expect(result).toEqual({ lol: 'kek', fields: {} });
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/src/shared/getValue.ts:
--------------------------------------------------------------------------------
1 | import { isEvent } from './helpers';
2 |
3 |
4 | export type Target = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
5 |
6 |
7 | const getSelectedValues = (options: HTMLOptionsCollection): string[] => Array.from(options)
8 | .filter((option) => option.selected)
9 | .map((option) => option.value);
10 |
11 | const getValue = (ev: React.SyntheticEvent | any): any => {
12 | if (!isEvent(ev)) {
13 | return ev;
14 | }
15 |
16 | const target = ev.target as Target;
17 |
18 | switch (target.type) {
19 | case 'checkbox':
20 | return (target as HTMLInputElement).checked;
21 | case 'file':
22 | return (target as HTMLInputElement).files;
23 | case 'select-multiple':
24 | return getSelectedValues((target as HTMLSelectElement).options);
25 | default:
26 | return target.value;
27 | }
28 | };
29 |
30 | export default getValue;
31 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/packages/redux-forms/src/shared/formProps.ts:
--------------------------------------------------------------------------------
1 | import {
2 | omit,
3 | } from 'ramda';
4 |
5 |
6 | export type Omitted = {
7 | name?: any,
8 | persistent?: any,
9 | withRef?: any,
10 | // state
11 | _form?: any,
12 | _values?: any,
13 | _valid?: any,
14 | _submitting?: any,
15 | // actions
16 | _addForm?: any,
17 | _removeForm?: any,
18 | _touchAll?: any,
19 | _submitStart?: any,
20 | _submitStop?: any,
21 | };
22 |
23 | const FORM_PROPS = [
24 | 'name',
25 | 'persistent',
26 | 'withRef',
27 | // state
28 | '_form',
29 | '_values',
30 | '_valid',
31 | '_submitting',
32 | // actions
33 | '_addForm',
34 | '_removeForm',
35 | '_touchAll',
36 | '_submitStart',
37 | '_submitStop',
38 | ];
39 |
40 | const formProps = (props: Omitted & T): T => omit(FORM_PROPS, props);
41 |
42 | export default formProps;
43 |
44 |
45 | export type NotUpdated = {
46 | _values?: any,
47 | _valid?: any,
48 | _submitting?: any,
49 | };
50 |
51 | const NOT_TO_UPDATE = [
52 | '_values',
53 | '_valid',
54 | '_submitting',
55 | ];
56 |
57 | export const toUpdate = (all: T & NotUpdated): T => omit(NOT_TO_UPDATE, all);
58 |
--------------------------------------------------------------------------------
/webpack.packages.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 |
4 | const production = process.env.NODE_ENV === 'production';
5 |
6 | const reactExternal = {
7 | root: 'React',
8 | commonjs2: 'react',
9 | commonjs: 'react',
10 | amd: 'react',
11 | };
12 |
13 | const reduxExternal = {
14 | root: 'Redux',
15 | commonjs2: 'redux',
16 | commonjs: 'redux',
17 | amd: 'redux'
18 | };
19 |
20 | const reactReduxExternal = {
21 | root: 'ReactRedux',
22 | commonjs2: 'react-redux',
23 | commonjs: 'react-redux',
24 | amd: 'react-redux'
25 | };
26 |
27 |
28 | const ext = production ? '.min.js' : '.js';
29 |
30 |
31 | module.exports = [{
32 | entry: path.join(__dirname, 'packages/redux-forms/src/index.ts'),
33 | outputPath: path.join(__dirname, 'packages/redux-forms/dist'),
34 | outputFilename: 'redux-forms' + ext,
35 | outputLibrary: 'ReduxForms',
36 | externals: {},
37 | }, {
38 | entry: path.join(__dirname, 'packages/redux-forms-react/src/index.ts'),
39 | outputPath: path.join(__dirname, 'packages/redux-forms-react/dist'),
40 | outputFilename: 'redux-forms-react' + ext,
41 | outputLibrary: 'ReduxFormsReact',
42 | externals: {
43 | 'react': reactExternal,
44 | 'redux': reduxExternal,
45 | 'react-redux': reactReduxExternal,
46 | },
47 | }];
48 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | const webpack = require('webpack');
3 | const R = require('ramda');
4 |
5 | const packages = require('./webpack.packages.js');
6 |
7 |
8 | const env = process.env.NODE_ENV;
9 |
10 | const config = {
11 | // package.entry
12 | // package.externals
13 | module: {
14 | rules: [
15 | { test: /\.tsx?$/, use: ['babel-loader', 'ts-loader'], exclude: /node_modules/ },
16 | ],
17 | },
18 | resolve: {
19 | extensions: ['.js', '.ts', '.tsx'],
20 | },
21 | output: {
22 | // package.outputPath
23 | // package.outputFilename
24 | // package.outputLibrary
25 | libraryTarget: 'umd',
26 | },
27 | plugins: [
28 | new webpack.DefinePlugin({
29 | 'process.env.NODE_ENV': JSON.stringify(env),
30 | }),
31 | ],
32 | };
33 |
34 | if (env === 'production') {
35 | config.plugins.push(
36 | new webpack.optimize.UglifyJsPlugin({
37 | compressor: {
38 | pure_getters: true,
39 | unsafe: true,
40 | unsafe_comps: true,
41 | warnings: false,
42 | },
43 | }) // eslint-disable-line comma-dangle
44 | );
45 | }
46 |
47 | module.exports = R.map((pkg) => R.compose(
48 | R.assoc('entry', pkg.entry),
49 | R.assoc('externals', pkg.externals),
50 | R.assocPath(['output', 'path'], pkg.outputPath),
51 | R.assocPath(['output', 'filename'], pkg.outputFilename),
52 | R.assocPath(['output', 'library'], pkg.outputLibrary)
53 | )(config), packages);
54 |
--------------------------------------------------------------------------------
/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/__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/src/shared/__tests__/fieldProps.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import * as R from "ramda";
3 |
4 | import fieldProps, { boolField } from '../fieldProps';
5 |
6 |
7 | const onChange = R.identity;
8 | const onFocus = R.identity;
9 | const onBlur = R.identity;
10 |
11 | const props = {
12 | // input
13 | // ---
14 | value: '1337',
15 | checked: false,
16 | name: 'fieldz',
17 | onChange,
18 | onFocus,
19 | onBlur,
20 |
21 | // meta
22 | // ---
23 | error: 'not enough peanuts',
24 | dirty: false,
25 | visited: false,
26 | touched: true,
27 | active: false,
28 |
29 | // field
30 | // ---
31 | _field: {},
32 |
33 | // custom
34 | // ---
35 | kek: 'bur',
36 | };
37 |
38 | const props2 = {
39 | ...props,
40 | value: true,
41 | };
42 |
43 | describe('#fieldProps', () => {
44 | it('should separate input props', () => {
45 | const result: any = fieldProps(props);
46 |
47 | expect(result.input.value).toBe('1337');
48 | expect(result.input.checked).toBe(false);
49 | expect(result.input.name).toBe('fieldz');
50 | expect(result.input.onChange).toBeDefined();
51 | expect(result.input.onFocus).toBeDefined();
52 | expect(result.input.onBlur).toBeDefined();
53 | });
54 |
55 | it('should separate meta props', () => {
56 | const result: any = fieldProps(props);
57 |
58 | expect(result.meta.error).toBe('not enough peanuts');
59 | expect(result.meta.dirty).toBe(false);
60 | expect(result.meta.visited).toBe(false);
61 | expect(result.meta.touched).toBe(true);
62 | expect(result.meta.active).toBe(false);
63 | });
64 |
65 | it('should add a "checked" prop for boolean value', () => {
66 | const result: any = fieldProps(props2);
67 |
68 | expect(result.input.checked).toBe(true);
69 | });
70 |
71 | it('should turn "_field" prop to a boolean', () => {
72 | expect(boolField(props)).toEqual({
73 | ...props,
74 | _field: true,
75 | });
76 | });
77 |
78 | it('should keep custom props', () => {
79 | const result: any = fieldProps(props);
80 |
81 | expect(result.rest.kek).toBe('bur');
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/packages/redux-forms/src/shared/__tests__/getValue.spec.ts:
--------------------------------------------------------------------------------
1 | import getValue from '../getValue';
2 |
3 | const preventDefault = (id: any) => id;
4 | const stopPropagation = (id: any) => id;
5 |
6 | const evValue: any = (value: any) => ({
7 | preventDefault,
8 | stopPropagation,
9 | target: {
10 | type: 'text',
11 | value,
12 | },
13 | });
14 |
15 | const evChecked: any = (checked: boolean) => ({
16 | preventDefault,
17 | stopPropagation,
18 | target: {
19 | type: 'checkbox',
20 | checked,
21 | },
22 | });
23 |
24 | const evFiles: any = (files: string[]) => ({
25 | preventDefault,
26 | stopPropagation,
27 | target: {
28 | type: 'file',
29 | files,
30 | },
31 | });
32 |
33 | type Option = { selected: boolean, value: string };
34 | const evSelect: any = (options: Option[]) => ({
35 | preventDefault,
36 | stopPropagation,
37 | target: {
38 | type: 'select-multiple',
39 | options,
40 | },
41 | });
42 |
43 | describe('#getValue', () => {
44 | it('should return value for non-event values', () => {
45 | expect(getValue(null)).toBeNull();
46 | expect(getValue('kek')).toBe('kek');
47 | expect(getValue(true)).toBe(true);
48 | expect(getValue(false)).toBe(false);
49 | });
50 |
51 | it('should return value for value', () => {
52 | expect(getValue(evValue(null))).toBeNull();
53 | expect(getValue(evValue(undefined))).toBeUndefined();
54 | expect(getValue(evValue(1337))).toBe(1337);
55 | expect(getValue(evValue('y u do dis'))).toBe('y u do dis');
56 | });
57 |
58 | it('should return checked for checkbox', () => {
59 | expect(getValue(evChecked(true))).toBe(true);
60 | expect(getValue(evChecked(false))).toBe(false);
61 | });
62 |
63 | it('should return files for files', () => {
64 | const files = ['lol', 'kek', 'bur'];
65 | expect(getValue(evFiles(files))).toEqual(files);
66 | });
67 |
68 | it('should return options for select-multiple', () => {
69 | const options = [
70 | { selected: false, value: 'lol' },
71 | { selected: true, value: 'kek' },
72 | { selected: false, value: 'bur' },
73 | ];
74 | expect(getValue(evSelect(options))).toEqual(['kek']);
75 | expect(getValue(evSelect([]))).toEqual([]);
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/packages/redux-forms/src/shared/fieldProps.ts:
--------------------------------------------------------------------------------
1 | import {
2 | merge,
3 | pick,
4 | compose,
5 | over,
6 | lensProp,
7 | omit,
8 | } from 'ramda';
9 |
10 | import { Target } from './getValue';
11 |
12 |
13 | export type InputProps = {
14 | name: string,
15 | value: any,
16 | checked?: boolean,
17 | onChange: (ev: React.SyntheticEvent) => void,
18 | onFocus: (ev: React.SyntheticEvent) => void,
19 | onBlur: (ev: React.SyntheticEvent) => void,
20 | };
21 |
22 | export type MetaProps = {
23 | error: string | null,
24 | dirty: boolean,
25 | touched: boolean,
26 | visited: boolean,
27 | active: boolean,
28 | };
29 |
30 | export type All = T & InputProps & MetaProps;
31 |
32 | export type SeparatedProps = {
33 | input: InputProps,
34 | meta: MetaProps,
35 | rest: T,
36 | };
37 |
38 |
39 | const INPUT_PROPS = [
40 | 'checked',
41 | 'name',
42 | 'value',
43 | 'onChange',
44 | 'onFocus',
45 | 'onBlur',
46 | ];
47 |
48 | export type InputProp =
49 | | 'checked'
50 | | 'name'
51 | | 'value'
52 | | 'onChange'
53 | | 'onFocus'
54 | | 'onBlur';
55 |
56 | const META_PROPS = [
57 | 'active',
58 | 'dirty',
59 | 'error',
60 | 'touched',
61 | 'visited',
62 | ];
63 |
64 | export type MetaProp =
65 | | 'active'
66 | | 'dirty'
67 | | 'error'
68 | | 'touched'
69 | | 'visited';
70 |
71 | const IGNORE_PROPS = [
72 | ...INPUT_PROPS,
73 | ...META_PROPS,
74 | 'validate',
75 | 'normalize',
76 | 'defaultValue',
77 | '_form',
78 | '_addField',
79 | '_fieldChange',
80 | '_fieldFocus',
81 | '_fieldBlur',
82 | ];
83 |
84 |
85 | const maybeCheckProps = (all: All): All => {
86 | if (typeof all.value === 'boolean') {
87 | return merge(all, { checked: all.value });
88 | }
89 | return all;
90 | };
91 |
92 | const separateProps = (all: All): SeparatedProps => ({
93 | input: pick, InputProp>(INPUT_PROPS, all),
94 | meta: pick, MetaProp>(META_PROPS, all),
95 | rest: omit(IGNORE_PROPS, all),
96 | });
97 |
98 | export default (all: All) => separateProps(maybeCheckProps(all));
99 |
100 |
101 | export const boolField = over(lensProp('_field'), Boolean);
102 |
--------------------------------------------------------------------------------
/packages/redux-forms/src/shared/__tests__/formProps.spec.ts:
--------------------------------------------------------------------------------
1 | import * as R from "ramda";
2 |
3 | import formProps, { toUpdate } from '../formProps';
4 |
5 |
6 | const props = {
7 | // to omit
8 | // ---
9 | name: 'form',
10 | persistent: true,
11 | withRef: R.identity,
12 | // state
13 | _form: {},
14 | _values: {},
15 | _valid: false,
16 | _submitting: false,
17 | // actions
18 | _addForm: R.identity,
19 | _removeForm: R.identity,
20 | _touchAll: R.identity,
21 | _submitStart: R.identity,
22 | _submitStop: R.identity,
23 |
24 | // custom
25 | // ---
26 | damage: 'tons of',
27 | wow: 'so test',
28 | };
29 |
30 | const props2 = {
31 | ...props,
32 | value: true,
33 | };
34 |
35 | describe('#fieldProps', () => {
36 | it('should omit props', () => {
37 | const result = formProps(props);
38 |
39 | expect(result.name).toBeUndefined();
40 | expect(result.persistent).toBeUndefined();
41 | expect(result.withRef).toBeUndefined();
42 | expect(result._form).toBeUndefined();
43 | expect(result._values).toBeUndefined();
44 | expect(result._valid).toBeUndefined();
45 | expect(result._submitting).toBeUndefined();
46 | expect(result._addForm).toBeUndefined();
47 | expect(result._removeForm).toBeUndefined();
48 | expect(result._touchAll).toBeUndefined();
49 | expect(result._submitStart).toBeUndefined();
50 | expect(result._submitStop).toBeUndefined();
51 | });
52 |
53 | it('should keep custom props', () => {
54 | const result = formProps(props);
55 |
56 | expect(result.damage).toBe('tons of');
57 | expect(result.wow).toBe('so test');
58 | });
59 |
60 | it('should omit props not to update for', () => {
61 | const result = toUpdate(props);
62 |
63 | expect(result._values).toBeUndefined();
64 | expect(result._valid).toBeUndefined();
65 | expect(result._submitting).toBeUndefined();
66 |
67 | expect(result.name).toBeDefined();
68 | expect(result.persistent).toBeDefined();
69 | expect(result.withRef).toBeDefined();
70 | expect(result._form).toBeDefined();
71 | expect(result._addForm).toBeDefined();
72 | expect(result._removeForm).toBeDefined();
73 | expect(result._touchAll).toBeDefined();
74 | expect(result._submitStart).toBeDefined();
75 | expect(result._submitStop).toBeDefined();
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/packages/redux-forms/src/shared/helpers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | compose,
3 | keys,
4 | reduce,
5 | prop,
6 | length,
7 | } from 'ramda';
8 |
9 |
10 | export function isString(cand: any): cand is string {
11 | return typeof cand === 'string';
12 | }
13 |
14 | export function isNumber(cand: any): cand is number {
15 | return typeof cand === 'number';
16 | }
17 |
18 | export function isPromise(cand: any): cand is PromiseLike {
19 | return Boolean(cand) && typeof cand === 'object' && typeof cand.then === 'function';
20 | }
21 |
22 | export function isFunction(cand: any): cand is Function { // tslint:disable-line ban-types
23 | return typeof cand === 'function';
24 | }
25 |
26 | export function isEvent(cand: any): cand is React.SyntheticEvent {
27 | return Boolean(
28 | cand &&
29 | typeof cand === 'object' &&
30 | isFunction(cand.preventDefault) &&
31 | isFunction(cand.stopPropagation),
32 | );
33 | }
34 |
35 |
36 | export type Props = { [key: string]: any };
37 |
38 | const keyCount = compose(length, keys);
39 |
40 | export function shallowCompare(props1: Props, props2: Props): boolean {
41 | if (props1 === props2) {
42 | return true;
43 | }
44 |
45 | if (keyCount(props1) !== keyCount(props2)) {
46 | return false;
47 | }
48 |
49 | return reduce((acc, key) =>
50 | acc && prop(key, props1) === prop(key, props2), true, keys(props1));
51 | }
52 |
53 |
54 | export type Flat = { [key: string]: any };
55 |
56 | // FIXME: ugly code
57 | // A rewrite would be welcome.
58 | export function unflatten(obj: Flat) {
59 | const result = {};
60 |
61 | Object.keys(obj)
62 | .forEach((propp) => propp.split('.')
63 | .reduce((acc: any, key, index, array) => {
64 | const k = isNaN(Number(key)) ? key : Number(key);
65 |
66 | if (index === array.length - 1) {
67 | return acc[k] = obj[propp];
68 | }
69 |
70 | if (acc[k]) {
71 | return acc[k] = acc[k];
72 | }
73 |
74 | if (!isNaN(Number(array[index + 1]))) {
75 | return acc[k] = [];
76 | }
77 |
78 | return acc[k] = {};
79 | }, result));
80 |
81 | return result;
82 | }
83 |
84 | export function invariant(cond: boolean, msg: string) {
85 | if (cond) {
86 | return;
87 | }
88 |
89 | const error = new Error(msg);
90 |
91 | error.name = 'Invariant violation';
92 |
93 | throw error;
94 | }
95 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/src/selectors.ts:
--------------------------------------------------------------------------------
1 | import {
2 | memoize,
3 | compose,
4 | map,
5 | prop,
6 | path,
7 | values,
8 | none,
9 | any,
10 | identity,
11 | } from 'ramda';
12 |
13 | import { State } from './reducer';
14 | import { Form } from './containers';
15 | import { unflatten } from './shared/helpers';
16 |
17 |
18 | export type Values = { [key: string]: any | any[] | Values[] };
19 | export type Error = string | null;
20 | export type Errors = { [key: string]: Error | Error[] | Errors[] };
21 |
22 | export interface IState {
23 | reduxForms: State;
24 | }
25 |
26 | type Memoize = (x: T[]) => T;
27 |
28 |
29 | const EMPTY = {};
30 |
31 | const memUnflatten = memoize(unflatten) as Memoize<{}>;
32 |
33 | const memValue = memoize(compose(
34 | memUnflatten,
35 | map(prop('value')),
36 | ));
37 |
38 | export function getValues(name: string, state: IState): Values {
39 | const form = path
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/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/shared/__tests__/helpers.spec.ts:
--------------------------------------------------------------------------------
1 | import * as R from 'ramda';
2 |
3 | import * as helpers from '../helpers';
4 |
5 |
6 | const event = {
7 | preventDefault: R.identity,
8 | stopPropagation: R.identity,
9 | };
10 |
11 | const fields = {
12 | 'name': 'John',
13 | 'nicknames.0': 'johnny',
14 | 'nicknames.1': 'leet',
15 | 'pets.0.foods.0.calories': 133,
16 | 'pets.0.foods.1.calories': 337,
17 | };
18 |
19 |
20 | describe('#helpers', () => {
21 | it('should recognize a string', () => {
22 | expect(helpers.isString('')).toBe(true);
23 | expect(helpers.isString('adsf')).toBe(true);
24 | expect(helpers.isString('1234')).toBe(true);
25 | });
26 |
27 | it('should not recognize a string', () => {
28 | expect(helpers.isString(undefined)).toBe(false);
29 | expect(helpers.isString(null)).toBe(false);
30 | expect(helpers.isString(1234)).toBe(false);
31 | expect(helpers.isString({})).toBe(false);
32 | expect(helpers.isString([])).toBe(false);
33 | });
34 |
35 | it('should recognize a number', () => {
36 | expect(helpers.isNumber(1234)).toBe(true);
37 | expect(helpers.isNumber(13.37)).toBe(true);
38 | });
39 |
40 | it('should not recognize a number', () => {
41 | expect(helpers.isNumber(undefined)).toBe(false);
42 | expect(helpers.isNumber(null)).toBe(false);
43 | expect(helpers.isNumber('1234')).toBe(false);
44 | expect(helpers.isNumber({})).toBe(false);
45 | expect(helpers.isNumber([])).toBe(false);
46 | });
47 |
48 | it('should recognize a promise', () => {
49 | expect(helpers.isPromise(new Promise((resolve) => resolve()))).toBe(true);
50 | expect(helpers.isPromise({ then: () => null })).toBe(true);
51 | });
52 |
53 | it('should not recognize a promise', () => {
54 | expect(helpers.isPromise(undefined)).toBe(false);
55 | expect(helpers.isPromise(null)).toBe(false);
56 | expect(helpers.isPromise(1234)).toBe(false);
57 | expect(helpers.isPromise('asdf')).toBe(false);
58 | expect(helpers.isPromise({})).toBe(false);
59 | expect(helpers.isPromise([])).toBe(false);
60 | });
61 |
62 | it('should recognize a function', () => {
63 | expect(helpers.isFunction(R.identity)).toBe(true);
64 | expect(helpers.isFunction(() => null)).toBe(true);
65 | });
66 |
67 | it('should not recognize a function', () => {
68 | expect(helpers.isFunction(undefined)).toBe(false);
69 | expect(helpers.isFunction(null)).toBe(false);
70 | expect(helpers.isFunction(1234)).toBe(false);
71 | expect(helpers.isFunction('asdf')).toBe(false);
72 | expect(helpers.isFunction({})).toBe(false);
73 | expect(helpers.isFunction([])).toBe(false);
74 | });
75 |
76 | it('should recognize an event', () => {
77 | expect(helpers.isEvent(event)).toBe(true);
78 | });
79 |
80 | it('should not recognize an event', () => {
81 | expect(helpers.isEvent(undefined)).toBe(false);
82 | expect(helpers.isEvent(null)).toBe(false);
83 | expect(helpers.isEvent(1234)).toBe(false);
84 | expect(helpers.isEvent('asdf')).toBe(false);
85 | expect(helpers.isEvent({})).toBe(false);
86 | expect(helpers.isEvent([])).toBe(false);
87 | });
88 |
89 | it('should compare objects', () => {
90 | const props1 = { lol: 'rofl', kek: 1337 };
91 | const props2 = { lol: 'rofl', kek: 1337 };
92 |
93 | expect(helpers.shallowCompare(props1, props1)).toBe(true);
94 | expect(helpers.shallowCompare(props1, props2)).toBe(true);
95 | });
96 |
97 | it('should not compare objects', () => {
98 | const propsKeys1 = { lol: 'rofl', kek: 1337 };
99 | const propsKeys2 = { lol: 'rofl' };
100 |
101 | const propsValues1 = { kek: 1337, lol: 'rofl' };
102 | const propsValues2 = { kek: 1336, lol: 'rofl' };
103 |
104 | const propsId1 = { lol: 'rofl', kek: [] };
105 | const propsId2 = { lol: 'rofl', kek: [] };
106 |
107 | expect(helpers.shallowCompare(propsKeys1, propsKeys2)).toBe(false);
108 | expect(helpers.shallowCompare(propsValues1, propsValues2)).toBe(false);
109 | expect(helpers.shallowCompare(propsId1, propsId2)).toBe(false);
110 | });
111 |
112 | it('should unflatten an object', () => {
113 | expect(helpers.unflatten(fields)).toEqual({
114 | name: 'John',
115 | nicknames: ['johnny', 'leet'],
116 | pets: [{
117 | foods: [{
118 | calories: 133,
119 | }, {
120 | calories: 337,
121 | }],
122 | }],
123 | });
124 | });
125 |
126 | it('should not throw if ok', () => {
127 | expect(() => helpers.invariant(true, 'asdf')).not.toThrow();
128 | });
129 |
130 | it('should throw if not ok', () => {
131 | expect(() => helpers.invariant(false, 'asdf')).toThrowError(/asdf/);
132 | });
133 | });
134 |
--------------------------------------------------------------------------------
/packages/redux-forms-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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/__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-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/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