├── .eslintignore ├── src ├── util │ ├── eventConsts.js │ ├── getDisplayName.js │ ├── prefixName.js │ ├── shallowCompare.js │ ├── eventMocks.js │ └── __tests__ │ │ ├── getDisplayName.spec.js │ │ ├── prefixName.spec.js │ │ └── eventMocks.spec.js ├── events │ ├── isEvent.js │ ├── silenceEvents.js │ ├── silenceEvent.js │ ├── onChangeValue.js │ ├── __tests__ │ │ ├── silenceEvent.spec.js │ │ ├── isEvent.spec.js │ │ └── onChangeValue.spec.js │ └── getValue.js ├── __tests__ │ ├── setup.js │ ├── reducer.destroy.spec.js │ ├── addExpectations.js │ ├── reducer.clearAsyncError.spec.js │ ├── reducer.startSubmit.spec.js │ ├── reducer.unregisterField.spec.js │ ├── reducer.arrayShift.spec.js │ ├── reducer.startAsyncValidation.spec.js │ ├── reducer.registerField.spec.js │ ├── reducer.clearSubmit.spec.js │ ├── reducer.submit.spec.js │ ├── reducer.arrayPop.spec.js │ ├── reducer.arrayUnshift.spec.js │ ├── defaultShouldAsyncValidate.spec.js │ ├── reducer.setSubmitSuceeded.spec.js │ └── fieldKeys.spec.js ├── isReactNative.js ├── selectors │ ├── getFormValues.js │ ├── getFormSyncErrors.js │ ├── getFormSubmitErrors.js │ ├── isInvalid.js │ ├── isDirty.js │ ├── isSubmitting.js │ ├── hasSubmitFailed.js │ ├── hasSubmitSucceeded.js │ ├── isPristine.js │ ├── isValid.js │ └── __tests__ │ │ ├── isSubmitting.spec.js │ │ ├── hasSubmitFailed.spec.js │ │ ├── hasSubmitSucceeded.spec.js │ │ └── getFormValues.spec.js ├── SubmissionError.js ├── isChecked.js ├── structure │ ├── plain │ │ ├── getIn.js │ │ ├── deepEqual.js │ │ ├── index.js │ │ ├── setIn.js │ │ ├── expectations.js │ │ ├── splice.js │ │ └── deleteIn.js │ └── immutable │ │ ├── deepEqual.js │ │ ├── splice.js │ │ ├── index.js │ │ ├── expectations.js │ │ └── setIn.js ├── defaultShouldValidate.js ├── values.js ├── Form.js ├── defaultShouldAsyncValidate.js ├── asyncValidation.js ├── generateValidator.js ├── hasError.js ├── formValueSelector.js ├── immutable.js ├── deleteInWithCleanUp.js ├── index.js ├── FormSection.js ├── fieldKeys.js ├── actionTypes.js └── createFieldArrayProps.js ├── immutable.js ├── examples ├── simple │ ├── .babelrc │ ├── README.md │ ├── devServer.js │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ ├── src │ │ └── Simple.md │ └── package.json ├── wizard │ ├── .babelrc │ ├── README.md │ ├── src │ │ ├── reducer.js │ │ ├── renderField.js │ │ ├── validate.js │ │ ├── WizardFormFirstPage.js │ │ ├── Wizard.md │ │ ├── WizardForm.js │ │ ├── WizardFormSecondPage.js │ │ └── WizardFormThirdPage.js │ ├── devServer.js │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ └── package.json ├── fieldArrays │ ├── .babelrc │ ├── README.md │ ├── devServer.js │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ ├── src │ │ ├── FieldArrays.md │ │ └── validate.js │ └── package.json ├── immutable │ ├── .babelrc │ ├── README.md │ ├── src │ │ ├── reducer.js │ │ ├── ImmutableValues.js │ │ ├── validate.js │ │ ├── ImmutableForm.js │ │ └── Immutable.md │ ├── devServer.js │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ └── package.json ├── normalizing │ ├── .babelrc │ ├── README.md │ ├── devServer.js │ ├── src │ │ ├── normalizePhone.js │ │ └── FieldNormalizing.md │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ └── package.json ├── asyncValidation │ ├── .babelrc │ ├── README.md │ ├── src │ │ ├── validate.js │ │ ├── reducer.js │ │ ├── asyncValidate.js │ │ ├── AsyncValidationForm.js │ │ └── AsyncValidation.md │ ├── devServer.js │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ └── package.json ├── remoteSubmit │ ├── .babelrc │ ├── README.md │ ├── src │ │ ├── RemoteSubmitButton.js │ │ ├── submit.js │ │ ├── RemoteSubmit.md │ │ └── RemoteSubmitForm.js │ ├── devServer.js │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ └── package.json ├── submitValidation │ ├── .babelrc │ ├── README.md │ ├── devServer.js │ ├── src │ │ ├── submit.js │ │ ├── SubmitValidationForm.js │ │ └── SubmitValidation.md │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ └── package.json ├── syncValidation │ ├── .babelrc │ ├── README.md │ ├── devServer.js │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ └── package.json ├── fieldLevelValidation │ ├── .babelrc │ ├── README.md │ ├── devServer.js │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ ├── src │ │ └── FieldLevelValidation.md │ └── package.json ├── initializeFromState │ ├── .babelrc │ ├── README.md │ ├── src │ │ └── account.js │ ├── devServer.js │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ └── package.json ├── selectingFormValues │ ├── .babelrc │ ├── README.md │ ├── devServer.js │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ ├── package.json │ └── src │ │ └── SelectingFormValues.md ├── material-ui │ ├── .babelrc │ ├── README.md │ ├── src │ │ ├── asyncValidate.js │ │ └── MaterialUi.md │ ├── devServer.js │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ └── package.json └── react-widgets │ ├── .babelrc │ ├── README.md │ ├── devServer.js │ ├── src │ └── ReactWidgets.md │ ├── package.json │ ├── webpack.config.dev.js │ └── webpack.config.prod.js ├── logo.png ├── docs ├── video-thumb.jpg ├── valueLifecycle.png ├── faq │ ├── SubmitFunction.md │ ├── ImmutableJs.md │ ├── HandleVsOn.md │ ├── README.md │ ├── ReactNative.md │ ├── CustomComponent.md │ ├── EnterToSubmit.md │ └── WebsocketSubmit.md ├── api │ ├── SubmissionError.md │ ├── Form.md │ └── Reducer.md └── ValueLifecycle.md ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── babel-lodash-es.js ├── .travis.yml ├── .eslintrc ├── .editorconfig ├── tools.md ├── .babelrc ├── scripts └── build-examples.sh ├── LICENSE ├── CONTRIBUTING.md └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack.config*.js 2 | node_modules 3 | -------------------------------------------------------------------------------- /src/util/eventConsts.js: -------------------------------------------------------------------------------- 1 | export const dataKey = 'text' 2 | -------------------------------------------------------------------------------- /immutable.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/immutable'); 2 | -------------------------------------------------------------------------------- /examples/simple/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } -------------------------------------------------------------------------------- /examples/wizard/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } -------------------------------------------------------------------------------- /examples/fieldArrays/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } -------------------------------------------------------------------------------- /examples/immutable/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } -------------------------------------------------------------------------------- /examples/normalizing/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } -------------------------------------------------------------------------------- /examples/asyncValidation/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } -------------------------------------------------------------------------------- /examples/remoteSubmit/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } -------------------------------------------------------------------------------- /examples/submitValidation/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } -------------------------------------------------------------------------------- /examples/syncValidation/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligolding/redux-form/master/logo.png -------------------------------------------------------------------------------- /examples/fieldLevelValidation/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } -------------------------------------------------------------------------------- /examples/initializeFromState/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } -------------------------------------------------------------------------------- /examples/selectingFormValues/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } -------------------------------------------------------------------------------- /examples/material-ui/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-2"] 3 | } -------------------------------------------------------------------------------- /examples/react-widgets/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-2"] 3 | } -------------------------------------------------------------------------------- /docs/video-thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligolding/redux-form/master/docs/video-thumb.jpg -------------------------------------------------------------------------------- /docs/valueLifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligolding/redux-form/master/docs/valueLifecycle.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | .nyc_output 4 | coverage 5 | node_modules 6 | dist 7 | lib 8 | es 9 | npm-debug.log 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /src/util/getDisplayName.js: -------------------------------------------------------------------------------- 1 | const getDisplayName = Comp => Comp.displayName || Comp.name || 'Component' 2 | 3 | export default getDisplayName 4 | -------------------------------------------------------------------------------- /src/events/isEvent.js: -------------------------------------------------------------------------------- 1 | const isEvent = candidate => !!(candidate && candidate.stopPropagation && candidate.preventDefault) 2 | 3 | export default isEvent 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | scripts 3 | docs 4 | .babelrc 5 | .eslint* 6 | .idea 7 | .editorconfig 8 | .npmignore 9 | .nyc_output 10 | .travis.yml 11 | webpack.* 12 | coverage 13 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # Simple Form Example 2 | 3 | ## To run locally 4 | 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | 10 | Then open [`http://localhost:3030/`](http://localhost:3030/). 11 | -------------------------------------------------------------------------------- /examples/fieldArrays/README.md: -------------------------------------------------------------------------------- 1 | # Field Arrays Example 2 | 3 | ## To run locally 4 | 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | 10 | Then open [`http://localhost:3030/`](http://localhost:3030/). 11 | -------------------------------------------------------------------------------- /examples/immutable/README.md: -------------------------------------------------------------------------------- 1 | # Immutable JS Example 2 | 3 | ## To run locally 4 | 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | 10 | Then open [`http://localhost:3030/`](http://localhost:3030/). 11 | -------------------------------------------------------------------------------- /examples/normalizing/README.md: -------------------------------------------------------------------------------- 1 | # Field Normalizing Example 2 | 3 | ## To run locally 4 | 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | 10 | Then open [`http://localhost:3030/`](http://localhost:3030/). 11 | -------------------------------------------------------------------------------- /examples/remoteSubmit/README.md: -------------------------------------------------------------------------------- 1 | # Remote Submit Example 2 | 3 | ## To run locally 4 | 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | 10 | Then open [`http://localhost:3030/`](http://localhost:3030/). 11 | -------------------------------------------------------------------------------- /examples/wizard/README.md: -------------------------------------------------------------------------------- 1 | # Multi Page Wizard Form Example 2 | 3 | ## To run locally 4 | 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | 10 | Then open [`http://localhost:3030/`](http://localhost:3030/). 11 | -------------------------------------------------------------------------------- /src/__tests__/setup.js: -------------------------------------------------------------------------------- 1 | import { jsdom } from 'jsdom' 2 | 3 | global.document = jsdom('') 4 | global.window = document.defaultView 5 | global.navigator = global.window.navigator 6 | -------------------------------------------------------------------------------- /examples/material-ui/README.md: -------------------------------------------------------------------------------- 1 | # Simple Material UI Form Example 2 | 3 | ## To run locally 4 | 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | 10 | Then open [`http://localhost:3030/`](http://localhost:3030/). 11 | -------------------------------------------------------------------------------- /examples/submitValidation/README.md: -------------------------------------------------------------------------------- 1 | # Submit Validation Example 2 | 3 | ## To run locally 4 | 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | 10 | Then open [`http://localhost:3030/`](http://localhost:3030/). 11 | -------------------------------------------------------------------------------- /examples/syncValidation/README.md: -------------------------------------------------------------------------------- 1 | # Synchronous Validation Example 2 | 3 | ## To run locally 4 | 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | 10 | Then open [`http://localhost:3030/`](http://localhost:3030/). 11 | -------------------------------------------------------------------------------- /src/events/silenceEvents.js: -------------------------------------------------------------------------------- 1 | import silenceEvent from './silenceEvent' 2 | 3 | const silenceEvents = fn => (event, ...args) => 4 | silenceEvent(event) ? fn(...args) : fn(event, ...args) 5 | 6 | export default silenceEvents 7 | -------------------------------------------------------------------------------- /src/isReactNative.js: -------------------------------------------------------------------------------- 1 | const isReactNative = 2 | typeof window !== 'undefined' && 3 | window.navigator && 4 | window.navigator.product && 5 | window.navigator.product === 'ReactNative' 6 | 7 | export default isReactNative 8 | -------------------------------------------------------------------------------- /examples/asyncValidation/README.md: -------------------------------------------------------------------------------- 1 | # Asynchronous Blur Validation Example 2 | 3 | ## To run locally 4 | 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | 10 | Then open [`http://localhost:3030/`](http://localhost:3030/). 11 | -------------------------------------------------------------------------------- /examples/fieldLevelValidation/README.md: -------------------------------------------------------------------------------- 1 | # Field Level Validation Example 2 | 3 | ## To run locally 4 | 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | 10 | Then open [`http://localhost:3030/`](http://localhost:3030/). 11 | -------------------------------------------------------------------------------- /examples/initializeFromState/README.md: -------------------------------------------------------------------------------- 1 | # Initialize From State Example 2 | 3 | ## To run locally 4 | 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | 10 | Then open [`http://localhost:3030/`](http://localhost:3030/). 11 | -------------------------------------------------------------------------------- /examples/selectingFormValues/README.md: -------------------------------------------------------------------------------- 1 | # Selecting Form Values Example 2 | 3 | ## To run locally 4 | 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | 10 | Then open [`http://localhost:3030/`](http://localhost:3030/). 11 | -------------------------------------------------------------------------------- /examples/wizard/src/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { reducer as reduxFormReducer } from 'redux-form' 3 | 4 | const reducer = combineReducers({ 5 | form: reduxFormReducer 6 | }) 7 | 8 | export default reducer 9 | -------------------------------------------------------------------------------- /src/selectors/getFormValues.js: -------------------------------------------------------------------------------- 1 | const createGetFormValues = ({ getIn }) => 2 | (form, getFormState = state => getIn(state, 'form')) => 3 | state => getIn(getFormState(state), `${form}.values`) 4 | 5 | export default createGetFormValues 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | Every release, along with the migration instructions, is documented on the Github [Releases](https://github.com/erikras/redux-form/releases) page. 5 | -------------------------------------------------------------------------------- /examples/immutable/src/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux-immutablejs' 2 | import { reducer as form } from 'redux-form/immutable' // <--- immutable import 3 | 4 | const reducer = combineReducers({ form }) 5 | 6 | export default reducer 7 | -------------------------------------------------------------------------------- /src/events/silenceEvent.js: -------------------------------------------------------------------------------- 1 | import isEvent from './isEvent' 2 | 3 | const silenceEvent = event => { 4 | const is = isEvent(event) 5 | if (is) { 6 | event.preventDefault() 7 | } 8 | return is 9 | } 10 | 11 | export default silenceEvent 12 | -------------------------------------------------------------------------------- /src/selectors/getFormSyncErrors.js: -------------------------------------------------------------------------------- 1 | const createGetFormSyncErrors = ({ getIn }) => 2 | (form, getFormState = state => getIn(state, 'form')) => 3 | state => getIn(getFormState(state), `${form}.syncErrors`) 4 | 5 | export default createGetFormSyncErrors 6 | -------------------------------------------------------------------------------- /examples/react-widgets/README.md: -------------------------------------------------------------------------------- 1 | # [React Widgets](https://github.com/jquense/react-widgets) Example 2 | 3 | ## To run locally 4 | 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | 10 | Then open [`http://localhost:3030/`](http://localhost:3030/). 11 | -------------------------------------------------------------------------------- /src/selectors/getFormSubmitErrors.js: -------------------------------------------------------------------------------- 1 | const createGetFormSubmitErrors = ({ getIn }) => 2 | (form, getFormState = state => getIn(state, 'form')) => 3 | state => getIn(getFormState(state), `${form}.submitErrors`) 4 | 5 | export default createGetFormSubmitErrors 6 | -------------------------------------------------------------------------------- /src/util/prefixName.js: -------------------------------------------------------------------------------- 1 | const isFieldArrayRegx = /\[\d+\]$/ 2 | 3 | export default function formatName(context, name) { 4 | const { _reduxForm: { sectionPrefix } } = context 5 | return !sectionPrefix || isFieldArrayRegx.test(name) ? name : `${sectionPrefix}.${name}` 6 | } 7 | -------------------------------------------------------------------------------- /src/SubmissionError.js: -------------------------------------------------------------------------------- 1 | import ExtendableError from 'es6-error' 2 | 3 | class SubmissionError extends ExtendableError { 4 | constructor(errors) { 5 | super('Submit Validation Failed') 6 | this.errors = errors 7 | } 8 | } 9 | 10 | export default SubmissionError 11 | -------------------------------------------------------------------------------- /babel-lodash-es.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return { 3 | visitor: { 4 | ImportDeclaration(path) { 5 | const source = path.node.source 6 | source.value = source.value.replace(/^lodash($|\/)/, 'lodash-es$1') 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | before_install: 4 | - npm install -g npm@latest 5 | 6 | node_js: 7 | - "6" 8 | - "5" 9 | - "4" 10 | - "stable" 11 | 12 | script: 13 | - npm run lint 14 | - npm test 15 | 16 | after_success: 17 | - npm run test:cov 18 | - npm run test:codecov -------------------------------------------------------------------------------- /src/selectors/isInvalid.js: -------------------------------------------------------------------------------- 1 | import createIsValid from './isValid' 2 | 3 | const createIsInvalid = structure => 4 | (form, getFormState) => { 5 | const isValid = createIsValid(structure)(form, getFormState) 6 | return state => !isValid(state) 7 | } 8 | 9 | export default createIsInvalid 10 | -------------------------------------------------------------------------------- /src/selectors/isDirty.js: -------------------------------------------------------------------------------- 1 | import createIsPristine from './isPristine' 2 | 3 | const createIsDirty = structure => 4 | (form, getFormState) => { 5 | const isPristine = createIsPristine(structure)(form, getFormState) 6 | return state => !isPristine(state) 7 | } 8 | 9 | export default createIsDirty 10 | -------------------------------------------------------------------------------- /src/selectors/isSubmitting.js: -------------------------------------------------------------------------------- 1 | const createIsSubmitting = ({ getIn }) => 2 | (form, getFormState = state => getIn(state, 'form')) => 3 | state => { 4 | const formState = getFormState(state) 5 | return getIn(formState, `${form}.submitting`) || false 6 | } 7 | 8 | export default createIsSubmitting 9 | -------------------------------------------------------------------------------- /src/util/shallowCompare.js: -------------------------------------------------------------------------------- 1 | import shallowEqual from 'shallowequal' 2 | 3 | const shallowCompare = (instance, nextProps, nextState) => { 4 | return ( 5 | !shallowEqual(instance.props, nextProps) || 6 | !shallowEqual(instance.state, nextState) 7 | ) 8 | } 9 | 10 | export default shallowCompare 11 | -------------------------------------------------------------------------------- /examples/asyncValidation/src/validate.js: -------------------------------------------------------------------------------- 1 | const validate = values => { 2 | const errors = {} 3 | if (!values.username) { 4 | errors.username = 'Required' 5 | } 6 | if (!values.password) { 7 | errors.password = 'Required' 8 | } 9 | return errors 10 | } 11 | 12 | export default validate 13 | 14 | -------------------------------------------------------------------------------- /src/selectors/hasSubmitFailed.js: -------------------------------------------------------------------------------- 1 | const createHasSubmitFailed = ({ getIn }) => 2 | (form, getFormState = state => getIn(state, 'form')) => 3 | state => { 4 | const formState = getFormState(state) 5 | return getIn(formState, `${form}.submitFailed`) || false 6 | } 7 | 8 | export default createHasSubmitFailed 9 | -------------------------------------------------------------------------------- /src/selectors/hasSubmitSucceeded.js: -------------------------------------------------------------------------------- 1 | const createHasSubmitSucceeded = ({ getIn }) => 2 | (form, getFormState = state => getIn(state, 'form')) => 3 | state => { 4 | const formState = getFormState(state) 5 | return getIn(formState, `${form}.submitSucceeded`) || false 6 | } 7 | 8 | export default createHasSubmitSucceeded 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-rackt", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "mocha": true, 7 | "node": true 8 | }, 9 | "rules": { 10 | "react/jsx-uses-react": 2, 11 | "react/jsx-uses-vars": 2, 12 | "react/jsx-no-undef": 2 13 | }, 14 | "plugins": [ 15 | "react" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | 13 | [*.js] 14 | indent_style = space 15 | indent_size = 2 16 | trim_trailing_whitespace = false 17 | insert_final_newline = true 18 | max_line_length = f 19 | -------------------------------------------------------------------------------- /examples/wizard/src/renderField.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const renderField = ({ input, label, type, meta: { touched, error } }) => ( 4 |
5 | 6 |
7 | 8 | {touched && error && {error}} 9 |
10 |
11 | ) 12 | 13 | export default renderField 14 | -------------------------------------------------------------------------------- /src/isChecked.js: -------------------------------------------------------------------------------- 1 | const isChecked = value => { 2 | if (typeof value === 'boolean') { 3 | return value 4 | } 5 | if (typeof value === 'string') { 6 | const lower = value.toLowerCase() 7 | if (lower === 'true') { 8 | return true 9 | } 10 | if (lower === 'false') { 11 | return false 12 | } 13 | } 14 | return undefined 15 | } 16 | 17 | export default isChecked 18 | -------------------------------------------------------------------------------- /examples/asyncValidation/src/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { reducer as formReducer } from 'redux-form' 3 | import validate from './validate' 4 | 5 | const reducer = combineReducers({ 6 | form: formReducer.validation({ 7 | asyncValidation: validate // "asyncValidation" is the form name given to reduxForm() decorator 8 | }) 9 | }) 10 | 11 | export default reducer 12 | -------------------------------------------------------------------------------- /examples/material-ui/src/asyncValidate.js: -------------------------------------------------------------------------------- 1 | const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) 2 | 3 | const asyncValidate = (values/*, dispatch */) => { 4 | return sleep(1000) // simulate server latency 5 | .then(() => { 6 | if ([ 'foo@foo.com', 'bar@bar.com' ].includes(values.email)) { 7 | throw { email: 'Email already Exists' } 8 | } 9 | }) 10 | } 11 | 12 | export default asyncValidate 13 | 14 | -------------------------------------------------------------------------------- /tools.md: -------------------------------------------------------------------------------- 1 | # Tools 2 | 3 | The following libraries are either built on top of `redux-form` or are compatible with `redux-form`. 4 | 5 | * [`redux-form-generator`](https://github.com/lemonCMS/redux-form-generator) 6 | * [`redux-form-schema`](https://github.com/inlight-media/redux-form-schema) 7 | * [`redux-validate`](https://github.com/ashtonwar/redux-validate) 8 | * [`revalidate`](https://github.com/jfairbank/revalidate) 9 | 10 | Add yours! 11 | -------------------------------------------------------------------------------- /src/selectors/isPristine.js: -------------------------------------------------------------------------------- 1 | const createIsPristine = ({ deepEqual, empty, getIn }) => 2 | (form, getFormState = state => getIn(state, 'form')) => 3 | state => { 4 | const formState = getFormState(state) 5 | const initial = getIn(formState, `${form}.initial`) || empty 6 | const values = getIn(formState, `${form}.values`) || initial 7 | return deepEqual(initial, values) 8 | } 9 | 10 | export default createIsPristine 11 | -------------------------------------------------------------------------------- /examples/asyncValidation/src/asyncValidate.js: -------------------------------------------------------------------------------- 1 | const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) 2 | 3 | const asyncValidate = (values/*, dispatch */) => { 4 | return sleep(1000) // simulate server latency 5 | .then(() => { 6 | if ([ 'john', 'paul', 'george', 'ringo' ].includes(values.username)) { 7 | throw { username: 'That username is taken' } 8 | } 9 | }) 10 | } 11 | 12 | export default asyncValidate 13 | 14 | -------------------------------------------------------------------------------- /src/structure/plain/getIn.js: -------------------------------------------------------------------------------- 1 | import { toPath } from 'lodash' 2 | 3 | const getIn = (state, field) => { 4 | if (!state) { 5 | return state 6 | } 7 | 8 | const path = toPath(field) 9 | const length = path.length 10 | if (!length) { 11 | return undefined 12 | } 13 | 14 | let result = state 15 | for (let i = 0; i < length && !!result; ++i) { 16 | result = result[path[i]] 17 | } 18 | 19 | return result 20 | } 21 | 22 | export default getIn 23 | -------------------------------------------------------------------------------- /src/util/eventMocks.js: -------------------------------------------------------------------------------- 1 | const getEvent = rest => ({ 2 | stopPropagation: id => id, 3 | preventDefault: id => id, 4 | ...rest 5 | }) 6 | 7 | export function valueMock(value) { 8 | return getEvent({ target: { value } }) 9 | } 10 | 11 | export function dragStartMock(setData) { 12 | return getEvent({ 13 | dataTransfer: { setData } 14 | }) 15 | } 16 | 17 | export function dropMock(getData) { 18 | return getEvent({ 19 | dataTransfer: { getData } 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /docs/faq/SubmitFunction.md: -------------------------------------------------------------------------------- 1 | # My submit function isn't being called! Help? 2 | 3 | Possible causes: 4 | 5 | * Your synchronous validation function is not returning `{}`. Probably because: 6 | * You are upgrading from a previous version of `redux-form` that required that `{valid: true}` be returned. 7 | * You have removed a field from your form, but forgotten to remove it from your validation function. 8 | * Your asynchronous validation function is returning a rejected promise for some reason. 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-no-commonjs", "react", "stage-2"], 3 | "env": { 4 | "development": { 5 | "plugins": ["lodash", "transform-es2015-modules-commonjs"] 6 | }, 7 | "test": { 8 | "plugins": ["lodash", "transform-es2015-modules-commonjs", "istanbul"] 9 | }, 10 | "production": { 11 | "plugins": ["lodash", "transform-es2015-modules-commonjs"] 12 | }, 13 | "es": { 14 | "plugins": ["lodash", "./babel-lodash-es"] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/defaultShouldValidate.js: -------------------------------------------------------------------------------- 1 | const defaultShouldValidate = ({ 2 | values, 3 | nextProps, 4 | // props, // not used in default implementation 5 | initialRender, 6 | lastFieldValidatorKeys, 7 | fieldValidatorKeys, 8 | structure 9 | }) => { 10 | if (initialRender) { 11 | return true 12 | } 13 | return !structure.deepEqual(values, nextProps.values) || 14 | !structure.deepEqual(lastFieldValidatorKeys, fieldValidatorKeys) 15 | } 16 | 17 | export default defaultShouldValidate 18 | -------------------------------------------------------------------------------- /src/values.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | const createValues = ({ getIn }) => 4 | config => { 5 | const { form, prop, getFormState } = { 6 | prop: 'values', 7 | getFormState: state => getIn(state, 'form'), 8 | ...config 9 | } 10 | return connect( 11 | state => ({ 12 | [prop]: getIn(getFormState(state), `${form}.values`) 13 | }), 14 | () => ({}) // ignore dispatch 15 | ) 16 | } 17 | 18 | export default createValues 19 | -------------------------------------------------------------------------------- /docs/faq/ImmutableJs.md: -------------------------------------------------------------------------------- 1 | # Does `redux-form` work with ImmutableJS? 2 | 3 | Yes! 4 | 5 | As of `v6`, `redux-form` has support for ImmutableJS built in. 6 | 7 | Simply import from a different endpoint and all of `redux-form`'s internal state will be kept 8 | with ImmutableJS data structures. 9 | 10 | Instead of... 11 | ```js 12 | import { reduxForm } from 'redux-form' 13 | ``` 14 | 15 | ...do... 16 | 17 | ```js 18 | import { reduxForm } from 'redux-form/immutable' 19 | ``` 20 | 21 | That's all there is to it! 22 | -------------------------------------------------------------------------------- /src/util/__tests__/getDisplayName.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import getDisplayName from '../getDisplayName' 3 | 4 | describe('getDisplayName', () => { 5 | it('should read displayName', () => { 6 | expect(getDisplayName({ displayName: 'foo' })).toBe('foo') 7 | }) 8 | 9 | it('should read name', () => { 10 | expect(getDisplayName({ name: 'foo' })).toBe('foo') 11 | }) 12 | 13 | it('should default to Component', () => { 14 | expect(getDisplayName({ })).toBe('Component') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/structure/plain/deepEqual.js: -------------------------------------------------------------------------------- 1 | import { isEqualWith } from 'lodash' 2 | 3 | const customizer = (obj, other) => { 4 | if (obj === other) return true 5 | if ((obj == null || obj === '' || obj === false) && 6 | (other == null || other === '' || other === false)) return true 7 | 8 | if (obj && other && obj._error !== other._error) return false 9 | if (obj && other && obj._warning !== other._warning) return false 10 | } 11 | 12 | const deepEqual = (a, b) => isEqualWith(a, b, customizer) 13 | 14 | export default deepEqual 15 | -------------------------------------------------------------------------------- /src/structure/plain/index.js: -------------------------------------------------------------------------------- 1 | import splice from './splice' 2 | import getIn from './getIn' 3 | import setIn from './setIn' 4 | import deepEqual from './deepEqual' 5 | import deleteIn from './deleteIn' 6 | import { some } from 'lodash' 7 | 8 | const structure = { 9 | empty: {}, 10 | emptyList: [], 11 | getIn, 12 | setIn, 13 | deepEqual, 14 | deleteIn, 15 | fromJS: value => value, 16 | size: array => array ? array.length : 0, 17 | some, 18 | splice, 19 | toJS: value => value 20 | } 21 | 22 | export default structure 23 | -------------------------------------------------------------------------------- /src/events/onChangeValue.js: -------------------------------------------------------------------------------- 1 | import getValue from './getValue' 2 | import isReactNative from '../isReactNative' 3 | 4 | const onChangeValue = (event, { name, parse, normalize }) => { 5 | // read value from input 6 | let value = getValue(event, isReactNative) 7 | 8 | // parse value if we have a parser 9 | if (parse) { 10 | value = parse(value, name) 11 | } 12 | 13 | // normalize value 14 | if (normalize) { 15 | value = normalize(name, value) 16 | } 17 | 18 | return value 19 | } 20 | 21 | export default onChangeValue 22 | -------------------------------------------------------------------------------- /examples/initializeFromState/src/account.js: -------------------------------------------------------------------------------- 1 | // Quack! This is a duck. https://github.com/erikras/ducks-modular-redux 2 | const LOAD = 'redux-form-examples/account/LOAD' 3 | 4 | const reducer = (state = {}, action) => { 5 | switch (action.type) { 6 | case LOAD: 7 | return { 8 | data: action.data 9 | } 10 | default: 11 | return state 12 | } 13 | } 14 | 15 | /** 16 | * Simulates data loaded into this reducer from somewhere 17 | */ 18 | export const load = data => ({ type: LOAD, data }) 19 | 20 | export default reducer 21 | -------------------------------------------------------------------------------- /examples/remoteSubmit/src/RemoteSubmitButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { submit } from 'redux-form' 4 | 5 | const style = { 6 | padding: '10px 20px', 7 | width: 140, 8 | display: 'block', 9 | margin: '20px auto', 10 | fontSize: '16px' 11 | } 12 | 13 | const RemoteSubmitButton = ({ dispatch }) => 14 | 18 | // ^^^^^^^^^^^^ name of the form 19 | 20 | export default connect()(RemoteSubmitButton) 21 | -------------------------------------------------------------------------------- /docs/faq/HandleVsOn.md: -------------------------------------------------------------------------------- 1 | # What's the difference between `handleSubmit` and `onSubmit`? 2 | 3 | From what I can tell from every example I have seen, there is an unwritten – until now! – rule in the React community: 4 | 5 | > **handleX is what you name the function that you pass to the onX prop.** 6 | 7 | ```javascript 8 | render() { 9 | const handleClick = () => console.log('Clicked!'); 10 | return ; 11 | } 12 | ``` 13 | 14 | Since the only way that redux-form can provide you with functionality is to pass it as a prop, it passes you a 15 | `handleSubmit` for you to pass to `onSubmit`. 16 | -------------------------------------------------------------------------------- /examples/wizard/src/validate.js: -------------------------------------------------------------------------------- 1 | const validate = values => { 2 | const errors = {} 3 | if (!values.firstName) { 4 | errors.firstName = 'Required' 5 | } 6 | if (!values.lastName) { 7 | errors.lastName = 'Required' 8 | } 9 | if (!values.email) { 10 | errors.email = 'Required' 11 | } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) { 12 | errors.email = 'Invalid email address' 13 | } 14 | if (!values.sex) { 15 | errors.sex = 'Required' 16 | } 17 | if (!values.favoriteColor) { 18 | errors.favoriteColor = 'Required' 19 | } 20 | return errors 21 | } 22 | 23 | export default validate 24 | -------------------------------------------------------------------------------- /src/__tests__/reducer.destroy.spec.js: -------------------------------------------------------------------------------- 1 | import { destroy } from '../actions' 2 | 3 | const describeDestroy = (reducer, expect, { fromJS }) => () => { 4 | it('should destroy form state', () => { 5 | const state = reducer(fromJS({ 6 | foo: { 7 | values: { 8 | myField: 'initialValue' 9 | }, 10 | active: 'myField' 11 | }, 12 | otherThing: { 13 | touchThis: false 14 | } 15 | }), destroy('foo')) 16 | expect(state) 17 | .toEqualMap({ 18 | otherThing: { 19 | touchThis: false 20 | } 21 | }) 22 | }) 23 | } 24 | 25 | export default describeDestroy 26 | -------------------------------------------------------------------------------- /scripts/build-examples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rebuild=false 3 | while test $# -gt 0; do 4 | case "$1" in 5 | --rebuild) 6 | rebuild=true 7 | shift 8 | ;; 9 | *) 10 | break 11 | ;; 12 | esac 13 | done 14 | 15 | if $rebuild 16 | then 17 | echo 'Rebuilding' 18 | fi 19 | 20 | cd examples 21 | for example in `ls -d */` 22 | do 23 | echo $example 24 | cd $example 25 | if $rebuild 26 | then 27 | rm -rf node_modules 28 | else 29 | rm -rf node_modules/redux-form 30 | rm -rf node_modules/redux-form-website-template 31 | fi 32 | npm install 33 | cd .. 34 | done 35 | tput bel 36 | say "The examples are built" 37 | -------------------------------------------------------------------------------- /src/Form.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | class Form extends Component { 4 | constructor(props, context) { 5 | super(props, context) 6 | if (!context._reduxForm) { 7 | throw new Error('Form must be inside a component decorated with reduxForm()') 8 | } 9 | } 10 | 11 | componentWillMount() { 12 | this.context._reduxForm.registerInnerOnSubmit(this.props.onSubmit) 13 | } 14 | 15 | render() { 16 | return
17 | } 18 | } 19 | 20 | Form.propTypes = { 21 | onSubmit: PropTypes.func.isRequired 22 | } 23 | Form.contextTypes = { 24 | _reduxForm: PropTypes.object 25 | } 26 | 27 | export default Form 28 | -------------------------------------------------------------------------------- /src/structure/immutable/deepEqual.js: -------------------------------------------------------------------------------- 1 | import { Iterable } from 'immutable' 2 | 3 | import { isEqualWith } from 'lodash' 4 | 5 | const customizer = (obj, other) => { 6 | if (obj == other) return true 7 | if ((obj == null || obj === '' || obj === false) && 8 | (other == null || other === '' || other === false)) return true 9 | 10 | if (Iterable.isIterable(obj) && Iterable.isIterable(other)) { 11 | return obj.count() === other.count() && obj.every((value, key) => { 12 | return isEqualWith(value, other.get(key), customizer) 13 | }) 14 | } 15 | 16 | return void 0 17 | } 18 | 19 | const deepEqual = (a, b) => isEqualWith(a, b, customizer) 20 | 21 | export default deepEqual 22 | -------------------------------------------------------------------------------- /src/defaultShouldAsyncValidate.js: -------------------------------------------------------------------------------- 1 | const defaultShouldAsyncValidate = ({ 2 | initialized, 3 | trigger, 4 | // blurredField, // not used in default implementation 5 | pristine, 6 | syncValidationPasses 7 | }) => { 8 | if(!syncValidationPasses) { 9 | return false 10 | } 11 | switch(trigger) { 12 | case 'blur': 13 | // blurring 14 | return true 15 | case 'submit': 16 | // submitting, so only async validate if form is dirty or was never initialized 17 | // conversely, DON'T async validate if the form is pristine just as it was initialized 18 | return !pristine || !initialized 19 | default: 20 | return false 21 | } 22 | } 23 | 24 | export default defaultShouldAsyncValidate 25 | -------------------------------------------------------------------------------- /examples/simple/devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'index.html')); 18 | }); 19 | 20 | app.listen(3030, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:3030'); 27 | }); -------------------------------------------------------------------------------- /examples/wizard/devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'index.html')); 18 | }); 19 | 20 | app.listen(3030, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:3030'); 27 | }); -------------------------------------------------------------------------------- /examples/fieldArrays/devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'index.html')); 18 | }); 19 | 20 | app.listen(3030, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:3030'); 27 | }); -------------------------------------------------------------------------------- /examples/immutable/devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'index.html')); 18 | }); 19 | 20 | app.listen(3030, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:3030'); 27 | }); -------------------------------------------------------------------------------- /examples/material-ui/devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'index.html')); 18 | }); 19 | 20 | app.listen(3030, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:3030'); 27 | }); -------------------------------------------------------------------------------- /examples/normalizing/devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'index.html')); 18 | }); 19 | 20 | app.listen(3030, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:3030'); 27 | }); -------------------------------------------------------------------------------- /examples/react-widgets/devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'index.html')); 18 | }); 19 | 20 | app.listen(3030, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:3030'); 27 | }); -------------------------------------------------------------------------------- /examples/remoteSubmit/devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'index.html')); 18 | }); 19 | 20 | app.listen(3030, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:3030'); 27 | }); -------------------------------------------------------------------------------- /examples/asyncValidation/devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'index.html')); 18 | }); 19 | 20 | app.listen(3030, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:3030'); 27 | }); -------------------------------------------------------------------------------- /examples/remoteSubmit/src/submit.js: -------------------------------------------------------------------------------- 1 | import { SubmissionError } from 'redux-form' 2 | 3 | const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) 4 | 5 | function submit(values) { 6 | return sleep(1000) // simulate server latency 7 | .then(() => { 8 | if (![ 'john', 'paul', 'george', 'ringo' ].includes(values.username)) { 9 | throw new SubmissionError({ username: 'User does not exist', _error: 'Login failed!' }) 10 | } else if (values.password !== 'redux-form') { 11 | throw new SubmissionError({ password: 'Wrong password', _error: 'Login failed!' }) 12 | } else { 13 | window.alert(`You submitted:\n\n${JSON.stringify(values, null, 2)}`) 14 | } 15 | }) 16 | } 17 | 18 | export default submit 19 | -------------------------------------------------------------------------------- /examples/submitValidation/devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'index.html')); 18 | }); 19 | 20 | app.listen(3030, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:3030'); 27 | }); -------------------------------------------------------------------------------- /examples/syncValidation/devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'index.html')); 18 | }); 19 | 20 | app.listen(3030, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:3030'); 27 | }); -------------------------------------------------------------------------------- /examples/fieldLevelValidation/devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'index.html')); 18 | }); 19 | 20 | app.listen(3030, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:3030'); 27 | }); -------------------------------------------------------------------------------- /examples/initializeFromState/devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'index.html')); 18 | }); 19 | 20 | app.listen(3030, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:3030'); 27 | }); -------------------------------------------------------------------------------- /examples/selectingFormValues/devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'index.html')); 18 | }); 19 | 20 | app.listen(3030, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:3030'); 27 | }); -------------------------------------------------------------------------------- /examples/submitValidation/src/submit.js: -------------------------------------------------------------------------------- 1 | import { SubmissionError } from 'redux-form' 2 | 3 | const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) 4 | 5 | function submit(values) { 6 | return sleep(1000) // simulate server latency 7 | .then(() => { 8 | if (![ 'john', 'paul', 'george', 'ringo' ].includes(values.username)) { 9 | throw new SubmissionError({ username: 'User does not exist', _error: 'Login failed!' }) 10 | } else if (values.password !== 'redux-form') { 11 | throw new SubmissionError({ password: 'Wrong password', _error: 'Login failed!' }) 12 | } else { 13 | window.alert(`You submitted:\n\n${JSON.stringify(values, null, 2)}`) 14 | } 15 | }) 16 | } 17 | 18 | export default submit 19 | -------------------------------------------------------------------------------- /examples/immutable/src/ImmutableValues.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { values as valuesDecorator } from 'redux-form/immutable' 3 | import { Code } from 'redux-form-website-template' 4 | 5 | /** 6 | * This is just like the Values component that the other examples import, except that it works 7 | * with Immutable JS. 8 | */ 9 | const ImmutableValues = ({ form }) => { 10 | const decorator = valuesDecorator({ form }) 11 | const component = ({ values }) => { 12 | return ( 13 |
14 |

Values

15 | 16 |
17 | ) 18 | } 19 | const Decorated = decorator(component) 20 | return 21 | } 22 | 23 | export default ImmutableValues 24 | -------------------------------------------------------------------------------- /src/asyncValidation.js: -------------------------------------------------------------------------------- 1 | import isPromise from 'is-promise' 2 | 3 | const asyncValidation = (fn, start, stop, field) => { 4 | start(field) 5 | const promise = fn() 6 | if (!isPromise(promise)) { 7 | throw new Error('asyncValidate function passed to reduxForm must return a promise') 8 | } 9 | const handleErrors = rejected => errors => { 10 | if (errors && Object.keys(errors).length) { 11 | stop(errors) 12 | return errors 13 | } else if (rejected) { 14 | stop() 15 | throw new Error('Asynchronous validation promise was rejected without errors.') 16 | } 17 | stop() 18 | return Promise.resolve() 19 | } 20 | return promise.then(handleErrors(false), handleErrors(true)) 21 | } 22 | 23 | export default asyncValidation 24 | -------------------------------------------------------------------------------- /docs/faq/README.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | Below is a list of common problems or questions that people have using `redux-form`. 4 | 5 | 1. [My submit function isn't being called! Help?](SubmitFunction.md) 6 | 7 | 2. [Will `redux-form` work with my custom input component?](CustomComponent.md) 8 | 9 | 3. [What's the difference between `handleSubmit` and `onSubmit`?](HandleVsOn.md) 10 | 11 | 4. [How can I clear my form after my submission succeeds?](HowToClear.md) 12 | 13 | 5. [How can I submit my form when the user presses Enter?](EnterToSubmit.md) 14 | 15 | 6. [Does `redux-form` work with React Native?](ReactNative.md) 16 | 17 | 7. [Does `redux-form` work with ImmutableJS?](ImmutableJs.md) 18 | 19 | 8. [Can I submit my form using websockets?](WebsocketSubmit.md) 20 | -------------------------------------------------------------------------------- /examples/normalizing/src/normalizePhone.js: -------------------------------------------------------------------------------- 1 | const normalizePhone = (value, previousValue) => { 2 | if (!value) { 3 | return value 4 | } 5 | const onlyNums = value.replace(/[^\d]/g, '') 6 | if (!previousValue || value.length > previousValue.length) { 7 | // typing forward 8 | if (onlyNums.length === 3) { 9 | return onlyNums + '-' 10 | } 11 | if (onlyNums.length === 6) { 12 | return onlyNums.slice(0, 3) + '-' + onlyNums.slice(3) + '-' 13 | } 14 | } 15 | if (onlyNums.length <= 3) { 16 | return onlyNums 17 | } 18 | if (onlyNums.length <= 6) { 19 | return onlyNums.slice(0, 3) + '-' + onlyNums.slice(3) 20 | } 21 | return onlyNums.slice(0, 3) + '-' + onlyNums.slice(3, 6) + '-' + onlyNums.slice(6, 10) 22 | } 23 | 24 | export default normalizePhone 25 | -------------------------------------------------------------------------------- /src/generateValidator.js: -------------------------------------------------------------------------------- 1 | import plain from './structure/plain' 2 | 3 | const toArray = value => Array.isArray(value) ? value : [ value ] 4 | 5 | const getError = (value, values, validators) => { 6 | for(const validator of toArray(validators)) { 7 | const error = validator(value, values) 8 | if(error) { 9 | return error 10 | } 11 | } 12 | } 13 | 14 | const generateValidator = (validators, { getIn }) => 15 | values => { 16 | let errors = {} 17 | Object.keys(validators).forEach(name => { 18 | const value = getIn(values, name) 19 | const error = getError(value, values, validators[name]) 20 | if(error) { 21 | errors = plain.setIn(errors, name, error) 22 | } 23 | }) 24 | return errors 25 | } 26 | 27 | export default generateValidator 28 | -------------------------------------------------------------------------------- /src/structure/plain/setIn.js: -------------------------------------------------------------------------------- 1 | import { toPath } from 'lodash' 2 | 3 | const setInWithPath = (state, value, path, pathIndex) => { 4 | if (pathIndex >= path.length) { 5 | return value 6 | } 7 | 8 | const first = path[pathIndex] 9 | const next = setInWithPath(state && state[first], value, path, pathIndex + 1) 10 | 11 | if (!state) { 12 | const initialized = isNaN(first) ? {} : [] 13 | initialized[first] = next 14 | return initialized 15 | } 16 | 17 | if (Array.isArray(state)) { 18 | const copy = [].concat(state) 19 | copy[first] = next 20 | return copy 21 | } 22 | 23 | return { 24 | ...state, 25 | [first]: next 26 | } 27 | } 28 | 29 | const setIn = (state, field, value) => setInWithPath(state, value, toPath(field), 0) 30 | 31 | export default setIn 32 | -------------------------------------------------------------------------------- /src/hasError.js: -------------------------------------------------------------------------------- 1 | import plainGetIn from './structure/plain/getIn' 2 | 3 | const getErrorKeys = (name, type) => { 4 | switch (type) { 5 | case 'Field': 6 | return [ name, `${name}._error` ] 7 | case 'FieldArray': 8 | return [ `${name}._error` ] 9 | } 10 | } 11 | 12 | const createHasError = ({ getIn }) => { 13 | const hasError = (field, syncErrors, asyncErrors, submitErrors) => { 14 | if (!syncErrors && !asyncErrors && !submitErrors) { 15 | return false 16 | } 17 | 18 | const name = getIn(field, 'name') 19 | const type = getIn(field, 'type') 20 | return getErrorKeys(name, type).some(key => 21 | plainGetIn(syncErrors, key) || 22 | getIn(asyncErrors, key) || 23 | getIn(submitErrors, key)) 24 | } 25 | return hasError 26 | } 27 | 28 | export default createHasError 29 | -------------------------------------------------------------------------------- /src/structure/immutable/splice.js: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable' 2 | 3 | export default (list, index, removeNum, value) => { 4 | list = List.isList(list) ? list : List() 5 | 6 | if (index < list.count()) { 7 | if (value === undefined && !removeNum) { // inserting undefined 8 | // first insert null and then re-set it to undefined 9 | return list.splice(index, 0, null).set(index, undefined) 10 | } 11 | if (value != null) { 12 | return list.splice(index, removeNum, value) // removing and adding 13 | } else { 14 | return list.splice(index, removeNum) // removing 15 | } 16 | } 17 | if (removeNum) { // trying to remove non-existant item: return original array 18 | return list 19 | } 20 | // trying to add outside of range: just set value 21 | return list.set(index, value) 22 | } 23 | -------------------------------------------------------------------------------- /src/__tests__/addExpectations.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | 3 | /** 4 | * Takes expectations and extends expect with them. Cannot use expect.extends due to the 5 | * asynchronous nature of the tests. 6 | * @param expectations Expectations to add 7 | */ 8 | const addExpectations = expectations => { 9 | const decorate = dest => { 10 | const wrap = (value, key) => { 11 | if (typeof value === 'function' && key !== 'actual') { 12 | dest[ key ] = (...params) => decorate(value.apply(dest, params)) 13 | } 14 | } 15 | for (let key in dest) { 16 | wrap(dest[ key ], key) 17 | } 18 | for (let key in expectations) { 19 | wrap(expectations[ key ], key) 20 | } 21 | return dest 22 | } 23 | return (...params) => decorate(expect(...params)) 24 | } 25 | 26 | export default addExpectations 27 | -------------------------------------------------------------------------------- /src/util/__tests__/prefixName.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import prefixName from '../prefixName' 3 | 4 | describe('prefixName', () => { 5 | it('should concat sectionPrefix and name', () => { 6 | const context = { 7 | _reduxForm: { 8 | sectionPrefix: 'foo' 9 | } 10 | } 11 | expect(prefixName(context,'bar')).toBe('foo.bar') 12 | }) 13 | 14 | it('should ignore empty sectionPrefix', () => { 15 | const context = { 16 | _reduxForm: { 17 | sectionPrefix: undefined 18 | } 19 | } 20 | expect(prefixName(context, 'bar')).toBe('bar') 21 | }) 22 | 23 | it('should not prefix array fields', () => { 24 | const context = { 25 | _reduxForm: { 26 | sectionPrefix: 'foo' 27 | } 28 | } 29 | expect(prefixName(context, 'bar.bar[0]')).toBe('bar.bar[0]') 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /examples/immutable/src/validate.js: -------------------------------------------------------------------------------- 1 | const validate = values => { 2 | // IMPORTANT: values is an Immutable.Map here! 3 | const errors = {} 4 | if (!values.get('username')) { 5 | errors.username = 'Required' 6 | } else if (values.get('username').length > 15) { 7 | errors.username = 'Must be 15 characters or less' 8 | } 9 | if (!values.get('email')) { 10 | errors.email = 'Required' 11 | } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.get('email'))) { 12 | errors.email = 'Invalid email address' 13 | } 14 | if (!values.get('age')) { 15 | errors.age = 'Required' 16 | } else if (isNaN(Number(values.get('age')))) { 17 | errors.age = 'Must be a number' 18 | } else if (Number(values.get('age')) < 18) { 19 | errors.age = 'Sorry, you must be at least 18 years old' 20 | } 21 | return errors 22 | } 23 | 24 | export default validate 25 | -------------------------------------------------------------------------------- /src/structure/plain/expectations.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import { isObject } from 'lodash' 3 | 4 | const expectations = { 5 | toBeAMap() { 6 | expect.assert( 7 | isObject(this.actual), 8 | 'expected %s to be an object', 9 | this.actual 10 | ) 11 | return this 12 | }, 13 | 14 | toBeAList() { 15 | expect.assert( 16 | Array.isArray(this.actual), 17 | 'expected %s to be an array', 18 | this.actual 19 | ) 20 | return this 21 | }, 22 | 23 | toBeSize(size) { 24 | expect.assert( 25 | this.actual && Object.keys(this.actual).length === size, 26 | 'expected %s to contain %s elements', 27 | this.actual, 28 | size 29 | ) 30 | return this 31 | }, 32 | 33 | toEqualMap(expected) { 34 | return expect(this.actual).toEqual(expected) 35 | } 36 | } 37 | 38 | export default expectations 39 | -------------------------------------------------------------------------------- /src/structure/plain/splice.js: -------------------------------------------------------------------------------- 1 | const splice = (array, index, removeNum, value) => { 2 | array = array || [] 3 | 4 | if (index < array.length) { 5 | if (value === undefined && !removeNum) { // inserting undefined 6 | const copy = [ ...array ] 7 | copy.splice(index, 0, null) 8 | copy[ index ] = undefined 9 | return copy 10 | } 11 | if (value != null) { 12 | const copy = [ ...array ] 13 | copy.splice(index, removeNum, value) // removing and adding 14 | return copy 15 | } 16 | const copy = [ ...array ] 17 | copy.splice(index, removeNum) // removing 18 | return copy 19 | } 20 | if (removeNum) { // trying to remove non-existant item: return original array 21 | return array 22 | } 23 | // trying to add outside of range: just set value 24 | const copy = [ ...array ] 25 | copy[ index ] = value 26 | return copy 27 | } 28 | 29 | export default splice 30 | -------------------------------------------------------------------------------- /examples/wizard/src/WizardFormFirstPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Field, reduxForm } from 'redux-form' 3 | import validate from './validate' 4 | import renderField from './renderField' 5 | 6 | const WizardFormFirstPage = (props) => { 7 | const { handleSubmit } = props 8 | return ( 9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 | ) 17 | } 18 | 19 | export default reduxForm({ 20 | form: 'wizard', // <------ same form name 21 | destroyOnUnmount: false, // <------ preserve form data 22 | forceUnregisterOnUnmount: true, // <------ unregister fields on unmount 23 | validate 24 | })(WizardFormFirstPage) 25 | -------------------------------------------------------------------------------- /src/structure/immutable/index.js: -------------------------------------------------------------------------------- 1 | import { Map, Iterable, List, fromJS } from 'immutable' 2 | import { toPath } from 'lodash' 3 | import deepEqual from './deepEqual' 4 | import setIn from './setIn' 5 | import splice from './splice' 6 | import plainGetIn from '../plain/getIn' 7 | 8 | const structure = { 9 | empty: Map(), 10 | emptyList: List(), 11 | getIn: (state, field) => 12 | Map.isMap(state) || List.isList(state) ? state.getIn(toPath(field)) : plainGetIn(state, field), 13 | setIn, 14 | deepEqual, 15 | deleteIn: (state, field) => state.deleteIn(toPath(field)), 16 | fromJS: jsValue => fromJS(jsValue, (key, value) => 17 | Iterable.isIndexed(value) ? value.toList() : value.toMap()), 18 | size: list => list ? list.size : 0, 19 | some: (iterable, callback) => Iterable.isIterable(iterable) ? iterable.some(callback) : false, 20 | splice, 21 | toJS: value => Iterable.isIterable(value) ? value.toJS() : value 22 | } 23 | 24 | export default structure 25 | -------------------------------------------------------------------------------- /src/__tests__/reducer.clearAsyncError.spec.js: -------------------------------------------------------------------------------- 1 | import { clearAsyncError } from '../actions' 2 | 3 | const describeClearAsyncError = (reducer, expect, { fromJS }) => () => { 4 | it('should do nothing on clear submit with no previous state', () => { 5 | const state = reducer(undefined, clearAsyncError('foo')) 6 | expect(state) 7 | .toEqualMap({ 8 | foo: {} 9 | }) 10 | }) 11 | 12 | it('should clear async errors with previous state', () => { 13 | const state = reducer(fromJS({ 14 | myForm: { 15 | asyncErrors: { 16 | foo: 'some validation message here', 17 | baar: 'second validation message' 18 | } 19 | } 20 | }), clearAsyncError('myForm', 'foo')) 21 | expect(state) 22 | .toEqualMap({ 23 | myForm: { 24 | asyncErrors: { 25 | baar: 'second validation message' 26 | } 27 | } 28 | }) 29 | }) 30 | } 31 | 32 | export default describeClearAsyncError 33 | -------------------------------------------------------------------------------- /src/formValueSelector.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant' 2 | import plain from './structure/plain' 3 | 4 | const createFormValueSelector = 5 | ({ getIn }) => 6 | (form, getFormState = state => getIn(state, 'form')) => { 7 | invariant(form, 'Form value must be specified') 8 | return (state, ...fields) => { 9 | invariant(fields.length, 'No fields specified') 10 | return fields.length === 1 ? 11 | // only selecting one field, so return its value 12 | getIn(getFormState(state), `${form}.values.${fields[ 0 ]}`) : 13 | // selecting many fields, so return an object of field values 14 | fields.reduce((accumulator, field) => { 15 | const value = getIn(getFormState(state), `${form}.values.${field}`) 16 | return value === undefined ? 17 | accumulator : 18 | plain.setIn(accumulator, field, value) 19 | }, {}) 20 | } 21 | } 22 | 23 | export default createFormValueSelector 24 | 25 | -------------------------------------------------------------------------------- /src/immutable.js: -------------------------------------------------------------------------------- 1 | import createAll from './createAll' 2 | import immutable from './structure/immutable' 3 | 4 | export const { 5 | actionTypes, 6 | arrayInsert, 7 | arrayMove, 8 | arrayPop, 9 | arrayPush, 10 | arrayRemove, 11 | arrayRemoveAll, 12 | arrayShift, 13 | arraySplice, 14 | arraySwap, 15 | arrayUnshift, 16 | autofill, 17 | blur, 18 | change, 19 | destroy, 20 | Field, 21 | Fields, 22 | FieldArray, 23 | Form, 24 | FormSection, 25 | focus, 26 | formValueSelector, 27 | getFormValues, 28 | initialize, 29 | isDirty, 30 | isInvalid, 31 | isPristine, 32 | isValid, 33 | isSubmitting, 34 | hasSubmitSucceeded, 35 | hasSubmitFailed, 36 | propTypes, 37 | reducer, 38 | reduxForm, 39 | reset, 40 | setSubmitFailed, 41 | setSubmitSucceeded, 42 | startAsyncValidation, 43 | startSubmit, 44 | stopAsyncValidation, 45 | stopSubmit, 46 | submit, 47 | SubmissionError, 48 | touch, 49 | untouch, 50 | values 51 | } = createAll(immutable) 52 | -------------------------------------------------------------------------------- /src/deleteInWithCleanUp.js: -------------------------------------------------------------------------------- 1 | import { toPath } from 'lodash' 2 | 3 | const createDeleteInWithCleanUp = ({ deepEqual, empty, getIn, deleteIn, setIn }) => { 4 | 5 | const deleteInWithCleanUp = (state, path) => { 6 | if (path[ path.length - 1 ] === ']') { // array path 7 | const pathTokens = toPath(path) 8 | pathTokens.pop() 9 | const parent = getIn(state, pathTokens.join('.')) 10 | return parent ? setIn(state, path, undefined) : state 11 | } 12 | const result = deleteIn(state, path) 13 | const dotIndex = path.lastIndexOf('.') 14 | if (dotIndex > 0) { 15 | const parentPath = path.substring(0, dotIndex) 16 | if (parentPath[ parentPath.length - 1 ] !== ']') { 17 | const parent = getIn(result, parentPath) 18 | if (deepEqual(parent, empty)) { 19 | return deleteInWithCleanUp(result, parentPath) 20 | } 21 | } 22 | } 23 | return result 24 | } 25 | 26 | return deleteInWithCleanUp 27 | } 28 | 29 | export default createDeleteInWithCleanUp 30 | -------------------------------------------------------------------------------- /src/util/__tests__/eventMocks.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import * as mocks from '../eventMocks' 3 | 4 | describe('#eventMocks', () => { 5 | it('should create a mock with identity functions', () => { 6 | const event = mocks.valueMock('value') 7 | 8 | expect(event.preventDefault).toBeA('function') 9 | expect(event.stopPropagation).toBeA('function') 10 | expect(event.preventDefault('id')).toEqual('id') 11 | expect(event.stopPropagation('id')).toEqual('id') 12 | }) 13 | 14 | it('should create a value mock', () => { 15 | const event = mocks.valueMock('value') 16 | 17 | expect(event.target.value).toEqual('value') 18 | }) 19 | 20 | it('should create a drag start mock', () => { 21 | const fn = id => id 22 | const event = mocks.dragStartMock(fn) 23 | 24 | expect(event.dataTransfer.setData).toBe(fn) 25 | }) 26 | 27 | it('should create a drop mock', () => { 28 | const fn = id => id 29 | const event = mocks.dropMock(fn) 30 | 31 | expect(event.dataTransfer.getData).toBe(fn) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /examples/remoteSubmit/src/RemoteSubmit.md: -------------------------------------------------------------------------------- 1 | # Remote Submit Example 2 | 3 | This example demonstrates how a form may be submitted by dispatching a `SUBMIT` action from an 4 | unrelated component or middleware. 5 | 6 | The "Submit" button you see here is not connected to the form component in any way; it only 7 | dispatches an action via Redux. 8 | 9 | Notice that for this to work, the submit function must be given to the form component via either 10 | a config value passed to `reduxForm()` or via a prop. 11 | 12 | ## Running this example locally 13 | 14 | To run this example locally on your machine clone the `redux-form` repository, 15 | then `cd redux-form` to change to the repo directory, and run `npm install`. 16 | 17 | Then run `npm run example:remoteSubmit` or manually run the 18 | following commands: 19 | ``` 20 | cd ./examples/remoteSubmit 21 | npm install 22 | npm start 23 | ``` 24 | 25 | ## How to use the form below: 26 | 27 | * Usernames that will pass validation: `john`, `paul`, `george`, or `ringo`. 28 | * Valid password for all users: `redux-form`. 29 | 30 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import createAll from './createAll' 2 | import plain from './structure/plain' 3 | 4 | export const { 5 | actionTypes, 6 | arrayInsert, 7 | arrayMove, 8 | arrayPop, 9 | arrayPush, 10 | arrayRemove, 11 | arrayRemoveAll, 12 | arrayShift, 13 | arraySplice, 14 | arraySwap, 15 | arrayUnshift, 16 | blur, 17 | autofill, 18 | change, 19 | destroy, 20 | Field, 21 | Fields, 22 | FieldArray, 23 | Form, 24 | FormSection, 25 | focus, 26 | formValueSelector, 27 | getFormValues, 28 | getFormSyncErrors, 29 | getFormSubmitErrors, 30 | initialize, 31 | isDirty, 32 | isInvalid, 33 | isPristine, 34 | isValid, 35 | isSubmitting, 36 | hasSubmitSucceeded, 37 | hasSubmitFailed, 38 | propTypes, 39 | reducer, 40 | reduxForm, 41 | registerField, 42 | reset, 43 | setSubmitFailed, 44 | setSubmitSucceeded, 45 | startAsyncValidation, 46 | startSubmit, 47 | stopAsyncValidation, 48 | stopSubmit, 49 | submit, 50 | SubmissionError, 51 | touch, 52 | unregisterField, 53 | untouch, 54 | values 55 | } = createAll(plain) 56 | -------------------------------------------------------------------------------- /docs/api/SubmissionError.md: -------------------------------------------------------------------------------- 1 | # `SubmissionError` 2 | 3 | [`View source on GitHub`](https://github.com/erikras/redux-form/blob/master/src/SubmissionError.js) 4 | 5 | A throwable error that is used to return submit validation errors from `onSubmit`. The purpose 6 | being to distinguish promise rejection because of validation errors from promise rejection because 7 | of AJAX I/O problems or other server errors. 8 | 9 | ## Importing 10 | 11 | ```javascript 12 | var SubmissionError = require('redux-form').SubmissionError; // ES5 13 | ``` 14 | ```javascript 15 | import { SubmissionError } from 'redux-form'; // ES6 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```js 21 | 22 | ajax.send(values) // however you send data to your server... 23 | .catch(error => { 24 | // how you pass server-side validation errors back is up to you 25 | if(error.validationErrors) { 26 | throw new SubmissionError(error.validationErrors) 27 | } else { 28 | // what you do about other communication errors is up to you 29 | } 30 | }) 31 | }/> 32 | ``` 33 | 34 | -------------------------------------------------------------------------------- /examples/wizard/src/Wizard.md: -------------------------------------------------------------------------------- 1 | # Wizard Form 2 | 3 | One common UI design pattern is to separate a single form out into sepearate pages of inputs, 4 | commonly known as a Wizard. There are several ways that this could be accomplished using 5 | `redux-form`, but the simplest and recommended way is to follow these instructions: 6 | 7 | * Connect each page with `reduxForm()` to the same form name. 8 | * Specify the `destroyOnUnmount: false` flag to preserve form data across form component unmounts. 9 | * You may specify sync validation for the entire form 10 | * Use `onSubmit` to transition forward to the next page; this forces validation to run. 11 | 12 | Things that are up to you to implement: 13 | 14 | * Call `props.destroy()` manually after a successful submit. 15 | 16 | ## Running this example locally 17 | 18 | To run this example locally on your machine clone the `redux-form` repository, 19 | then `cd redux-form` to change to the repo directory, and run `npm install`. 20 | 21 | Then run `npm run example:wizard` or manually run the 22 | following commands: 23 | ``` 24 | cd ./examples/wizard 25 | npm install 26 | npm start 27 | ``` 28 | 29 | -------------------------------------------------------------------------------- /docs/faq/ReactNative.md: -------------------------------------------------------------------------------- 1 | # Does `redux-form` work with React Native? 2 | 3 | Yes, it does. 4 | 5 | #### If your are using all `react-native@18+`, `redux@4+` and `npm@3+` 6 | 7 | Just import it as usual: 8 | 9 | ```javascript 10 | import {reduxForm} from 'redux-form'; 11 | ``` 12 | 13 | #### Else 14 | 15 | All you have to do is use: 16 | 17 | ```javascript 18 | import {reduxForm} from 'redux-form/native'; 19 | ``` 20 | instead of 21 | ```javascript 22 | import {reduxForm} from 'redux-form'; 23 | ``` 24 | 25 | #### Note: 26 | 27 | `react-redux/native` is deprecated in `react-redux@4+`, it only appears in `react-redux@3` 28 | 29 | #### See Also: 30 | 31 | 1. [[react-native] Depend on npm "react" instead of shipping with a vendored copy #2985](https://github.com/facebook/react-native/issues/2985) 32 | 2. [[react-redux] Update React Native installation instructions #236](https://github.com/rackt/react-redux/issues/236) 33 | 3. [Until React Native works on top of React instead of shipping a fork of React, you’ll need to keep using React Redux 3.x branch and documentation.](https://github.com/erikras/redux-form/issues/473#issuecomment-167690524) 34 | -------------------------------------------------------------------------------- /examples/simple/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'babel-polyfill', 8 | 'eventsource-polyfill', // necessary for hot reloading with IE 9 | 'webpack-hot-middleware/client', 10 | './src/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/dist/' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.HotModuleReplacementPlugin() 20 | ], 21 | resolve: { 22 | modulesDirectories: [ 23 | 'src', 24 | 'node_modules' 25 | ], 26 | extensions: [ '', '.json', '.js' ] 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.jsx?/, 32 | loaders: [ 'babel', 'eslint' ], 33 | include: path.join(__dirname, 'src') 34 | }, 35 | { 36 | test: /\.json$/, 37 | loader: 'json-loader' 38 | }, 39 | { 40 | test: /\.md/, 41 | loaders: [ "html-loader", "markdown-loader" ] 42 | } 43 | ] 44 | } 45 | }; -------------------------------------------------------------------------------- /examples/wizard/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'babel-polyfill', 8 | 'eventsource-polyfill', // necessary for hot reloading with IE 9 | 'webpack-hot-middleware/client', 10 | './src/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/dist/' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.HotModuleReplacementPlugin() 20 | ], 21 | resolve: { 22 | modulesDirectories: [ 23 | 'src', 24 | 'node_modules' 25 | ], 26 | extensions: [ '', '.json', '.js' ] 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.jsx?/, 32 | loaders: [ 'babel', 'eslint' ], 33 | include: path.join(__dirname, 'src') 34 | }, 35 | { 36 | test: /\.json$/, 37 | loader: 'json-loader' 38 | }, 39 | { 40 | test: /\.md/, 41 | loaders: [ "html-loader", "markdown-loader" ] 42 | } 43 | ] 44 | } 45 | }; -------------------------------------------------------------------------------- /src/__tests__/reducer.startSubmit.spec.js: -------------------------------------------------------------------------------- 1 | import { startSubmit } from '../actions' 2 | 3 | const describeStartSubmit = (reducer, expect, { fromJS }) => () => { 4 | it('should set submitting on startSubmit', () => { 5 | const state = reducer(fromJS({ 6 | foo: { 7 | doesnt: 'matter', 8 | should: 'notchange' 9 | } 10 | }), startSubmit('foo')) 11 | expect(state) 12 | .toEqualMap({ 13 | foo: { 14 | doesnt: 'matter', 15 | should: 'notchange', 16 | submitting: true 17 | } 18 | }) 19 | }) 20 | 21 | it('should set submitting on startSubmit, and NOT reset submitFailed', () => { 22 | const state = reducer(fromJS({ 23 | foo: { 24 | doesnt: 'matter', 25 | should: 'notchange', 26 | submitFailed: true 27 | } 28 | }), startSubmit('foo')) 29 | expect(state) 30 | .toEqualMap({ 31 | foo: { 32 | doesnt: 'matter', 33 | should: 'notchange', 34 | submitting: true, 35 | submitFailed: true 36 | } 37 | }) 38 | }) 39 | } 40 | 41 | export default describeStartSubmit 42 | -------------------------------------------------------------------------------- /src/events/__tests__/silenceEvent.spec.js: -------------------------------------------------------------------------------- 1 | import expect, { createSpy } from 'expect' 2 | import { noop } from 'lodash' 3 | import silenceEvent from '../silenceEvent' 4 | 5 | describe('silenceEvent', () => { 6 | it('should return false if not an event', () => { 7 | expect(silenceEvent(undefined)).toBe(false) 8 | expect(silenceEvent(null)).toBe(false) 9 | expect(silenceEvent(true)).toBe(false) 10 | expect(silenceEvent(42)).toBe(false) 11 | expect(silenceEvent({})).toBe(false) 12 | expect(silenceEvent([])).toBe(false) 13 | expect(silenceEvent(noop)).toBe(false) 14 | }) 15 | 16 | it('should return true if an event', () => { 17 | expect(silenceEvent({ 18 | preventDefault: noop, 19 | stopPropagation: noop 20 | })).toBe(true) 21 | }) 22 | 23 | it('should call preventDefault and stopPropagation', () => { 24 | const preventDefault = createSpy() 25 | const stopPropagation = createSpy() 26 | 27 | silenceEvent({ 28 | preventDefault, 29 | stopPropagation 30 | }) 31 | expect(preventDefault).toHaveBeenCalled() 32 | expect(stopPropagation).toNotHaveBeenCalled() 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Erik Rasmussen 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 | -------------------------------------------------------------------------------- /examples/fieldArrays/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'babel-polyfill', 8 | 'eventsource-polyfill', // necessary for hot reloading with IE 9 | 'webpack-hot-middleware/client', 10 | './src/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/dist/' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.HotModuleReplacementPlugin() 20 | ], 21 | resolve: { 22 | modulesDirectories: [ 23 | 'src', 24 | 'node_modules' 25 | ], 26 | extensions: [ '', '.json', '.js' ] 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.jsx?/, 32 | loaders: [ 'babel', 'eslint' ], 33 | include: path.join(__dirname, 'src') 34 | }, 35 | { 36 | test: /\.json$/, 37 | loader: 'json-loader' 38 | }, 39 | { 40 | test: /\.md/, 41 | loaders: [ "html-loader", "markdown-loader" ] 42 | } 43 | ] 44 | } 45 | }; -------------------------------------------------------------------------------- /examples/immutable/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'babel-polyfill', 8 | 'eventsource-polyfill', // necessary for hot reloading with IE 9 | 'webpack-hot-middleware/client', 10 | './src/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/dist/' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.HotModuleReplacementPlugin() 20 | ], 21 | resolve: { 22 | modulesDirectories: [ 23 | 'src', 24 | 'node_modules' 25 | ], 26 | extensions: [ '', '.json', '.js' ] 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.jsx?/, 32 | loaders: [ 'babel', 'eslint' ], 33 | include: path.join(__dirname, 'src') 34 | }, 35 | { 36 | test: /\.json$/, 37 | loader: 'json-loader' 38 | }, 39 | { 40 | test: /\.md/, 41 | loaders: [ "html-loader", "markdown-loader" ] 42 | } 43 | ] 44 | } 45 | }; -------------------------------------------------------------------------------- /examples/material-ui/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'babel-polyfill', 8 | 'eventsource-polyfill', // necessary for hot reloading with IE 9 | 'webpack-hot-middleware/client', 10 | './src/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/dist/' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.HotModuleReplacementPlugin() 20 | ], 21 | resolve: { 22 | modulesDirectories: [ 23 | 'src', 24 | 'node_modules' 25 | ], 26 | extensions: [ '', '.json', '.js' ] 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.jsx?/, 32 | loaders: [ 'babel', 'eslint' ], 33 | include: path.join(__dirname, 'src') 34 | }, 35 | { 36 | test: /\.json$/, 37 | loader: 'json-loader' 38 | }, 39 | { 40 | test: /\.md/, 41 | loaders: [ "html-loader", "markdown-loader" ] 42 | } 43 | ] 44 | } 45 | }; -------------------------------------------------------------------------------- /examples/normalizing/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'babel-polyfill', 8 | 'eventsource-polyfill', // necessary for hot reloading with IE 9 | 'webpack-hot-middleware/client', 10 | './src/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/dist/' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.HotModuleReplacementPlugin() 20 | ], 21 | resolve: { 22 | modulesDirectories: [ 23 | 'src', 24 | 'node_modules' 25 | ], 26 | extensions: [ '', '.json', '.js' ] 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.jsx?/, 32 | loaders: [ 'babel', 'eslint' ], 33 | include: path.join(__dirname, 'src') 34 | }, 35 | { 36 | test: /\.json$/, 37 | loader: 'json-loader' 38 | }, 39 | { 40 | test: /\.md/, 41 | loaders: [ "html-loader", "markdown-loader" ] 42 | } 43 | ] 44 | } 45 | }; -------------------------------------------------------------------------------- /examples/asyncValidation/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'babel-polyfill', 8 | 'eventsource-polyfill', // necessary for hot reloading with IE 9 | 'webpack-hot-middleware/client', 10 | './src/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/dist/' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.HotModuleReplacementPlugin() 20 | ], 21 | resolve: { 22 | modulesDirectories: [ 23 | 'src', 24 | 'node_modules' 25 | ], 26 | extensions: [ '', '.json', '.js' ] 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.jsx?/, 32 | loaders: [ 'babel', 'eslint' ], 33 | include: path.join(__dirname, 'src') 34 | }, 35 | { 36 | test: /\.json$/, 37 | loader: 'json-loader' 38 | }, 39 | { 40 | test: /\.md/, 41 | loaders: [ "html-loader", "markdown-loader" ] 42 | } 43 | ] 44 | } 45 | }; -------------------------------------------------------------------------------- /examples/remoteSubmit/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'babel-polyfill', 8 | 'eventsource-polyfill', // necessary for hot reloading with IE 9 | 'webpack-hot-middleware/client', 10 | './src/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/dist/' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.HotModuleReplacementPlugin() 20 | ], 21 | resolve: { 22 | modulesDirectories: [ 23 | 'src', 24 | 'node_modules' 25 | ], 26 | extensions: [ '', '.json', '.js' ] 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.jsx?/, 32 | loaders: [ 'babel', 'eslint' ], 33 | include: path.join(__dirname, 'src') 34 | }, 35 | { 36 | test: /\.json$/, 37 | loader: 'json-loader' 38 | }, 39 | { 40 | test: /\.md/, 41 | loaders: [ "html-loader", "markdown-loader" ] 42 | } 43 | ] 44 | } 45 | }; -------------------------------------------------------------------------------- /examples/submitValidation/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'babel-polyfill', 8 | 'eventsource-polyfill', // necessary for hot reloading with IE 9 | 'webpack-hot-middleware/client', 10 | './src/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/dist/' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.HotModuleReplacementPlugin() 20 | ], 21 | resolve: { 22 | modulesDirectories: [ 23 | 'src', 24 | 'node_modules' 25 | ], 26 | extensions: [ '', '.json', '.js' ] 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.jsx?/, 32 | loaders: [ 'babel', 'eslint' ], 33 | include: path.join(__dirname, 'src') 34 | }, 35 | { 36 | test: /\.json$/, 37 | loader: 'json-loader' 38 | }, 39 | { 40 | test: /\.md/, 41 | loaders: [ "html-loader", "markdown-loader" ] 42 | } 43 | ] 44 | } 45 | }; -------------------------------------------------------------------------------- /examples/syncValidation/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'babel-polyfill', 8 | 'eventsource-polyfill', // necessary for hot reloading with IE 9 | 'webpack-hot-middleware/client', 10 | './src/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/dist/' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.HotModuleReplacementPlugin() 20 | ], 21 | resolve: { 22 | modulesDirectories: [ 23 | 'src', 24 | 'node_modules' 25 | ], 26 | extensions: [ '', '.json', '.js' ] 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.jsx?/, 32 | loaders: [ 'babel', 'eslint' ], 33 | include: path.join(__dirname, 'src') 34 | }, 35 | { 36 | test: /\.json$/, 37 | loader: 'json-loader' 38 | }, 39 | { 40 | test: /\.md/, 41 | loaders: [ "html-loader", "markdown-loader" ] 42 | } 43 | ] 44 | } 45 | }; -------------------------------------------------------------------------------- /examples/fieldLevelValidation/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'babel-polyfill', 8 | 'eventsource-polyfill', // necessary for hot reloading with IE 9 | 'webpack-hot-middleware/client', 10 | './src/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/dist/' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.HotModuleReplacementPlugin() 20 | ], 21 | resolve: { 22 | modulesDirectories: [ 23 | 'src', 24 | 'node_modules' 25 | ], 26 | extensions: [ '', '.json', '.js' ] 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.jsx?/, 32 | loaders: [ 'babel', 'eslint' ], 33 | include: path.join(__dirname, 'src') 34 | }, 35 | { 36 | test: /\.json$/, 37 | loader: 'json-loader' 38 | }, 39 | { 40 | test: /\.md/, 41 | loaders: [ "html-loader", "markdown-loader" ] 42 | } 43 | ] 44 | } 45 | }; -------------------------------------------------------------------------------- /examples/initializeFromState/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'babel-polyfill', 8 | 'eventsource-polyfill', // necessary for hot reloading with IE 9 | 'webpack-hot-middleware/client', 10 | './src/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/dist/' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.HotModuleReplacementPlugin() 20 | ], 21 | resolve: { 22 | modulesDirectories: [ 23 | 'src', 24 | 'node_modules' 25 | ], 26 | extensions: [ '', '.json', '.js' ] 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.jsx?/, 32 | loaders: [ 'babel', 'eslint' ], 33 | include: path.join(__dirname, 'src') 34 | }, 35 | { 36 | test: /\.json$/, 37 | loader: 'json-loader' 38 | }, 39 | { 40 | test: /\.md/, 41 | loaders: [ "html-loader", "markdown-loader" ] 42 | } 43 | ] 44 | } 45 | }; -------------------------------------------------------------------------------- /examples/selectingFormValues/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'babel-polyfill', 8 | 'eventsource-polyfill', // necessary for hot reloading with IE 9 | 'webpack-hot-middleware/client', 10 | './src/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/dist/' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.HotModuleReplacementPlugin() 20 | ], 21 | resolve: { 22 | modulesDirectories: [ 23 | 'src', 24 | 'node_modules' 25 | ], 26 | extensions: [ '', '.json', '.js' ] 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.jsx?/, 32 | loaders: [ 'babel', 'eslint' ], 33 | include: path.join(__dirname, 'src') 34 | }, 35 | { 36 | test: /\.json$/, 37 | loader: 'json-loader' 38 | }, 39 | { 40 | test: /\.md/, 41 | loaders: [ "html-loader", "markdown-loader" ] 42 | } 43 | ] 44 | } 45 | }; -------------------------------------------------------------------------------- /src/events/getValue.js: -------------------------------------------------------------------------------- 1 | import isEvent from './isEvent' 2 | 3 | const getSelectedValues = options => { 4 | const result = [] 5 | if (options) { 6 | for (let index = 0; index < options.length; index++) { 7 | const option = options[ index ] 8 | if (option.selected) { 9 | result.push(option.value) 10 | } 11 | } 12 | } 13 | return result 14 | } 15 | 16 | const getValue = (event, isReactNative) => { 17 | if (isEvent(event)) { 18 | if (!isReactNative && event.nativeEvent && event.nativeEvent.text !== undefined) { 19 | return event.nativeEvent.text 20 | } 21 | if (isReactNative && event.nativeEvent !== undefined) { 22 | return event.nativeEvent.text 23 | } 24 | const { target: { type, value, checked, files }, dataTransfer } = event 25 | if (type === 'checkbox') { 26 | return checked 27 | } 28 | if (type === 'file') { 29 | return files || dataTransfer && dataTransfer.files 30 | } 31 | if (type === 'select-multiple') { 32 | return getSelectedValues(event.target.options) 33 | } 34 | return value 35 | } 36 | return event 37 | } 38 | 39 | export default getValue 40 | -------------------------------------------------------------------------------- /examples/remoteSubmit/src/RemoteSubmitForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Field, reduxForm } from 'redux-form' 3 | import submit from './submit' 4 | 5 | const renderField = ({ input, label, type, meta: { touched, error } }) => ( 6 |
7 | 8 |
9 | 10 | {touched && error && {error}} 11 |
12 |
13 | ) 14 | 15 | const RemoteSubmitForm = (props) => { 16 | const { error, handleSubmit } = props 17 | return ( 18 |
19 | 20 | 21 | {error && {error}} 22 |
23 | No submit button in the form. The submit button below is a separate unlinked component. 24 |
25 | 26 | ) 27 | } 28 | 29 | export default reduxForm({ 30 | form: 'remoteSubmit', // a unique identifier for this form 31 | onSubmit: submit // submit function must be passed to onSubmit 32 | })(RemoteSubmitForm) 33 | -------------------------------------------------------------------------------- /src/events/__tests__/isEvent.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import { noop } from 'lodash' 3 | import isEvent from '../isEvent' 4 | 5 | describe('isEvent', () => { 6 | it('should return false if event is undefined', () => { 7 | expect(isEvent()).toBe(false) 8 | }) 9 | 10 | it('should return false if event is null', () => { 11 | expect(isEvent(null)).toBe(false) 12 | }) 13 | 14 | it('should return false if event is not an object', () => { 15 | expect(isEvent(42)).toBe(false) 16 | expect(isEvent(true)).toBe(false) 17 | expect(isEvent(false)).toBe(false) 18 | expect(isEvent('not an event')).toBe(false) 19 | }) 20 | 21 | it('should return false if event has no stopPropagation', () => { 22 | expect(isEvent({ 23 | preventDefault: noop 24 | })).toBe(false) 25 | }) 26 | 27 | it('should return false if event has no preventDefault', () => { 28 | expect(isEvent({ 29 | stopPropagation: noop 30 | })).toBe(false) 31 | }) 32 | 33 | it('should return true if event has stopPropagation, and preventDefault', () => { 34 | expect(isEvent({ 35 | stopPropagation: noop, 36 | preventDefault: noop 37 | })).toBe(true) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /examples/submitValidation/src/SubmitValidationForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Field, reduxForm } from 'redux-form' 3 | import submit from './submit' 4 | 5 | const renderField = ({ input, label, type, meta: { touched, error } }) => ( 6 |
7 | 8 |
9 | 10 | {touched && error && {error}} 11 |
12 |
13 | ) 14 | 15 | const SubmitValidationForm = (props) => { 16 | const { error, handleSubmit, pristine, reset, submitting } = props 17 | return ( 18 |
19 | 20 | 21 | {error && {error}} 22 |
23 | 24 | 25 |
26 | 27 | ) 28 | } 29 | 30 | export default reduxForm({ 31 | form: 'submitValidation' // a unique identifier for this form 32 | })(SubmitValidationForm) 33 | -------------------------------------------------------------------------------- /examples/simple/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | 'babel-polyfill', 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/dist/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | NODE_ENV: JSON.stringify('production') 20 | } 21 | }), 22 | new webpack.optimize.UglifyJsPlugin({ 23 | compressor: { 24 | warnings: false 25 | } 26 | }) 27 | ], 28 | resolve: { 29 | modulesDirectories: [ 30 | 'src', 31 | 'node_modules' 32 | ], 33 | extensions: [ '', '.json', '.js' ] 34 | }, 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.js$/, 39 | loaders: [ 'babel' ], 40 | include: path.join(__dirname, 'src') 41 | }, 42 | { 43 | test: /\.json$/, 44 | loader: 'json-loader' 45 | }, 46 | { 47 | test: /\.md/, 48 | loaders: [ "html-loader", "markdown-loader" ] 49 | } 50 | ] 51 | } 52 | }; -------------------------------------------------------------------------------- /examples/wizard/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | 'babel-polyfill', 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/dist/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | NODE_ENV: JSON.stringify('production') 20 | } 21 | }), 22 | new webpack.optimize.UglifyJsPlugin({ 23 | compressor: { 24 | warnings: false 25 | } 26 | }) 27 | ], 28 | resolve: { 29 | modulesDirectories: [ 30 | 'src', 31 | 'node_modules' 32 | ], 33 | extensions: [ '', '.json', '.js' ] 34 | }, 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.js$/, 39 | loaders: [ 'babel' ], 40 | include: path.join(__dirname, 'src') 41 | }, 42 | { 43 | test: /\.json$/, 44 | loader: 'json-loader' 45 | }, 46 | { 47 | test: /\.md/, 48 | loaders: [ "html-loader", "markdown-loader" ] 49 | } 50 | ] 51 | } 52 | }; -------------------------------------------------------------------------------- /examples/fieldArrays/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | 'babel-polyfill', 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/dist/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | NODE_ENV: JSON.stringify('production') 20 | } 21 | }), 22 | new webpack.optimize.UglifyJsPlugin({ 23 | compressor: { 24 | warnings: false 25 | } 26 | }) 27 | ], 28 | resolve: { 29 | modulesDirectories: [ 30 | 'src', 31 | 'node_modules' 32 | ], 33 | extensions: [ '', '.json', '.js' ] 34 | }, 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.js$/, 39 | loaders: [ 'babel' ], 40 | include: path.join(__dirname, 'src') 41 | }, 42 | { 43 | test: /\.json$/, 44 | loader: 'json-loader' 45 | }, 46 | { 47 | test: /\.md/, 48 | loaders: [ "html-loader", "markdown-loader" ] 49 | } 50 | ] 51 | } 52 | }; -------------------------------------------------------------------------------- /examples/immutable/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | 'babel-polyfill', 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/dist/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | NODE_ENV: JSON.stringify('production') 20 | } 21 | }), 22 | new webpack.optimize.UglifyJsPlugin({ 23 | compressor: { 24 | warnings: false 25 | } 26 | }) 27 | ], 28 | resolve: { 29 | modulesDirectories: [ 30 | 'src', 31 | 'node_modules' 32 | ], 33 | extensions: [ '', '.json', '.js' ] 34 | }, 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.js$/, 39 | loaders: [ 'babel' ], 40 | include: path.join(__dirname, 'src') 41 | }, 42 | { 43 | test: /\.json$/, 44 | loader: 'json-loader' 45 | }, 46 | { 47 | test: /\.md/, 48 | loaders: [ "html-loader", "markdown-loader" ] 49 | } 50 | ] 51 | } 52 | }; -------------------------------------------------------------------------------- /examples/material-ui/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | 'babel-polyfill', 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/dist/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | NODE_ENV: JSON.stringify('production') 20 | } 21 | }), 22 | new webpack.optimize.UglifyJsPlugin({ 23 | compressor: { 24 | warnings: false 25 | } 26 | }) 27 | ], 28 | resolve: { 29 | modulesDirectories: [ 30 | 'src', 31 | 'node_modules' 32 | ], 33 | extensions: [ '', '.json', '.js' ] 34 | }, 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.js$/, 39 | loaders: [ 'babel' ], 40 | include: path.join(__dirname, 'src') 41 | }, 42 | { 43 | test: /\.json$/, 44 | loader: 'json-loader' 45 | }, 46 | { 47 | test: /\.md/, 48 | loaders: [ "html-loader", "markdown-loader" ] 49 | } 50 | ] 51 | } 52 | }; -------------------------------------------------------------------------------- /examples/normalizing/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | 'babel-polyfill', 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/dist/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | NODE_ENV: JSON.stringify('production') 20 | } 21 | }), 22 | new webpack.optimize.UglifyJsPlugin({ 23 | compressor: { 24 | warnings: false 25 | } 26 | }) 27 | ], 28 | resolve: { 29 | modulesDirectories: [ 30 | 'src', 31 | 'node_modules' 32 | ], 33 | extensions: [ '', '.json', '.js' ] 34 | }, 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.js$/, 39 | loaders: [ 'babel' ], 40 | include: path.join(__dirname, 'src') 41 | }, 42 | { 43 | test: /\.json$/, 44 | loader: 'json-loader' 45 | }, 46 | { 47 | test: /\.md/, 48 | loaders: [ "html-loader", "markdown-loader" ] 49 | } 50 | ] 51 | } 52 | }; -------------------------------------------------------------------------------- /examples/asyncValidation/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | 'babel-polyfill', 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/dist/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | NODE_ENV: JSON.stringify('production') 20 | } 21 | }), 22 | new webpack.optimize.UglifyJsPlugin({ 23 | compressor: { 24 | warnings: false 25 | } 26 | }) 27 | ], 28 | resolve: { 29 | modulesDirectories: [ 30 | 'src', 31 | 'node_modules' 32 | ], 33 | extensions: [ '', '.json', '.js' ] 34 | }, 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.js$/, 39 | loaders: [ 'babel' ], 40 | include: path.join(__dirname, 'src') 41 | }, 42 | { 43 | test: /\.json$/, 44 | loader: 'json-loader' 45 | }, 46 | { 47 | test: /\.md/, 48 | loaders: [ "html-loader", "markdown-loader" ] 49 | } 50 | ] 51 | } 52 | }; -------------------------------------------------------------------------------- /examples/remoteSubmit/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | 'babel-polyfill', 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/dist/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | NODE_ENV: JSON.stringify('production') 20 | } 21 | }), 22 | new webpack.optimize.UglifyJsPlugin({ 23 | compressor: { 24 | warnings: false 25 | } 26 | }) 27 | ], 28 | resolve: { 29 | modulesDirectories: [ 30 | 'src', 31 | 'node_modules' 32 | ], 33 | extensions: [ '', '.json', '.js' ] 34 | }, 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.js$/, 39 | loaders: [ 'babel' ], 40 | include: path.join(__dirname, 'src') 41 | }, 42 | { 43 | test: /\.json$/, 44 | loader: 'json-loader' 45 | }, 46 | { 47 | test: /\.md/, 48 | loaders: [ "html-loader", "markdown-loader" ] 49 | } 50 | ] 51 | } 52 | }; -------------------------------------------------------------------------------- /examples/submitValidation/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | 'babel-polyfill', 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/dist/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | NODE_ENV: JSON.stringify('production') 20 | } 21 | }), 22 | new webpack.optimize.UglifyJsPlugin({ 23 | compressor: { 24 | warnings: false 25 | } 26 | }) 27 | ], 28 | resolve: { 29 | modulesDirectories: [ 30 | 'src', 31 | 'node_modules' 32 | ], 33 | extensions: [ '', '.json', '.js' ] 34 | }, 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.js$/, 39 | loaders: [ 'babel' ], 40 | include: path.join(__dirname, 'src') 41 | }, 42 | { 43 | test: /\.json$/, 44 | loader: 'json-loader' 45 | }, 46 | { 47 | test: /\.md/, 48 | loaders: [ "html-loader", "markdown-loader" ] 49 | } 50 | ] 51 | } 52 | }; -------------------------------------------------------------------------------- /examples/syncValidation/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | 'babel-polyfill', 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/dist/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | NODE_ENV: JSON.stringify('production') 20 | } 21 | }), 22 | new webpack.optimize.UglifyJsPlugin({ 23 | compressor: { 24 | warnings: false 25 | } 26 | }) 27 | ], 28 | resolve: { 29 | modulesDirectories: [ 30 | 'src', 31 | 'node_modules' 32 | ], 33 | extensions: [ '', '.json', '.js' ] 34 | }, 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.js$/, 39 | loaders: [ 'babel' ], 40 | include: path.join(__dirname, 'src') 41 | }, 42 | { 43 | test: /\.json$/, 44 | loader: 'json-loader' 45 | }, 46 | { 47 | test: /\.md/, 48 | loaders: [ "html-loader", "markdown-loader" ] 49 | } 50 | ] 51 | } 52 | }; -------------------------------------------------------------------------------- /examples/fieldLevelValidation/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | 'babel-polyfill', 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/dist/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | NODE_ENV: JSON.stringify('production') 20 | } 21 | }), 22 | new webpack.optimize.UglifyJsPlugin({ 23 | compressor: { 24 | warnings: false 25 | } 26 | }) 27 | ], 28 | resolve: { 29 | modulesDirectories: [ 30 | 'src', 31 | 'node_modules' 32 | ], 33 | extensions: [ '', '.json', '.js' ] 34 | }, 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.js$/, 39 | loaders: [ 'babel' ], 40 | include: path.join(__dirname, 'src') 41 | }, 42 | { 43 | test: /\.json$/, 44 | loader: 'json-loader' 45 | }, 46 | { 47 | test: /\.md/, 48 | loaders: [ "html-loader", "markdown-loader" ] 49 | } 50 | ] 51 | } 52 | }; -------------------------------------------------------------------------------- /examples/initializeFromState/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | 'babel-polyfill', 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/dist/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | NODE_ENV: JSON.stringify('production') 20 | } 21 | }), 22 | new webpack.optimize.UglifyJsPlugin({ 23 | compressor: { 24 | warnings: false 25 | } 26 | }) 27 | ], 28 | resolve: { 29 | modulesDirectories: [ 30 | 'src', 31 | 'node_modules' 32 | ], 33 | extensions: [ '', '.json', '.js' ] 34 | }, 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.js$/, 39 | loaders: [ 'babel' ], 40 | include: path.join(__dirname, 'src') 41 | }, 42 | { 43 | test: /\.json$/, 44 | loader: 'json-loader' 45 | }, 46 | { 47 | test: /\.md/, 48 | loaders: [ "html-loader", "markdown-loader" ] 49 | } 50 | ] 51 | } 52 | }; -------------------------------------------------------------------------------- /examples/selectingFormValues/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | 'babel-polyfill', 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/dist/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | NODE_ENV: JSON.stringify('production') 20 | } 21 | }), 22 | new webpack.optimize.UglifyJsPlugin({ 23 | compressor: { 24 | warnings: false 25 | } 26 | }) 27 | ], 28 | resolve: { 29 | modulesDirectories: [ 30 | 'src', 31 | 'node_modules' 32 | ], 33 | extensions: [ '', '.json', '.js' ] 34 | }, 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.js$/, 39 | loaders: [ 'babel' ], 40 | include: path.join(__dirname, 'src') 41 | }, 42 | { 43 | test: /\.json$/, 44 | loader: 'json-loader' 45 | }, 46 | { 47 | test: /\.md/, 48 | loaders: [ "html-loader", "markdown-loader" ] 49 | } 50 | ] 51 | } 52 | }; -------------------------------------------------------------------------------- /examples/wizard/src/WizardForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import WizardFormFirstPage from './WizardFormFirstPage' 3 | import WizardFormSecondPage from './WizardFormSecondPage' 4 | import WizardFormThirdPage from './WizardFormThirdPage' 5 | 6 | class WizardForm extends Component { 7 | constructor(props) { 8 | super(props) 9 | this.nextPage = this.nextPage.bind(this) 10 | this.previousPage = this.previousPage.bind(this) 11 | this.state = { 12 | page: 1 13 | } 14 | } 15 | nextPage() { 16 | this.setState({ page: this.state.page + 1 }) 17 | } 18 | 19 | previousPage() { 20 | this.setState({ page: this.state.page - 1 }) 21 | } 22 | 23 | render() { 24 | const { onSubmit } = this.props 25 | const { page } = this.state 26 | return (
27 | {page === 1 && } 28 | {page === 2 && } 29 | {page === 3 && } 30 |
31 | ) 32 | } 33 | } 34 | 35 | WizardForm.propTypes = { 36 | onSubmit: PropTypes.func.isRequired 37 | } 38 | 39 | export default WizardForm 40 | -------------------------------------------------------------------------------- /examples/immutable/src/ImmutableForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Field, reduxForm } from 'redux-form/immutable' // <--- immutable import 3 | import validate from './validate' 4 | 5 | const renderField = ({ input, label, type, meta: { touched, error } }) => ( 6 |
7 | 8 |
9 | 10 | {touched && error && {error}} 11 |
12 |
13 | ) 14 | 15 | const ImmutableForm = (props) => { 16 | const { handleSubmit, pristine, reset, submitting } = props 17 | return ( 18 |
19 | 20 | 21 | 22 |
23 | 24 | 25 |
26 | 27 | ) 28 | } 29 | 30 | export default reduxForm({ 31 | form: 'immutableExample', // a unique identifier for this form 32 | validate 33 | })(ImmutableForm) 34 | -------------------------------------------------------------------------------- /src/selectors/isValid.js: -------------------------------------------------------------------------------- 1 | import createHasError from '../hasError' 2 | 3 | const createIsValid = structure => { 4 | const { getIn } = structure 5 | const hasError = createHasError(structure) 6 | return (form, getFormState = state => getIn(state, 'form'), ignoreSubmitErrors = false) => 7 | state => { 8 | const formState = getFormState(state) 9 | const syncError = getIn(formState, `${form}.syncError`) 10 | if (syncError) { 11 | return false 12 | } 13 | if (!ignoreSubmitErrors) { 14 | const error = getIn(formState, `${form}.error`) 15 | if (error) { 16 | return false 17 | } 18 | } 19 | const syncErrors = getIn(formState, `${form}.syncErrors`) 20 | const asyncErrors = getIn(formState, `${form}.asyncErrors`) 21 | const submitErrors = ignoreSubmitErrors ? 22 | undefined : 23 | getIn(formState, `${form}.submitErrors`) 24 | if (!syncErrors && !asyncErrors && !submitErrors) { 25 | return true 26 | } 27 | 28 | const registeredFields = getIn(formState, `${form}.registeredFields`) || [] 29 | return !registeredFields.some(field => hasError(field, syncErrors, asyncErrors, submitErrors)) 30 | } 31 | } 32 | 33 | export default createIsValid 34 | -------------------------------------------------------------------------------- /src/__tests__/reducer.unregisterField.spec.js: -------------------------------------------------------------------------------- 1 | import { unregisterField } from '../actions' 2 | 3 | const describeUnregisterField = (reducer, expect, { fromJS }) => () => { 4 | it('should remove a field from registeredFields', () => { 5 | const state = reducer(fromJS({ 6 | foo: { 7 | registeredFields: [ { name: 'bar', type: 'field' } ] 8 | } 9 | }), unregisterField('foo', 'bar')) 10 | expect(state) 11 | .toEqualMap({ 12 | foo: {} 13 | }) 14 | }) 15 | 16 | it('should do nothing if there are no registered fields', () => { 17 | const initialState = fromJS({ 18 | foo: {} 19 | }) 20 | const state = reducer(initialState, unregisterField('foo', 'bar')) 21 | expect(state) 22 | .toEqual(initialState) 23 | }) 24 | 25 | it('should do nothing if the field is not registered', () => { 26 | const state = reducer(fromJS({ 27 | foo: { 28 | registeredFields: [ 29 | { name: 'bar', type: 'field' } 30 | ] 31 | } 32 | }), unregisterField('foo', 'baz')) 33 | expect(state) 34 | .toEqualMap({ 35 | foo: { 36 | registeredFields: [ { name: 'bar', type: 'field' } ] 37 | } 38 | }) 39 | }) 40 | } 41 | 42 | export default describeUnregisterField 43 | -------------------------------------------------------------------------------- /docs/faq/CustomComponent.md: -------------------------------------------------------------------------------- 1 | # Will `redux-form` work with my custom input component? 2 | 3 | The minimum interface needed for a custom component to work with `redux-form` is to make sure that 4 | `value` and `onChange` are passed properly. These are pretty standard prop names, so it's 5 | possible that your component will work right out of the box. 6 | 7 | But let's say that you have a custom component called `MyStrangeInput` that has `currentValue` 8 | and `thingsChanged` props that expect the value to be wrapped in an object under a `val` key. You 9 | would have to do something like: 10 | 11 | ```javascript 12 | const renderMyStrangeInput = field => ( 13 | field.input.onChange(param.val)}/> 16 | ) 17 | 18 | ... 19 | 20 | render() { 21 | return ( 22 |
23 | 24 |
25 | ); 26 | } 27 | ``` 28 | 29 | The point is that almost any input can be adjusted to use `redux-form`. If you are using an input 30 | component with a non-standard interface many times in your application, it is recommended that 31 | you wrap it in another component that will allow you to do the normal field destructuring: 32 | ``. 33 | -------------------------------------------------------------------------------- /src/__tests__/reducer.arrayShift.spec.js: -------------------------------------------------------------------------------- 1 | import { arrayShift } from '../actions' 2 | 3 | const describeArrayShift = (reducer, expect, { fromJS }) => () => { 4 | it('should remove from beginning', () => { 5 | const state = reducer(fromJS({ 6 | foo: { 7 | values: { 8 | myField: { 9 | subField: [ 'a', 'b', 'c', 'd' ] 10 | } 11 | }, 12 | fields: { 13 | myField: { 14 | subField: [ 15 | { touched: true, visited: true }, 16 | { touched: true }, 17 | { touched: true, visited: true }, 18 | { touched: true } 19 | ] 20 | } 21 | } 22 | } 23 | }), arrayShift('foo', 'myField.subField')) 24 | expect(state) 25 | .toEqualMap({ 26 | foo: { 27 | values: { 28 | myField: { 29 | subField: [ 'b', 'c', 'd' ] 30 | } 31 | }, 32 | fields: { 33 | myField: { 34 | subField: [ 35 | { touched: true }, 36 | { touched: true, visited: true }, 37 | { touched: true } 38 | ] 39 | } 40 | } 41 | } 42 | }) 43 | }) 44 | } 45 | 46 | export default describeArrayShift 47 | -------------------------------------------------------------------------------- /docs/faq/EnterToSubmit.md: -------------------------------------------------------------------------------- 1 | # How can I submit my form when the user presses Enter? 2 | 3 | The default browser behavior for `text` and `password` inputs when ↵ is pressed is to activate the first ` 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | ``` 26 | 27 | Things to notice: 28 | 29 | 1. `handleSubmit` is on the `
` element, not on the ` 24 | 25 | 26 |
27 | ) 28 | } 29 | 30 | export default reduxForm({ 31 | form: 'wizard', //Form name is same 32 | destroyOnUnmount: false, 33 | forceUnregisterOnUnmount: true, // <------ unregister fields on unmount 34 | validate 35 | })(WizardFormSecondPage) 36 | -------------------------------------------------------------------------------- /src/__tests__/reducer.startAsyncValidation.spec.js: -------------------------------------------------------------------------------- 1 | import { startAsyncValidation } from '../actions' 2 | 3 | const describeStartAsyncValidation = (reducer, expect, { fromJS }) => () => { 4 | it('should set asyncValidating on startAsyncValidation', () => { 5 | const state = reducer(fromJS({ 6 | foo: { 7 | doesnt: 'matter', 8 | should: 'notchange' 9 | } 10 | }), startAsyncValidation('foo')) 11 | expect(state) 12 | .toEqualMap({ 13 | foo: { 14 | doesnt: 'matter', 15 | should: 'notchange', 16 | asyncValidating: true 17 | } 18 | }) 19 | }) 20 | 21 | it('should set asyncValidating with field name on startAsyncValidation', () => { 22 | const state = reducer(fromJS({ 23 | foo: { 24 | values: { 25 | myField: 'initialValue' 26 | }, 27 | initial: { 28 | myField: 'initialValue' 29 | } 30 | } 31 | }), startAsyncValidation('foo', 'myField')) 32 | expect(state) 33 | .toEqualMap({ 34 | foo: { 35 | values: { 36 | myField: 'initialValue' 37 | }, 38 | initial: { 39 | myField: 'initialValue' 40 | }, 41 | asyncValidating: 'myField' 42 | } 43 | }) 44 | }) 45 | } 46 | 47 | export default describeStartAsyncValidation 48 | -------------------------------------------------------------------------------- /examples/asyncValidation/src/AsyncValidationForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Field, reduxForm } from 'redux-form' 3 | import validate from './validate' 4 | import asyncValidate from './asyncValidate' 5 | 6 | const renderField = ({ input, label, type, meta: { asyncValidating, touched, error } }) => ( 7 |
8 | 9 |
10 | 11 | {touched && error && {error}} 12 |
13 |
14 | ) 15 | 16 | const AsyncValidationForm = (props) => { 17 | const { handleSubmit, pristine, reset, submitting } = props 18 | return ( 19 |
20 | 21 | 22 |
23 | 24 | 25 |
26 | 27 | ) 28 | } 29 | 30 | export default reduxForm({ 31 | form: 'asyncValidation', // a unique identifier for this form 32 | validate, 33 | asyncValidate, 34 | asyncBlurFields: [ 'username' ] 35 | })(AsyncValidationForm) 36 | -------------------------------------------------------------------------------- /docs/api/Form.md: -------------------------------------------------------------------------------- 1 | # `Form` 2 | 3 | The `Form` component is a simple wrapper for the React `
` component that allows 4 | the surrounding `redux-form`-decorated component to trigger its `onSubmit` function. 5 | 6 | It is only useful if you are: 7 | 8 | - performing your submission from inside your form component by passing 9 | `onSubmit={this.props.handleSubmit(this.mySubmitFunction)}` to your `` 10 | component 11 | - **AND EITHER:** 12 | - initiating your submission via the [`submit()` Instance API](http://redux-form.com/6.4.1/docs/api/ReduxForm.md/#-submit-promise-) (i.e. calling it directly on a reference to your decorated form component) 13 | - initiating your submission by [dispatching the `submit(form)` action](http://redux-form.com/6.4.1/examples/remoteSubmit/) 14 | 15 | If you are passing in your `onSubmit` function as a config parameter or prop, this component will do nothing for you. 16 | 17 | ## Importing 18 | 19 | ```javascript 20 | var Form = require('redux-form').Form; // ES5 21 | ``` 22 | ```javascript 23 | import { Form } from 'redux-form'; // ES6 24 | ``` 25 | ## Props you can pass to `Form` 26 | 27 | Any that you can pass to ``, but only one is required. 28 | 29 | ### `onSubmit : Function` [required] 30 | 31 | > The function to call when form submission is triggered. 32 | 33 | ## Usage 34 | 35 | All you do is replace your `` with ``. -------------------------------------------------------------------------------- /examples/fieldArrays/src/FieldArrays.md: -------------------------------------------------------------------------------- 1 | # Field Arrays Example 2 | 3 | This example demonstrates how to have arrays of fields, both an array of one field or of a group 4 | of fields. In this form, each member of the club has a first name, last name, and a list of 5 | hobbies. The following array manipulation actions are available, as raw action creators, as bound 6 | actions to your form under the `this.props.array` object, and as actions bound to both the form 7 | and array on the object provided by the `FieldArray` component: `insert`, `pop`, `push`, `remove`, 8 | `shift`, `swap`, and `unshift`. More detail can be found under the 9 | [`FieldArray` docs](https://redux-form.com/6.4.1/docs/api/FieldArray.md). 10 | 11 | Notice that array-specific errors are available if set on the array structure itself under the 12 | `_error` key. (Hint: Add more than five hobbies to see an error.) 13 | 14 | ## Running this example locally 15 | 16 | To run this example locally on your machine clone the `redux-form` repository, 17 | then `cd redux-form` to change to the repo directory, and run `npm install`. 18 | 19 | Then run `npm run example:fieldArrays` or manually run the 20 | following commands: 21 | ``` 22 | cd ./examples/fieldArrays 23 | npm install 24 | npm start 25 | ``` 26 | 27 | Then open [`localhost:3030`](http://localhost:3030) in your 28 | browser to view the example running locally on your machine. 29 | 30 | -------------------------------------------------------------------------------- /docs/ValueLifecycle.md: -------------------------------------------------------------------------------- 1 | # Understanding Field Value Lifecycle 2 | 3 | It's important to understand how your field value is flowing through `redux-form`. 4 | 5 | ## Value Lifecycle Hooks 6 | 7 | `redux-form` provides three value lifecycle hooks, provided as props to the `Field` component. 8 | **They are all optional.** 9 | 10 | ### `format(value:Any) => String` 11 | 12 | > Formats the value from the Redux store to be used for your input component. Common use cases are 13 | for maintaining data as `Number`s or `Date`s in the store, but formatting them a specific way in 14 | your input. 15 | 16 | ### `parse(value:String) => Any` 17 | 18 | > Parses a string input by the user into the data type that you want to use in the Redux store. 19 | Common use cases are for maintaining data as `Number`s or `Date`s in the store. 20 | 21 | ### `normalize(value:Any, previousValue:Any, allValues:Object, previousAllValues:Object) => Any` 22 | 23 | > Allows you to add logic based on all your form values to put a constraint on the value of the 24 | current field. Common use cases include making sure that `minDate` is before `maxDate`. If you 25 | have provided a parser, the value given to `normalize()` will already be parsed. 26 | 27 | ## Value Lifecycle 28 | 29 |
30 | 31 |
32 | 33 | -------------------------------------------------------------------------------- /examples/simple/src/Simple.md: -------------------------------------------------------------------------------- 1 | # Simple Form Example 2 | 3 | This is a simple demonstration of how to connect all the standard HTML form elements to 4 | `redux-form`. 5 | 6 | For the most part, it is a matter of wrapping each form control in a `` component, 7 | specifying which type of `React.DOM` component you wish to be rendered. 8 | 9 | The `Field` component will provide your input with `onChange`, `onBlur`, `onFocus`, `onDrag`, and 10 | `onDrop` props to listen to the events, as well as a `value` prop to make each input a 11 | [controlled component](http://facebook.github.io/react/docs/forms.html#controlled-components). 12 | Notice that the `SimpleForm` component has no state; in fact, it uses the functional stateless 13 | component syntax. 14 | 15 | The delay between when you click "Submit" and when the alert dialog pops up is intentional, to 16 | simulate server latency. 17 | 18 | This form does no validation. To learn about how to do client-side validation, see the 19 | [Synchronous Validation](../syncValidation) example. 20 | 21 | ## Running this example locally 22 | 23 | To run this example locally on your machine clone the `redux-form` repository, 24 | then `cd redux-form` to change to the repo directory, and run `npm install`. 25 | 26 | Then run `npm run example:simple` or manually run the 27 | following commands: 28 | ``` 29 | cd ./examples/simple 30 | npm install 31 | npm start 32 | ``` 33 | 34 | -------------------------------------------------------------------------------- /src/structure/immutable/expectations.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import deepEqual from 'deep-equal' 3 | import { Map, List, Iterable, fromJS } from 'immutable' 4 | 5 | const deepEqualValues = (a, b) => { 6 | if (Iterable.isIterable(a)) { 7 | return Iterable.isIterable(b) && 8 | a.count() === b.count() && 9 | a.every((value, key) => deepEqualValues(value, b.get(key))) 10 | } 11 | return deepEqual(a, b) // neither are immutable iterables 12 | } 13 | 14 | const api = { 15 | toBeAMap() { 16 | expect.assert( 17 | Map.isMap(this.actual), 18 | 'expected %s to be an immutable Map', 19 | this.actual 20 | ) 21 | return this 22 | }, 23 | 24 | toBeAList() { 25 | expect.assert( 26 | List.isList(this.actual), 27 | 'expected %s to be an immutable List', 28 | this.actual 29 | ) 30 | return this 31 | }, 32 | 33 | toBeSize(size) { 34 | expect.assert( 35 | Iterable.isIterable(this.actual) && this.actual.count() === size, 36 | 'expected %s to contain %s elements', 37 | this.actual, 38 | size 39 | ) 40 | return this 41 | }, 42 | 43 | toEqualMap(expected) { 44 | expect.assert( 45 | deepEqualValues(this.actual, fromJS(expected)), 46 | 'expected...\n%s\n...but found...\n%s', 47 | fromJS(expected), 48 | this.actual 49 | ) 50 | return this 51 | } 52 | } 53 | 54 | export default api 55 | -------------------------------------------------------------------------------- /src/structure/plain/deleteIn.js: -------------------------------------------------------------------------------- 1 | import { toPath } from 'lodash' 2 | 3 | const deleteInWithPath = (state, first, ...rest) => { 4 | if (state === undefined || first === undefined) { 5 | return state 6 | } 7 | if (rest.length) { 8 | if (Array.isArray(state)) { 9 | if (first < state.length) { 10 | const result = deleteInWithPath(state && state[ first ], ...rest) 11 | if (result !== state[ first ]) { 12 | const copy = [ ...state ] 13 | copy[ first ] = result 14 | return copy 15 | } 16 | } 17 | return state 18 | } 19 | if (first in state) { 20 | const result = deleteInWithPath(state && state[ first ], ...rest) 21 | return state[ first ] === result ? state : { 22 | ...state, 23 | [first]: result 24 | } 25 | } 26 | return state 27 | } 28 | if (Array.isArray(state)) { 29 | if (isNaN(first)) { 30 | throw new Error('Cannot delete non-numerical index from an array') 31 | } 32 | if (first < state.length) { 33 | const copy = [ ...state ] 34 | copy.splice(first, 1) 35 | return copy 36 | } 37 | return state 38 | } 39 | if (first in state) { 40 | const copy = { ...state } 41 | delete copy[ first ] 42 | return copy 43 | } 44 | return state 45 | } 46 | 47 | const deleteIn = (state, field) => deleteInWithPath(state, ...toPath(field)) 48 | 49 | export default deleteIn 50 | -------------------------------------------------------------------------------- /src/__tests__/reducer.registerField.spec.js: -------------------------------------------------------------------------------- 1 | import { registerField } from '../actions' 2 | 3 | const describeRegisterField = (reducer, expect, { fromJS }) => () => { 4 | it('should create registeredFields if it does not exist and a field', () => { 5 | const state = reducer(fromJS({ 6 | foo: {} 7 | }), registerField('foo', 'bar', 'Field' )) 8 | expect(state) 9 | .toEqualMap({ 10 | foo: { 11 | registeredFields: [ { name: 'bar', type: 'Field' } ] 12 | } 13 | }) 14 | }) 15 | 16 | it('should add a field to registeredFields', () => { 17 | const state = reducer(fromJS({ 18 | foo: { 19 | registeredFields: [ { name: 'baz', type: 'FieldArray' } ] 20 | } 21 | }), registerField('foo', 'bar', 'Field' )) 22 | expect(state) 23 | .toEqualMap({ 24 | foo: { 25 | registeredFields: [ 26 | { name: 'baz', type: 'FieldArray' }, 27 | { name: 'bar', type: 'Field' } 28 | ] 29 | } 30 | }) 31 | }) 32 | 33 | it('should do nothing if the field already exists', () => { 34 | const initialState = fromJS({ 35 | foo: { 36 | registeredFields: [ { name: 'bar', type: 'FieldArray' } ] 37 | } 38 | }) 39 | const state = reducer(initialState, registerField('foo', 'bar', 'Field' )) 40 | expect(state) 41 | .toEqual(initialState) 42 | }) 43 | } 44 | 45 | export default describeRegisterField 46 | -------------------------------------------------------------------------------- /src/FormSection.js: -------------------------------------------------------------------------------- 1 | import React, { createElement, Component, PropTypes } from 'react' 2 | import prefixName from './util/prefixName' 3 | 4 | class FormSection extends Component { 5 | constructor(props, context) { 6 | super(props, context) 7 | if (!context._reduxForm) { 8 | throw new Error('FormSection must be inside a component decorated with reduxForm()') 9 | } 10 | } 11 | 12 | getChildContext() { 13 | const { context, props: { name } } = this 14 | return { 15 | _reduxForm: { 16 | ...context._reduxForm, 17 | sectionPrefix: prefixName(context, name) 18 | } 19 | } 20 | } 21 | 22 | render() { 23 | const { 24 | children, 25 | name, // eslint-disable-line no-unused-vars 26 | component, 27 | ...rest 28 | } = this.props 29 | 30 | if (React.isValidElement(children)) { 31 | return children 32 | } 33 | 34 | return createElement(component, { 35 | ...rest, 36 | children 37 | }) 38 | } 39 | } 40 | 41 | FormSection.propTypes = { 42 | name: PropTypes.string.isRequired, 43 | component: PropTypes.oneOfType([ PropTypes.func, PropTypes.string ]) 44 | } 45 | 46 | FormSection.defaultProps = { 47 | component: 'div' 48 | } 49 | 50 | FormSection.childContextTypes = { 51 | _reduxForm: PropTypes.object.isRequired 52 | } 53 | 54 | FormSection.contextTypes = { 55 | _reduxForm: PropTypes.object 56 | } 57 | 58 | export default FormSection 59 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We are open to, and grateful for, any contributions made by the community. 4 | 5 | ## Reporting issues and asking questions 6 | 7 | Before opening an issue, please search the [issue tracker](https://github.com/erikras/redux-form/issues) to make sure your issue hasn’t already been reported. 8 | 9 | **We use the issue tracker to keep track of bugs and improvements** to Redux Form itself, its examples, and the documentation. We encourage you to open issues to discuss improvements, architecture, internal implementation, etc. If a topic has been discussed before, we will ask you to join the previous discussion. 10 | 11 | For support or usage questions, please search and ask on [StackOverflow with a `redux-form` tag](https://stackoverflow.com/questions/tagged/redux-form). We ask you to do this because StackOverflow has a much better job at keeping popular questions visible. Unfortunately good answers get lost and outdated on GitHub. 12 | 13 | ## Sending a pull request 14 | 15 | For non-trivial changes, please open an issue with a proposal for a new feature or refactoring before starting on the work. We don’t want you to waste your efforts on a pull request that we won’t want to accept. 16 | 17 | Please try to keep your pull request focused in scope and avoid including unrelated commits. 18 | 19 | After you have submitted your pull request, we’ll try to get back to you as soon as possible. We may suggest some changes or improvements. 20 | -------------------------------------------------------------------------------- /docs/api/Reducer.md: -------------------------------------------------------------------------------- 1 | # `reducer` 2 | 3 | [`View source on GitHub`](https://github.com/erikras/redux-form/blob/master/src/reducer.js) 4 | 5 | > The form reducer. Should be given to mounted to your Redux state at `form`. 6 | 7 | > If you absolutely must mount it somewhere other than `form`, you may provide a 8 | `getFormState(state)` function to the `reduxForm()` decorator, to get the slice of the Redux 9 | state where you have mounted the `redux-form` reducer. 10 | 11 | > If you're using Immutablejs to manage your Redux state, you MUST import the reducer from 'redux-form/immutable'. 12 | 13 | ### ES5 Example 14 | 15 | ```javascript 16 | var redux = require('redux'); 17 | var formReducer = require('redux-form').reducer; 18 | // Or with Immutablejs: 19 | // var formReducer = require('redux-form/immutable').reducer; 20 | 21 | var reducers = { 22 | // ... your other reducers here ... 23 | form: formReducer 24 | }; 25 | var reducer = redux.combineReducers(reducers); 26 | var store = redux.createStore(reducer); 27 | ``` 28 | 29 | ### ES6 Example 30 | 31 | ```javascript 32 | import { createStore, combineReducers } from 'redux'; 33 | import { reducer as formReducer } from 'redux-form'; 34 | // Or with Immutablejs: 35 | // import { reducer as formReducer } from 'redux-form/immutable'; 36 | 37 | const reducers = { 38 | // ... your other reducers here ... 39 | form: formReducer 40 | }; 41 | const reducer = combineReducers(reducers); 42 | const store = createStore(reducer); 43 | ``` 44 | -------------------------------------------------------------------------------- /src/__tests__/reducer.clearSubmit.spec.js: -------------------------------------------------------------------------------- 1 | import { clearSubmit } from '../actions' 2 | 3 | const describeClearSubmit = (reducer, expect, { fromJS }) => () => { 4 | it('should do nothing on clear submit with no previous state', () => { 5 | const state = reducer(undefined, clearSubmit('foo')) 6 | expect(state) 7 | .toEqualMap({ 8 | foo: {} 9 | }) 10 | }) 11 | 12 | it('should clear triggerSubmit with previous state', () => { 13 | const state = reducer(fromJS({ 14 | foo: { 15 | values: { 16 | myField: 'initialValue' 17 | }, 18 | initial: { 19 | myField: 'initialValue' 20 | }, 21 | fields: { 22 | myField: {}, 23 | otherField: { 24 | visited: true, 25 | active: true 26 | } 27 | }, 28 | active: 'otherField', 29 | triggerSubmit: true 30 | } 31 | }), clearSubmit('foo')) 32 | expect(state) 33 | .toEqualMap({ 34 | foo: { 35 | values: { 36 | myField: 'initialValue' 37 | }, 38 | initial: { 39 | myField: 'initialValue' 40 | }, 41 | fields: { 42 | myField: {}, 43 | otherField: { 44 | visited: true, 45 | active: true 46 | } 47 | }, 48 | active: 'otherField' 49 | } 50 | }) 51 | }) 52 | } 53 | 54 | export default describeClearSubmit 55 | -------------------------------------------------------------------------------- /src/__tests__/reducer.submit.spec.js: -------------------------------------------------------------------------------- 1 | import { submit } from '../actions' 2 | 3 | const describeSubmit = (reducer, expect, { fromJS }) => () => { 4 | it('should set triggerSubmit with no previous state', () => { 5 | const state = reducer(undefined, submit('foo')) 6 | expect(state) 7 | .toEqualMap({ 8 | foo: { 9 | triggerSubmit: true 10 | } 11 | }) 12 | }) 13 | 14 | it('should set triggerSubmit with previous state', () => { 15 | const state = reducer(fromJS({ 16 | foo: { 17 | values: { 18 | myField: 'initialValue' 19 | }, 20 | initial: { 21 | myField: 'initialValue' 22 | }, 23 | fields: { 24 | myField: {}, 25 | otherField: { 26 | visited: true, 27 | active: true 28 | } 29 | }, 30 | active: 'otherField' 31 | } 32 | }), submit('foo')) 33 | expect(state) 34 | .toEqualMap({ 35 | foo: { 36 | values: { 37 | myField: 'initialValue' 38 | }, 39 | initial: { 40 | myField: 'initialValue' 41 | }, 42 | fields: { 43 | myField: {}, 44 | otherField: { 45 | visited: true, 46 | active: true 47 | } 48 | }, 49 | active: 'otherField', 50 | triggerSubmit: true 51 | } 52 | }) 53 | }) 54 | } 55 | 56 | export default describeSubmit 57 | -------------------------------------------------------------------------------- /examples/react-widgets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf dist", 5 | "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", 6 | "build": "npm run clean && npm run build:webpack", 7 | "lint": "eslint src", 8 | "start": "node devServer.js" 9 | }, 10 | "dependencies": { 11 | "babel-polyfill": "^6.16.0", 12 | "html-loader": "^0.4.4", 13 | "json-loader": "0.5.4", 14 | "markdown-loader": "0.1.7", 15 | "raw-loader": "0.5.1", 16 | "react": "^15.4.1", 17 | "react-dom": "^15.4.1", 18 | "react-redux": "^4.4.6", 19 | "react-widgets": "^3.3.1", 20 | "redux": "^3.6.0", 21 | "redux-form": "file:../../", 22 | "redux-form-website-template": "0.0.41" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.18.2", 26 | "babel-eslint": "^7.1.1", 27 | "babel-loader": "^6.2.8", 28 | "babel-preset-es2015": "^6.18.0", 29 | "babel-preset-react": "^6.16.0", 30 | "cross-env": "^3.1.3", 31 | "eslint": "^3.11.1", 32 | "eslint-config-rackt": "1.1.1", 33 | "eslint-loader": "^1.6.1", 34 | "eslint-plugin-babel": "^4.0.0", 35 | "eslint-plugin-react": "^6.7.1", 36 | "eventsource-polyfill": "0.9.6", 37 | "express": "^4.14.0", 38 | "extract-text-webpack-plugin": "1.0.1", 39 | "redbox-react": "^1.3.3", 40 | "rimraf": "^2.5.4", 41 | "webpack": "^1.13.3", 42 | "webpack-dev-middleware": "^1.8.4", 43 | "webpack-hot-middleware": "^2.13.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf dist", 5 | "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", 6 | "build": "npm run clean && npm run build:webpack", 7 | "lint": "eslint src", 8 | "start": "node devServer.js", 9 | "prepublish": "npm run lint && npm run build" 10 | }, 11 | "dependencies": { 12 | "babel-polyfill": "^6.16.0", 13 | "html-loader": "^0.4.4", 14 | "json-loader": "0.5.4", 15 | "markdown-loader": "0.1.7", 16 | "raw-loader": "0.5.1", 17 | "react": "^15.4.1", 18 | "react-dom": "^15.4.1", 19 | "react-redux": "^4.4.6", 20 | "redux": "^3.6.0", 21 | "redux-form": "file:../../", 22 | "redux-form-website-template": "0.0.41" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.18.2", 26 | "babel-eslint": "^7.1.1", 27 | "babel-loader": "^6.2.8", 28 | "babel-preset-es2015": "^6.18.0", 29 | "babel-preset-react": "^6.16.0", 30 | "cross-env": "^3.1.3", 31 | "eslint": "^3.11.1", 32 | "eslint-config-rackt": "1.1.1", 33 | "eslint-loader": "^1.6.1", 34 | "eslint-plugin-babel": "^4.0.0", 35 | "eslint-plugin-react": "^6.7.1", 36 | "eventsource-polyfill": "0.9.6", 37 | "express": "^4.14.0", 38 | "extract-text-webpack-plugin": "1.0.1", 39 | "redbox-react": "^1.3.3", 40 | "rimraf": "^2.5.4", 41 | "webpack": "^1.13.3", 42 | "webpack-dev-middleware": "^1.8.4", 43 | "webpack-hot-middleware": "^2.13.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/wizard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf dist", 5 | "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", 6 | "build": "npm run clean && npm run build:webpack", 7 | "lint": "eslint src", 8 | "start": "node devServer.js", 9 | "prepublish": "npm run lint && npm run build" 10 | }, 11 | "dependencies": { 12 | "babel-polyfill": "^6.16.0", 13 | "html-loader": "^0.4.4", 14 | "json-loader": "0.5.4", 15 | "markdown-loader": "0.1.7", 16 | "raw-loader": "0.5.1", 17 | "react": "^15.4.1", 18 | "react-dom": "^15.4.1", 19 | "react-redux": "^4.4.6", 20 | "redux": "^3.6.0", 21 | "redux-form": "file:../../", 22 | "redux-form-website-template": "0.0.41" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.18.2", 26 | "babel-eslint": "^7.1.1", 27 | "babel-loader": "^6.2.8", 28 | "babel-preset-es2015": "^6.18.0", 29 | "babel-preset-react": "^6.16.0", 30 | "cross-env": "^3.1.3", 31 | "eslint": "^3.11.1", 32 | "eslint-config-rackt": "1.1.1", 33 | "eslint-loader": "^1.6.1", 34 | "eslint-plugin-babel": "^4.0.0", 35 | "eslint-plugin-react": "^6.7.1", 36 | "eventsource-polyfill": "0.9.6", 37 | "express": "^4.14.0", 38 | "extract-text-webpack-plugin": "1.0.1", 39 | "redbox-react": "^1.3.3", 40 | "rimraf": "^2.5.4", 41 | "webpack": "^1.13.3", 42 | "webpack-dev-middleware": "^1.8.4", 43 | "webpack-hot-middleware": "^2.13.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/fieldArrays/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf dist", 5 | "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", 6 | "build": "npm run clean && npm run build:webpack", 7 | "lint": "eslint src", 8 | "start": "node devServer.js", 9 | "prepublish": "npm run lint && npm run build" 10 | }, 11 | "dependencies": { 12 | "babel-polyfill": "^6.16.0", 13 | "html-loader": "^0.4.4", 14 | "json-loader": "0.5.4", 15 | "markdown-loader": "0.1.7", 16 | "raw-loader": "0.5.1", 17 | "react": "^15.4.1", 18 | "react-dom": "^15.4.1", 19 | "react-redux": "^4.4.6", 20 | "redux": "^3.6.0", 21 | "redux-form": "file:../../", 22 | "redux-form-website-template": "0.0.41" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.18.2", 26 | "babel-eslint": "^7.1.1", 27 | "babel-loader": "^6.2.8", 28 | "babel-preset-es2015": "^6.18.0", 29 | "babel-preset-react": "^6.16.0", 30 | "cross-env": "^3.1.3", 31 | "eslint": "^3.11.1", 32 | "eslint-config-rackt": "1.1.1", 33 | "eslint-loader": "^1.6.1", 34 | "eslint-plugin-babel": "^4.0.0", 35 | "eslint-plugin-react": "^6.7.1", 36 | "eventsource-polyfill": "0.9.6", 37 | "express": "^4.14.0", 38 | "extract-text-webpack-plugin": "1.0.1", 39 | "redbox-react": "^1.3.3", 40 | "rimraf": "^2.5.4", 41 | "webpack": "^1.13.3", 42 | "webpack-dev-middleware": "^1.8.4", 43 | "webpack-hot-middleware": "^2.13.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/normalizing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf dist", 5 | "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", 6 | "build": "npm run clean && npm run build:webpack", 7 | "lint": "eslint src", 8 | "start": "node devServer.js", 9 | "prepublish": "npm run lint && npm run build" 10 | }, 11 | "dependencies": { 12 | "babel-polyfill": "^6.16.0", 13 | "html-loader": "^0.4.4", 14 | "json-loader": "0.5.4", 15 | "markdown-loader": "0.1.7", 16 | "raw-loader": "0.5.1", 17 | "react": "^15.4.1", 18 | "react-dom": "^15.4.1", 19 | "react-redux": "^4.4.6", 20 | "redux": "^3.6.0", 21 | "redux-form": "file:../../", 22 | "redux-form-website-template": "0.0.41" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.18.2", 26 | "babel-eslint": "^7.1.1", 27 | "babel-loader": "^6.2.8", 28 | "babel-preset-es2015": "^6.18.0", 29 | "babel-preset-react": "^6.16.0", 30 | "cross-env": "^3.1.3", 31 | "eslint": "^3.11.1", 32 | "eslint-config-rackt": "1.1.1", 33 | "eslint-loader": "^1.6.1", 34 | "eslint-plugin-babel": "^4.0.0", 35 | "eslint-plugin-react": "^6.7.1", 36 | "eventsource-polyfill": "0.9.6", 37 | "express": "^4.14.0", 38 | "extract-text-webpack-plugin": "1.0.1", 39 | "redbox-react": "^1.3.3", 40 | "rimraf": "^2.5.4", 41 | "webpack": "^1.13.3", 42 | "webpack-dev-middleware": "^1.8.4", 43 | "webpack-hot-middleware": "^2.13.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/remoteSubmit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf dist", 5 | "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", 6 | "build": "npm run clean && npm run build:webpack", 7 | "lint": "eslint src", 8 | "start": "node devServer.js", 9 | "prepublish": "npm run lint && npm run build" 10 | }, 11 | "dependencies": { 12 | "babel-polyfill": "^6.16.0", 13 | "html-loader": "^0.4.4", 14 | "json-loader": "0.5.4", 15 | "markdown-loader": "0.1.7", 16 | "raw-loader": "0.5.1", 17 | "react": "^15.4.1", 18 | "react-dom": "^15.4.1", 19 | "react-redux": "^4.4.6", 20 | "redux": "^3.6.0", 21 | "redux-form": "file:../../", 22 | "redux-form-website-template": "0.0.41" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.18.2", 26 | "babel-eslint": "^7.1.1", 27 | "babel-loader": "^6.2.8", 28 | "babel-preset-es2015": "^6.18.0", 29 | "babel-preset-react": "^6.16.0", 30 | "cross-env": "^3.1.3", 31 | "eslint": "^3.11.1", 32 | "eslint-config-rackt": "1.1.1", 33 | "eslint-loader": "^1.6.1", 34 | "eslint-plugin-babel": "^4.0.0", 35 | "eslint-plugin-react": "^6.7.1", 36 | "eventsource-polyfill": "0.9.6", 37 | "express": "^4.14.0", 38 | "extract-text-webpack-plugin": "1.0.1", 39 | "redbox-react": "^1.3.3", 40 | "rimraf": "^2.5.4", 41 | "webpack": "^1.13.3", 42 | "webpack-dev-middleware": "^1.8.4", 43 | "webpack-hot-middleware": "^2.13.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/syncValidation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf dist", 5 | "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", 6 | "build": "npm run clean && npm run build:webpack", 7 | "lint": "eslint src", 8 | "start": "node devServer.js", 9 | "prepublish": "npm run lint && npm run build" 10 | }, 11 | "dependencies": { 12 | "babel-polyfill": "^6.16.0", 13 | "html-loader": "^0.4.4", 14 | "json-loader": "0.5.4", 15 | "markdown-loader": "0.1.7", 16 | "raw-loader": "0.5.1", 17 | "react": "^15.4.1", 18 | "react-dom": "^15.4.1", 19 | "react-redux": "^4.4.6", 20 | "redux": "^3.6.0", 21 | "redux-form": "file:../../", 22 | "redux-form-website-template": "0.0.41" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.18.2", 26 | "babel-eslint": "^7.1.1", 27 | "babel-loader": "^6.2.8", 28 | "babel-preset-es2015": "^6.18.0", 29 | "babel-preset-react": "^6.16.0", 30 | "cross-env": "^3.1.3", 31 | "eslint": "^3.11.1", 32 | "eslint-config-rackt": "1.1.1", 33 | "eslint-loader": "^1.6.1", 34 | "eslint-plugin-babel": "^4.0.0", 35 | "eslint-plugin-react": "^6.7.1", 36 | "eventsource-polyfill": "0.9.6", 37 | "express": "^4.14.0", 38 | "extract-text-webpack-plugin": "1.0.1", 39 | "redbox-react": "^1.3.3", 40 | "rimraf": "^2.5.4", 41 | "webpack": "^1.13.3", 42 | "webpack-dev-middleware": "^1.8.4", 43 | "webpack-hot-middleware": "^2.13.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/fieldKeys.js: -------------------------------------------------------------------------------- 1 | /** 2 | EXPERIMENTAL 3 | 4 | function* generate(field, values, path = '') { 5 | const [ , key, rest ] = /([^.]+)\.?(.+)?/.exec(field) 6 | 7 | if (/.+\[\]/.test(key)) { 8 | // is array key 9 | const noBrackets = key.substring(0, key.length - 2) 10 | const array = values && values[ noBrackets ] 11 | if (array && !Array.isArray(array)) { 12 | throw new Error(`Expected array value for ${path}${field}, but found ${typeof array}: ${JSON.stringify(array)}`) 13 | } 14 | if (array) { 15 | if (rest) { 16 | for (let index in array) { 17 | yield * generate(rest, array[ index ], `${path}${noBrackets}[${index}].`) 18 | } 19 | } else { 20 | for (let index in array) { 21 | yield `${path}${noBrackets}[${index}]` 22 | } 23 | } 24 | } else { 25 | yield `${path}${key}` 26 | } 27 | } else if (rest) { 28 | // need to recurse 29 | yield * generate(rest, values && values[ key ], `${path}${key}.`) 30 | } else { 31 | // value key 32 | yield `${path}${key}` 33 | } 34 | } 35 | */ 36 | 37 | /** 38 | * Iterates over all the fields specified by a fields array and store values. 39 | * 40 | * @param fields The fields array given to redux-form 41 | * @param values The current values of the form in the Redux store 42 | */ 43 | 44 | /** 45 | EXPERIMENTAL 46 | 47 | export default function* fieldKeys(fields = [], values = {}) { 48 | for (let field of fields) { 49 | yield * generate(field, values) 50 | } 51 | } 52 | */ 53 | -------------------------------------------------------------------------------- /examples/asyncValidation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf dist", 5 | "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", 6 | "build": "npm run clean && npm run build:webpack", 7 | "lint": "eslint src", 8 | "start": "node devServer.js", 9 | "prepublish": "npm run lint && npm run build" 10 | }, 11 | "dependencies": { 12 | "babel-polyfill": "^6.16.0", 13 | "html-loader": "^0.4.4", 14 | "json-loader": "0.5.4", 15 | "markdown-loader": "0.1.7", 16 | "raw-loader": "0.5.1", 17 | "react": "^15.4.1", 18 | "react-dom": "^15.4.1", 19 | "react-redux": "^4.4.6", 20 | "redux": "^3.6.0", 21 | "redux-form": "file:../../", 22 | "redux-form-website-template": "0.0.41" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.18.2", 26 | "babel-eslint": "^7.1.1", 27 | "babel-loader": "^6.2.8", 28 | "babel-preset-es2015": "^6.18.0", 29 | "babel-preset-react": "^6.16.0", 30 | "cross-env": "^3.1.3", 31 | "eslint": "^3.11.1", 32 | "eslint-config-rackt": "1.1.1", 33 | "eslint-loader": "^1.6.1", 34 | "eslint-plugin-babel": "^4.0.0", 35 | "eslint-plugin-react": "^6.7.1", 36 | "eventsource-polyfill": "0.9.6", 37 | "express": "^4.14.0", 38 | "extract-text-webpack-plugin": "1.0.1", 39 | "redbox-react": "^1.3.3", 40 | "rimraf": "^2.5.4", 41 | "webpack": "^1.13.3", 42 | "webpack-dev-middleware": "^1.8.4", 43 | "webpack-hot-middleware": "^2.13.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/initializeFromState/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf dist", 5 | "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", 6 | "build": "npm run clean && npm run build:webpack", 7 | "lint": "eslint src", 8 | "start": "node devServer.js", 9 | "prepublish": "npm run lint && npm run build" 10 | }, 11 | "dependencies": { 12 | "babel-polyfill": "^6.16.0", 13 | "html-loader": "^0.4.4", 14 | "json-loader": "0.5.4", 15 | "markdown-loader": "0.1.7", 16 | "raw-loader": "0.5.1", 17 | "react": "^15.4.1", 18 | "react-dom": "^15.4.1", 19 | "react-redux": "^4.4.6", 20 | "redux": "^3.6.0", 21 | "redux-form": "file:../../", 22 | "redux-form-website-template": "0.0.41" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.18.2", 26 | "babel-eslint": "^7.1.1", 27 | "babel-loader": "^6.2.8", 28 | "babel-preset-es2015": "^6.18.0", 29 | "babel-preset-react": "^6.16.0", 30 | "cross-env": "^3.1.3", 31 | "eslint": "^3.11.1", 32 | "eslint-config-rackt": "1.1.1", 33 | "eslint-loader": "^1.6.1", 34 | "eslint-plugin-babel": "^4.0.0", 35 | "eslint-plugin-react": "^6.7.1", 36 | "eventsource-polyfill": "0.9.6", 37 | "express": "^4.14.0", 38 | "extract-text-webpack-plugin": "1.0.1", 39 | "redbox-react": "^1.3.3", 40 | "rimraf": "^2.5.4", 41 | "webpack": "^1.13.3", 42 | "webpack-dev-middleware": "^1.8.4", 43 | "webpack-hot-middleware": "^2.13.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/selectingFormValues/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf dist", 5 | "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", 6 | "build": "npm run clean && npm run build:webpack", 7 | "lint": "eslint src", 8 | "start": "node devServer.js", 9 | "prepublish": "npm run lint && npm run build" 10 | }, 11 | "dependencies": { 12 | "babel-polyfill": "^6.16.0", 13 | "html-loader": "^0.4.4", 14 | "json-loader": "0.5.4", 15 | "markdown-loader": "0.1.7", 16 | "raw-loader": "0.5.1", 17 | "react": "^15.4.1", 18 | "react-dom": "^15.4.1", 19 | "react-redux": "^4.4.6", 20 | "redux": "^3.6.0", 21 | "redux-form": "file:../../", 22 | "redux-form-website-template": "0.0.41" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.18.2", 26 | "babel-eslint": "^7.1.1", 27 | "babel-loader": "^6.2.8", 28 | "babel-preset-es2015": "^6.18.0", 29 | "babel-preset-react": "^6.16.0", 30 | "cross-env": "^3.1.3", 31 | "eslint": "^3.11.1", 32 | "eslint-config-rackt": "1.1.1", 33 | "eslint-loader": "^1.6.1", 34 | "eslint-plugin-babel": "^4.0.0", 35 | "eslint-plugin-react": "^6.7.1", 36 | "eventsource-polyfill": "0.9.6", 37 | "express": "^4.14.0", 38 | "extract-text-webpack-plugin": "1.0.1", 39 | "redbox-react": "^1.3.3", 40 | "rimraf": "^2.5.4", 41 | "webpack": "^1.13.3", 42 | "webpack-dev-middleware": "^1.8.4", 43 | "webpack-hot-middleware": "^2.13.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/submitValidation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf dist", 5 | "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", 6 | "build": "npm run clean && npm run build:webpack", 7 | "lint": "eslint src", 8 | "start": "node devServer.js", 9 | "prepublish": "npm run lint && npm run build" 10 | }, 11 | "dependencies": { 12 | "babel-polyfill": "^6.16.0", 13 | "html-loader": "^0.4.4", 14 | "json-loader": "0.5.4", 15 | "markdown-loader": "0.1.7", 16 | "raw-loader": "0.5.1", 17 | "react": "^15.4.1", 18 | "react-dom": "^15.4.1", 19 | "react-redux": "^4.4.6", 20 | "redux": "^3.6.0", 21 | "redux-form": "file:../../", 22 | "redux-form-website-template": "0.0.41" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.18.2", 26 | "babel-eslint": "^7.1.1", 27 | "babel-loader": "^6.2.8", 28 | "babel-preset-es2015": "^6.18.0", 29 | "babel-preset-react": "^6.16.0", 30 | "cross-env": "^3.1.3", 31 | "eslint": "^3.11.1", 32 | "eslint-config-rackt": "1.1.1", 33 | "eslint-loader": "^1.6.1", 34 | "eslint-plugin-babel": "^4.0.0", 35 | "eslint-plugin-react": "^6.7.1", 36 | "eventsource-polyfill": "0.9.6", 37 | "express": "^4.14.0", 38 | "extract-text-webpack-plugin": "1.0.1", 39 | "redbox-react": "^1.3.3", 40 | "rimraf": "^2.5.4", 41 | "webpack": "^1.13.3", 42 | "webpack-dev-middleware": "^1.8.4", 43 | "webpack-hot-middleware": "^2.13.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/fieldLevelValidation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf dist", 5 | "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", 6 | "build": "npm run clean && npm run build:webpack", 7 | "lint": "eslint src", 8 | "start": "node devServer.js", 9 | "prepublish": "npm run lint && npm run build" 10 | }, 11 | "dependencies": { 12 | "babel-polyfill": "^6.16.0", 13 | "html-loader": "^0.4.4", 14 | "json-loader": "0.5.4", 15 | "markdown-loader": "0.1.7", 16 | "raw-loader": "0.5.1", 17 | "react": "^15.4.1", 18 | "react-dom": "^15.4.1", 19 | "react-redux": "^4.4.6", 20 | "redux": "^3.6.0", 21 | "redux-form": "file:../../", 22 | "redux-form-website-template": "0.0.41" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.18.2", 26 | "babel-eslint": "^7.1.1", 27 | "babel-loader": "^6.2.8", 28 | "babel-preset-es2015": "^6.18.0", 29 | "babel-preset-react": "^6.16.0", 30 | "cross-env": "^3.1.3", 31 | "eslint": "^3.11.1", 32 | "eslint-config-rackt": "1.1.1", 33 | "eslint-loader": "^1.6.1", 34 | "eslint-plugin-babel": "^4.0.0", 35 | "eslint-plugin-react": "^6.7.1", 36 | "eventsource-polyfill": "0.9.6", 37 | "express": "^4.14.0", 38 | "extract-text-webpack-plugin": "1.0.1", 39 | "redbox-react": "^1.3.3", 40 | "rimraf": "^2.5.4", 41 | "webpack": "^1.13.3", 42 | "webpack-dev-middleware": "^1.8.4", 43 | "webpack-hot-middleware": "^2.13.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/__tests__/reducer.arrayPop.spec.js: -------------------------------------------------------------------------------- 1 | import { arrayPop } from '../actions' 2 | 3 | const describeArrayPop = (reducer, expect, { fromJS }) => () => { 4 | it('should do nothing with no array', () => { 5 | const state = reducer(fromJS({ 6 | foo: { } 7 | }), arrayPop('foo', 'myField.subField')) 8 | expect(state) 9 | .toEqualMap({ 10 | foo: { } 11 | }) 12 | }) 13 | 14 | it('should pop from end', () => { 15 | const state = reducer(fromJS({ 16 | foo: { 17 | values: { 18 | myField: { 19 | subField: [ 'a', 'b', 'c', 'd' ] 20 | } 21 | }, 22 | fields: { 23 | myField: { 24 | subField: [ 25 | { touched: true, visited: true }, 26 | { touched: true }, 27 | { touched: true, visited: true }, 28 | { touched: true } 29 | ] 30 | } 31 | } 32 | } 33 | }), arrayPop('foo', 'myField.subField')) 34 | expect(state) 35 | .toEqualMap({ 36 | foo: { 37 | values: { 38 | myField: { 39 | subField: [ 'a', 'b', 'c' ] 40 | } 41 | }, 42 | fields: { 43 | myField: { 44 | subField: [ 45 | { touched: true, visited: true }, 46 | { touched: true }, 47 | { touched: true, visited: true } 48 | ] 49 | } 50 | } 51 | } 52 | }) 53 | }) 54 | } 55 | 56 | export default describeArrayPop 57 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var webpack = require('webpack') 3 | var LodashModuleReplacementPlugin = require('lodash-webpack-plugin') 4 | 5 | var env = process.env.NODE_ENV 6 | 7 | var reactExternal = { 8 | root: 'React', 9 | commonjs2: 'react', 10 | commonjs: 'react', 11 | amd: 'react' 12 | } 13 | 14 | var reduxExternal = { 15 | root: 'Redux', 16 | commonjs2: 'redux', 17 | commonjs: 'redux', 18 | amd: 'redux' 19 | } 20 | 21 | var reactReduxExternal = { 22 | root: 'ReactRedux', 23 | commonjs2: 'react-redux', 24 | commonjs: 'react-redux', 25 | amd: 'react-redux' 26 | } 27 | 28 | var config = { 29 | externals: { 30 | 'react': reactExternal, 31 | 'redux': reduxExternal, 32 | 'react-redux': reactReduxExternal 33 | }, 34 | module: { 35 | loaders: [ 36 | { test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ } 37 | ] 38 | }, 39 | output: { 40 | library: 'ReduxForm', 41 | libraryTarget: 'umd' 42 | }, 43 | plugins: [ 44 | new LodashModuleReplacementPlugin, 45 | new webpack.optimize.OccurenceOrderPlugin(), 46 | new webpack.DefinePlugin({ 47 | 'process.env.NODE_ENV': JSON.stringify(env) 48 | }) 49 | ] 50 | } 51 | 52 | if (env === 'production') { 53 | config.plugins.push( 54 | new webpack.optimize.UglifyJsPlugin({ 55 | compressor: { 56 | pure_getters: true, 57 | unsafe: true, 58 | unsafe_comps: true, 59 | warnings: false 60 | } 61 | }) 62 | ) 63 | config.plugins.push(new webpack.optimize.DedupePlugin()) 64 | } 65 | 66 | module.exports = config 67 | -------------------------------------------------------------------------------- /examples/immutable/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf dist", 5 | "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", 6 | "build": "npm run clean && npm run build:webpack", 7 | "lint": "eslint src", 8 | "start": "node devServer.js", 9 | "prepublish": "npm run lint && npm run build" 10 | }, 11 | "dependencies": { 12 | "babel-polyfill": "^6.16.0", 13 | "html-loader": "^0.4.4", 14 | "immutable": "^3.8.1", 15 | "json-loader": "0.5.4", 16 | "markdown-loader": "0.1.7", 17 | "raw-loader": "0.5.1", 18 | "react": "^15.4.1", 19 | "react-dom": "^15.4.1", 20 | "react-redux": "^4.4.6", 21 | "redux": "^3.6.0", 22 | "redux-form": "file:../../", 23 | "redux-form-website-template": "0.0.41", 24 | "redux-immutablejs": "0.0.8" 25 | }, 26 | "devDependencies": { 27 | "babel-core": "^6.18.2", 28 | "babel-eslint": "^7.1.1", 29 | "babel-loader": "^6.2.8", 30 | "babel-preset-es2015": "^6.18.0", 31 | "babel-preset-react": "^6.16.0", 32 | "cross-env": "^3.1.3", 33 | "eslint": "^3.11.1", 34 | "eslint-config-rackt": "1.1.1", 35 | "eslint-loader": "^1.6.1", 36 | "eslint-plugin-babel": "^4.0.0", 37 | "eslint-plugin-react": "^6.7.1", 38 | "eventsource-polyfill": "0.9.6", 39 | "express": "^4.14.0", 40 | "extract-text-webpack-plugin": "1.0.1", 41 | "redbox-react": "^1.3.3", 42 | "rimraf": "^2.5.4", 43 | "webpack": "^1.13.3", 44 | "webpack-dev-middleware": "^1.8.4", 45 | "webpack-hot-middleware": "^2.13.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/events/__tests__/onChangeValue.spec.js: -------------------------------------------------------------------------------- 1 | import expect, { createSpy } from 'expect' 2 | import onChangeValue from '../onChangeValue' 3 | import { valueMock } from '../../util/eventMocks' 4 | 5 | const name = 'sampleField' 6 | 7 | describe('onChangeValue', () => { 8 | it('should parse the value before returning', () => { 9 | const parse = createSpy((value) => `parsed-${value}`).andCallThrough() 10 | const value = onChangeValue(valueMock('bar'), { name, parse }) 11 | expect(parse) 12 | .toHaveBeenCalled() 13 | .toHaveBeenCalledWith('bar', name) 14 | expect(value) 15 | .toBe('parsed-bar') 16 | }) 17 | 18 | it('should normalize the value before returning', () => { 19 | const normalize = createSpy((_, value) => `normalized-${value}`).andCallThrough() 20 | const value = onChangeValue(valueMock('bar'), { name, normalize }) 21 | expect(normalize) 22 | .toHaveBeenCalled() 23 | .toHaveBeenCalledWith(name, 'bar') 24 | expect(value) 25 | .toBe('normalized-bar') 26 | }) 27 | 28 | it('should parse before normalize', () => { 29 | const parse = createSpy((value) => `parsed-${value}`).andCallThrough() 30 | const normalize = createSpy((_, value) => `normalized-${value}`).andCallThrough() 31 | const value = onChangeValue(valueMock('bar'), { name, normalize, parse }) 32 | expect(parse) 33 | .toHaveBeenCalled() 34 | .toHaveBeenCalledWith('bar', name) 35 | expect(normalize) 36 | .toHaveBeenCalled() 37 | .toHaveBeenCalledWith(name, 'parsed-bar') 38 | expect(value) 39 | .toBe('normalized-parsed-bar') 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /examples/material-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf dist", 5 | "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", 6 | "build": "npm run clean && npm run build:webpack", 7 | "lint": "eslint src", 8 | "start": "node devServer.js", 9 | "prepublish": "npm run lint && npm run build" 10 | }, 11 | "dependencies": { 12 | "babel-polyfill": "^6.16.0", 13 | "html-loader": "^0.4.4", 14 | "json-loader": "0.5.4", 15 | "markdown-loader": "0.1.7", 16 | "material-ui": "^0.16.4", 17 | "raw-loader": "0.5.1", 18 | "react": "^15.4.1", 19 | "react-dom": "^15.4.1", 20 | "react-redux": "^4.4.6", 21 | "react-tap-event-plugin": "^2.0.1", 22 | "redux": "^3.6.0", 23 | "redux-form": "file:../../", 24 | "redux-form-website-template": "0.0.41" 25 | }, 26 | "devDependencies": { 27 | "babel-core": "^6.18.2", 28 | "babel-eslint": "^7.1.1", 29 | "babel-loader": "^6.2.8", 30 | "babel-preset-es2015": "^6.18.0", 31 | "babel-preset-react": "^6.16.0", 32 | "cross-env": "^3.1.3", 33 | "eslint": "^3.11.1", 34 | "eslint-config-rackt": "1.1.1", 35 | "eslint-loader": "^1.6.1", 36 | "eslint-plugin-babel": "^4.0.0", 37 | "eslint-plugin-react": "^6.7.1", 38 | "eventsource-polyfill": "0.9.6", 39 | "express": "^4.14.0", 40 | "extract-text-webpack-plugin": "1.0.1", 41 | "redbox-react": "^1.3.3", 42 | "rimraf": "^2.5.4", 43 | "webpack": "^1.13.3", 44 | "webpack-dev-middleware": "^1.8.4", 45 | "webpack-hot-middleware": "^2.13.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/__tests__/reducer.arrayUnshift.spec.js: -------------------------------------------------------------------------------- 1 | import { arrayUnshift } from '../actions' 2 | 3 | const describeArrayUnshift = (reducer, expect, { fromJS }) => () => { 4 | it('should work with empty state', () => { 5 | const state = reducer(undefined, arrayUnshift('foo', 'myField', 'myValue')) 6 | expect(state) 7 | .toEqualMap({ 8 | foo: { 9 | values: { 10 | myField: [ 'myValue' ] 11 | } 12 | } 13 | }) 14 | }) 15 | 16 | it('should insert at beginning', () => { 17 | const state = reducer(fromJS({ 18 | foo: { 19 | values: { 20 | myField: { 21 | subField: [ 'a', 'b', 'c' ] 22 | } 23 | }, 24 | fields: { 25 | myField: { 26 | subField: [ 27 | { touched: true }, 28 | { touched: true, visited: true }, 29 | { touched: true } 30 | ] 31 | } 32 | } 33 | } 34 | }), arrayUnshift('foo', 'myField.subField', 'newValue')) 35 | expect(state) 36 | .toEqualMap({ 37 | foo: { 38 | values: { 39 | myField: { 40 | subField: [ 'newValue', 'a', 'b', 'c' ] 41 | } 42 | }, 43 | fields: { 44 | myField: { 45 | subField: [ 46 | {}, 47 | { touched: true }, 48 | { touched: true, visited: true }, 49 | { touched: true } 50 | ] 51 | } 52 | } 53 | } 54 | }) 55 | }) 56 | } 57 | 58 | export default describeArrayUnshift 59 | -------------------------------------------------------------------------------- /examples/react-widgets/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'babel-polyfill', 8 | 'eventsource-polyfill', // necessary for hot reloading with IE 9 | 'webpack-hot-middleware/client', 10 | './src/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/dist/' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.HotModuleReplacementPlugin() 20 | ], 21 | resolve: { 22 | modulesDirectories: [ 23 | 'src', 24 | 'node_modules' 25 | ], 26 | extensions: [ '', '.json', '.js' ] 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.jsx?/, 32 | loaders: [ 'babel', 'eslint' ], 33 | include: path.join(__dirname, 'src') 34 | }, 35 | { 36 | test: /\.css$/, 37 | loader: "style-loader!css-loader" 38 | }, 39 | { 40 | test: /\.gif$/, 41 | loader: "url-loader?mimetype=image/png" 42 | }, 43 | { 44 | test: /\.woff(2)?(\?v=[0-9].[0-9].[0-9])?$/, 45 | loader: "url-loader?mimetype=application/font-woff" 46 | }, 47 | { 48 | test: /\.(ttf|eot|svg)(\?v=[0-9].[0-9].[0-9])?$/, 49 | loader: "file-loader?name=[name].[ext]" 50 | }, 51 | { 52 | test: /\.json$/, 53 | loader: 'json-loader' 54 | }, 55 | { 56 | test: /\.md/, 57 | loaders: [ "html-loader", "markdown-loader" ] 58 | } 59 | ] 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /src/selectors/__tests__/isSubmitting.spec.js: -------------------------------------------------------------------------------- 1 | import createIsSubmitting from '../isSubmitting' 2 | import plain from '../../structure/plain' 3 | import plainExpectations from '../../structure/plain/expectations' 4 | import immutable from '../../structure/immutable' 5 | import immutableExpectations from '../../structure/immutable/expectations' 6 | import addExpectations from '../../__tests__/addExpectations' 7 | 8 | const describeIsSubmitting = (name, structure, expect) => { 9 | const isSubmitting = createIsSubmitting(structure) 10 | 11 | const { fromJS, getIn } = structure 12 | 13 | describe(name, () => { 14 | it('should return a function XXX', () => { 15 | expect(isSubmitting('foo')).toBeA('function') 16 | }) 17 | 18 | it('should return false when value not present', () => { 19 | expect(isSubmitting('foo')(fromJS({ 20 | form: {} 21 | }))).toBe(false) 22 | }) 23 | 24 | it('should return true when submitting', () => { 25 | expect(isSubmitting('foo')(fromJS({ 26 | form: { 27 | foo: { 28 | submitting: true 29 | } 30 | } 31 | }))).toBe(true) 32 | }) 33 | 34 | it('should use getFormState if provided', () => { 35 | expect(isSubmitting('foo', state => getIn(state, 'someOtherSlice'))(fromJS({ 36 | someOtherSlice: { 37 | foo: { 38 | submitting: true 39 | } 40 | } 41 | }))).toBe(true) 42 | }) 43 | }) 44 | } 45 | 46 | describeIsSubmitting('isSubmitting.plain', plain, addExpectations(plainExpectations)) 47 | describeIsSubmitting('isSubmitting.immutable', immutable, addExpectations(immutableExpectations)) 48 | -------------------------------------------------------------------------------- /examples/normalizing/src/FieldNormalizing.md: -------------------------------------------------------------------------------- 1 | # Field Normalizing Example 2 | 3 | When you need to put some control between what the user enters and the value that gets stored in 4 | Redux, you can use a "normalizer". A normalizer is just a function that gets run every time a 5 | value is changed that can transform the value before storing. 6 | 7 | One common use case is when you need a value to be in a certain format, like a phone number or a 8 | credit card. 9 | 10 | Normalizers are passed four parameters: 11 | 12 | * `value` - The value of the field on which you have placed the normalizer 13 | * `previousValue` - The value of the field on which you have placed the normalizer before the 14 | most recent change 15 | * `allValues` - All the values in the form, with the current field value set 16 | * `previousAllValues` - All the values in the form before the current field is changed 17 | 18 | This allows you to do things like restrict one field value based on the value of another field, 19 | like the `min` and `max` fields in the example below. Notice that you cannot set `min` to be 20 | greater than `max`, and you cannot set `max` to be less than `min`. 21 | 22 | ## Running this example locally 23 | 24 | To run this example locally on your machine clone the `redux-form` repository, 25 | then `cd redux-form` to change to the repo directory, and run `npm install`. 26 | 27 | Then run `npm run example:normalizing` or manually run the 28 | following commands: 29 | ``` 30 | cd ./examples/normalizing 31 | npm install 32 | npm start 33 | ``` 34 | 35 | Then open [`localhost:3030`](http://localhost:3030) in your 36 | browser to view the example running locally on your machine. 37 | 38 | -------------------------------------------------------------------------------- /src/__tests__/defaultShouldAsyncValidate.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import defaultShouldAsyncValidate from '../defaultShouldAsyncValidate' 3 | 4 | describe('defaultShouldAsyncValidate', () => { 5 | 6 | it('should not async validate if sync validation is not passing', () => { 7 | expect(defaultShouldAsyncValidate({ 8 | syncValidationPasses: false 9 | })).toBe(false) 10 | }) 11 | 12 | it('should async validate if blur triggered and sync passes', () => { 13 | expect(defaultShouldAsyncValidate({ 14 | syncValidationPasses: true, 15 | trigger: 'blur' 16 | })).toBe(true) 17 | }) 18 | 19 | it('should not async validate when pristine and initialized', () => { 20 | expect(defaultShouldAsyncValidate({ 21 | syncValidationPasses: true, 22 | trigger: 'submit', 23 | pristine: true, 24 | initialized: true 25 | })).toBe(false) 26 | }) 27 | 28 | it('should async validate when submitting and dirty', () => { 29 | expect(defaultShouldAsyncValidate({ 30 | syncValidationPasses: true, 31 | trigger: 'submit', 32 | pristine: false, 33 | initialized: true 34 | })).toBe(true) 35 | }) 36 | 37 | it('should async validate when submitting and not initialized', () => { 38 | expect(defaultShouldAsyncValidate({ 39 | syncValidationPasses: true, 40 | trigger: 'submit', 41 | pristine: true, 42 | initialized: false 43 | })).toBe(true) 44 | }) 45 | 46 | it('should not async validate when unknown trigger', () => { 47 | expect(defaultShouldAsyncValidate({ 48 | syncValidationPasses: true, 49 | trigger: 'wtf' 50 | })).toBe(false) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /docs/faq/WebsocketSubmit.md: -------------------------------------------------------------------------------- 1 | # Can I submit my form using websockets? 2 | 3 | Yes. `redux-form` has built-in support for managing `submitting` state and errors using promises, 4 | but you can easily replicate its behavior using any other asynchronous paradigm. All you need do 5 | is to dispatch the `START_SUBMIT` and `STOP_SUBMIT` actions yourself using the exported 6 | [Action Creators](#/api/action-creators). 7 | 8 | ```javascript 9 | import {startSubmit, stopSubmit} from 'redux-form'; 10 | 11 | function submitForm(data, dispatch) { 12 | // tell redux-form that the submission has started 13 | dispatch(startSubmit('myFormName')); 14 | 15 | channels.methods.push('submit:myFormName', {data}) 16 | receive('ok', () => { 17 | // tell redux-form that the submission has stopped 18 | dispatch(stopSubmit('myFormName')); 19 | // ^ not necessary if you are redirecting or doing something 20 | // else that will result in the form state being destroyed 21 | }) 22 | .receive('error', response => { 23 | const {errors} = response; 24 | // tell redux-form that the submission has stopped with errors 25 | dispatch(stopSubmit('myFormName', errors)); 26 | }); 27 | } 28 | ``` 29 | 30 | The `submitForm` function can then be passed either as an `onSubmit` prop to your decorated form 31 | or as a parameter to `handleSubmit()` inside your form component. For more on this, look at 32 | [the `handleSubmit()` docs](#/api/props). 33 | 34 | --- 35 | 36 | Thanks to [Vlad Shcherbin](https://github.com/VladShcherbin) for presenting 37 | [his solution](https://github.com/erikras/redux-form/issues/450#issuecomment-166457681), from 38 | which this answer was modeled. 39 | -------------------------------------------------------------------------------- /src/selectors/__tests__/hasSubmitFailed.spec.js: -------------------------------------------------------------------------------- 1 | import createHasSubmitFailed from '../hasSubmitFailed' 2 | import plain from '../../structure/plain' 3 | import plainExpectations from '../../structure/plain/expectations' 4 | import immutable from '../../structure/immutable' 5 | import immutableExpectations from '../../structure/immutable/expectations' 6 | import addExpectations from '../../__tests__/addExpectations' 7 | 8 | const describeHasSubmitFailed = (name, structure, expect) => { 9 | const hasSubmitFailed = createHasSubmitFailed(structure) 10 | 11 | const { fromJS, getIn } = structure 12 | 13 | describe(name, () => { 14 | it('should return a function XXX', () => { 15 | expect(hasSubmitFailed('foo')).toBeA('function') 16 | }) 17 | 18 | it('should return false when value not present', () => { 19 | expect(hasSubmitFailed('foo')(fromJS({ 20 | form: {} 21 | }))).toBe(false) 22 | }) 23 | 24 | it('should return true when submitting', () => { 25 | expect(hasSubmitFailed('foo')(fromJS({ 26 | form: { 27 | foo: { 28 | submitFailed: true 29 | } 30 | } 31 | }))).toBe(true) 32 | }) 33 | 34 | it('should use getFormState if provided', () => { 35 | expect(hasSubmitFailed('foo', state => getIn(state, 'someOtherSlice'))(fromJS({ 36 | someOtherSlice: { 37 | foo: { 38 | submitFailed: true 39 | } 40 | } 41 | }))).toBe(true) 42 | }) 43 | }) 44 | } 45 | 46 | describeHasSubmitFailed('hasSubmitFailed.plain', plain, addExpectations(plainExpectations)) 47 | describeHasSubmitFailed('hasSubmitFailed.immutable', immutable, addExpectations(immutableExpectations)) 48 | -------------------------------------------------------------------------------- /examples/fieldArrays/src/validate.js: -------------------------------------------------------------------------------- 1 | const validate = values => { 2 | const errors = {} 3 | if(!values.clubName) { 4 | errors.clubName = 'Required' 5 | } 6 | if (!values.members || !values.members.length) { 7 | errors.members = { _error: 'At least one member must be entered' } 8 | } else { 9 | const membersArrayErrors = [] 10 | values.members.forEach((member, memberIndex) => { 11 | const memberErrors = {} 12 | if (!member || !member.firstName) { 13 | memberErrors.firstName = 'Required' 14 | membersArrayErrors[memberIndex] = memberErrors 15 | } 16 | if (!member || !member.lastName) { 17 | memberErrors.lastName = 'Required' 18 | membersArrayErrors[memberIndex] = memberErrors 19 | } 20 | if (member && member.hobbies && member.hobbies.length) { 21 | const hobbyArrayErrors = [] 22 | member.hobbies.forEach((hobby, hobbyIndex) => { 23 | if (!hobby || !hobby.length) { 24 | hobbyArrayErrors[hobbyIndex] = 'Required' 25 | } 26 | }) 27 | if(hobbyArrayErrors.length) { 28 | memberErrors.hobbies = hobbyArrayErrors 29 | membersArrayErrors[memberIndex] = memberErrors 30 | } 31 | if (member.hobbies.length > 5) { 32 | if(!memberErrors.hobbies) { 33 | memberErrors.hobbies = [] 34 | } 35 | memberErrors.hobbies._error = 'No more than five hobbies allowed' 36 | membersArrayErrors[memberIndex] = memberErrors 37 | } 38 | } 39 | }) 40 | if(membersArrayErrors.length) { 41 | errors.members = membersArrayErrors 42 | } 43 | } 44 | return errors 45 | } 46 | 47 | export default validate 48 | -------------------------------------------------------------------------------- /examples/immutable/src/Immutable.md: -------------------------------------------------------------------------------- 1 | # Immutable JS Example 2 | 3 | Using `redux-form` with [Immutable JS](http://facebook.github.io/immutable-js/) could not be 4 | easier. By default, `redux-form` uses plain javascript objects to store its state in Redux, but 5 | if you are using a library, like [`redux-immutable`](https://github.com/gajus/redux-immutable) or 6 | [`redux-immutablejs`](https://github.com/indexiatech/redux-immutablejs) to keep your Redux state 7 | with Immutable JS, you can use the "immutable" version of `redux-form` by importing from 8 | `redux-form/immutable` instead of `redux-form`. 9 | 10 | Also do not forget to import your form reducer from `redux-form/immutable` as well. 11 | 12 | ```js 13 | import { reduxForm } from 'redux-form/immutable' 14 | ``` 15 | 16 | **IMPORTANT**: When you are using the immutable version of `redux-form`, the values that 17 | `redux-form` gives you for validation and submission will be in an `Immutable.Map`. 18 | 19 | This is because proper use of Immutable JS involves doing as little `toJS`/`fromJS` conversions as 20 | possible, and `redux-form` does not know or care which form you want your data in, so it gives it 21 | to you in the form that it is stored. 22 | 23 | ## Running this example locally 24 | 25 | To run this example locally on your machine clone the `redux-form` repository, 26 | then `cd redux-form` to change to the repo directory, and run `npm install`. 27 | 28 | Then run `npm run example:immutable` or manually run the 29 | following commands: 30 | ``` 31 | cd ./examples/immutable 32 | npm install 33 | npm start 34 | ``` 35 | 36 | Then open [`localhost:3030`](http://localhost:3030) in your 37 | browser to view the example running locally on your machine. 38 | 39 | -------------------------------------------------------------------------------- /src/__tests__/reducer.setSubmitSuceeded.spec.js: -------------------------------------------------------------------------------- 1 | import { setSubmitSucceeded } from '../actions' 2 | 3 | const describeSetSubmitSucceeded = (reducer, expect, { fromJS }) => () => { 4 | it('should set submitSucceeded flag on submitSucceeded', () => { 5 | const state = reducer(fromJS({ 6 | foo: { 7 | doesnt: 'matter', 8 | should: 'change' 9 | } 10 | }), setSubmitSucceeded('foo')) 11 | expect(state) 12 | .toEqualMap({ 13 | foo: { 14 | doesnt: 'matter', 15 | should: 'change', 16 | submitSucceeded: true 17 | } 18 | }) 19 | }) 20 | 21 | it('should clear submitting flag on submitSucceeded', () => { 22 | const state = reducer(fromJS({ 23 | foo: { 24 | doesnt: 'matter', 25 | should: 'change', 26 | submitting: true 27 | } 28 | }), setSubmitSucceeded('foo')) 29 | expect(state) 30 | .toEqualMap({ 31 | foo: { 32 | doesnt: 'matter', 33 | should: 'change', 34 | submitSucceeded: true, 35 | submitting: true 36 | } 37 | }) 38 | }) 39 | 40 | it('should clear submitFailed flag on submitSucceeded', () => { 41 | const state = reducer(fromJS({ 42 | foo: { 43 | doesnt: 'matter', 44 | should: 'notchange', 45 | submitting: true, 46 | submitFailed: true 47 | } 48 | }), setSubmitSucceeded('foo')) 49 | expect(state) 50 | .toEqualMap({ 51 | foo: { 52 | doesnt: 'matter', 53 | should: 'notchange', 54 | submitSucceeded: true, 55 | submitting: true 56 | } 57 | }) 58 | }) 59 | } 60 | 61 | export default describeSetSubmitSucceeded 62 | -------------------------------------------------------------------------------- /examples/react-widgets/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | 'babel-polyfill', 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/dist/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | NODE_ENV: JSON.stringify('production') 20 | } 21 | }), 22 | new webpack.optimize.UglifyJsPlugin({ 23 | compressor: { 24 | warnings: false 25 | } 26 | }) 27 | ], 28 | resolve: { 29 | modulesDirectories: [ 30 | 'src', 31 | 'node_modules' 32 | ], 33 | extensions: [ '', '.json', '.js' ] 34 | }, 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.js$/, 39 | loaders: [ 'babel' ], 40 | include: path.join(__dirname, 'src') 41 | }, 42 | { 43 | test: /\.css$/, 44 | loader: "style-loader!css-loader" 45 | }, 46 | { 47 | test: /\.gif$/, 48 | loader: "url-loader?mimetype=image/png" 49 | }, 50 | { 51 | test: /\.woff(2)?(\?v=[0-9].[0-9].[0-9])?$/, 52 | loader: "url-loader?mimetype=application/font-woff" 53 | }, 54 | { 55 | test: /\.(ttf|eot|svg)(\?v=[0-9].[0-9].[0-9])?$/, 56 | loader: "file-loader?name=[name].[ext]" 57 | }, 58 | { 59 | test: /\.json$/, 60 | loader: 'json-loader' 61 | }, 62 | { 63 | test: /\.md/, 64 | loaders: [ "html-loader", "markdown-loader" ] 65 | } 66 | ] 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /src/selectors/__tests__/hasSubmitSucceeded.spec.js: -------------------------------------------------------------------------------- 1 | import createHasSubmitSucceeded from '../hasSubmitSucceeded' 2 | import plain from '../../structure/plain' 3 | import plainExpectations from '../../structure/plain/expectations' 4 | import immutable from '../../structure/immutable' 5 | import immutableExpectations from '../../structure/immutable/expectations' 6 | import addExpectations from '../../__tests__/addExpectations' 7 | 8 | const describeHasSubmitSucceeded = (name, structure, expect) => { 9 | const hasSubmitSucceeded = createHasSubmitSucceeded(structure) 10 | 11 | const { fromJS, getIn } = structure 12 | 13 | describe(name, () => { 14 | it('should return a function XXX', () => { 15 | expect(hasSubmitSucceeded('foo')).toBeA('function') 16 | }) 17 | 18 | it('should return false when value not present', () => { 19 | expect(hasSubmitSucceeded('foo')(fromJS({ 20 | form: {} 21 | }))).toBe(false) 22 | }) 23 | 24 | it('should return true when submitting', () => { 25 | expect(hasSubmitSucceeded('foo')(fromJS({ 26 | form: { 27 | foo: { 28 | submitSucceeded: true 29 | } 30 | } 31 | }))).toBe(true) 32 | }) 33 | 34 | it('should use getFormState if provided', () => { 35 | expect(hasSubmitSucceeded('foo', state => getIn(state, 'someOtherSlice'))(fromJS({ 36 | someOtherSlice: { 37 | foo: { 38 | submitSucceeded: true 39 | } 40 | } 41 | }))).toBe(true) 42 | }) 43 | }) 44 | } 45 | 46 | describeHasSubmitSucceeded('hasSubmitSucceeded.plain', plain, addExpectations(plainExpectations)) 47 | describeHasSubmitSucceeded('hasSubmitSucceeded.immutable', immutable, addExpectations(immutableExpectations)) 48 | -------------------------------------------------------------------------------- /examples/wizard/src/WizardFormThirdPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Field, reduxForm } from 'redux-form' 3 | import validate from './validate' 4 | const colors = [ 'Red', 'Orange', 'Yellow', 'Green', 'Blue', 'Indigo', 'Violet' ] 5 | 6 | const renderColorSelector = ({ input, meta: { touched, error } }) => ( 7 |
8 | 12 | {touched && error && {error}} 13 |
14 | ) 15 | 16 | const WizardFormThirdPage = (props) => { 17 | const { handleSubmit, pristine, previousPage, submitting } = props 18 | return ( 19 | 20 |
21 | 22 | 23 |
24 |
25 | 26 |
27 | 28 |
29 |
30 |
31 | 32 |
33 | 34 |
35 |
36 |
37 | 38 | 39 |
40 | 41 | ) 42 | } 43 | export default reduxForm({ 44 | form: 'wizard', //Form name is same 45 | destroyOnUnmount: false, 46 | forceUnregisterOnUnmount: true, // <------ unregister fields on unmount 47 | validate 48 | })(WizardFormThirdPage) 49 | -------------------------------------------------------------------------------- /src/selectors/__tests__/getFormValues.spec.js: -------------------------------------------------------------------------------- 1 | import createGetFormValues from '../getFormValues' 2 | import plain from '../../structure/plain' 3 | import plainExpectations from '../../structure/plain/expectations' 4 | import immutable from '../../structure/immutable' 5 | import immutableExpectations from '../../structure/immutable/expectations' 6 | import addExpectations from '../../__tests__/addExpectations' 7 | 8 | const describeGetFormValues = (name, structure, expect) => { 9 | const getFormValues = createGetFormValues(structure) 10 | 11 | const { fromJS, getIn } = structure 12 | 13 | describe(name, () => { 14 | it('should return a function', () => { 15 | expect(getFormValues('foo')).toBeA('function') 16 | }) 17 | 18 | it('should get the form values from state', () => { 19 | expect(getFormValues('foo')(fromJS({ 20 | form: { 21 | foo: { 22 | values: { 23 | dog: 'Snoopy', 24 | cat: 'Garfield' 25 | } 26 | } 27 | } 28 | }))).toEqualMap({ 29 | dog: 'Snoopy', 30 | cat: 'Garfield' 31 | }) 32 | }) 33 | 34 | it('should use getFormState if provided', () => { 35 | expect(getFormValues('foo', state => getIn(state, 'someOtherSlice'))(fromJS({ 36 | someOtherSlice: { 37 | foo: { 38 | values: { 39 | dog: 'Snoopy', 40 | cat: 'Garfield' 41 | } 42 | } 43 | } 44 | }))).toEqualMap({ 45 | dog: 'Snoopy', 46 | cat: 'Garfield' 47 | }) 48 | }) 49 | }) 50 | } 51 | 52 | describeGetFormValues('getFormValues.plain', plain, addExpectations(plainExpectations)) 53 | describeGetFormValues('getFormValues.immutable', immutable, addExpectations(immutableExpectations)) 54 | -------------------------------------------------------------------------------- /src/structure/immutable/setIn.js: -------------------------------------------------------------------------------- 1 | import { List, Map } from 'immutable' 2 | import { toPath } from 'lodash' 3 | 4 | const arrayPattern = /\[(\d+)\]/ 5 | 6 | const undefinedArrayMerge = (previous, next) => 7 | next !== undefined 8 | ? next 9 | : previous 10 | 11 | const mergeLists = (original, value) => 12 | original && List.isList(original) 13 | ? original.mergeDeepWith(undefinedArrayMerge, value) 14 | : value 15 | 16 | /* 17 | * ImmutableJS' setIn function doesn't support array (List) creation 18 | * so we must pre-insert all arrays in the path ahead of time. 19 | * 20 | * Additionally we must also pre-set a dummy Map at the location 21 | * of an array index if there's parts that come afterwards because 22 | * the setIn function uses `{}` to mark an unset value instead of 23 | * undefined (which is the case for list / arrays). 24 | */ 25 | export default function setIn(state, field, value) { 26 | if (!field || typeof field !== 'string' || !arrayPattern.test(field)) { 27 | return state.setIn(toPath(field), value) 28 | } 29 | 30 | return state.withMutations(mutable => { 31 | let arraySafePath = field.split('.') 32 | let pathSoFar = null 33 | 34 | for (let partIndex in arraySafePath) { 35 | let part = arraySafePath[partIndex] 36 | let match = arrayPattern.exec(part) 37 | 38 | pathSoFar = pathSoFar === null ? part : `${pathSoFar}.${part}` 39 | 40 | if (!match) continue 41 | 42 | let arr = [] 43 | arr[parseInt(match[1])] = partIndex + 1 >= arraySafePath.length 44 | ? new Map() 45 | : undefined 46 | 47 | mutable = mutable.updateIn( 48 | toPath(pathSoFar).slice(0, -1), 49 | value => mergeLists(value, new List(arr))) 50 | } 51 | 52 | return mutable.setIn(toPath(field), value) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /examples/submitValidation/src/SubmitValidation.md: -------------------------------------------------------------------------------- 1 | # Submit Validation Example 2 | 3 | The recommended way to do server-side validation with `redux-form` is to return a rejected promise 4 | from the `onSubmit` function. There are two ways to give `redux-form` a function to run when your 5 | form is submitted: 6 | 7 | 1. Pass it as an `onSubmit` prop to your decorated component. In which case, you would use 8 | `onSubmit={this.props.handleSubmit}` inside your decorated component to cause it to fire when the 9 | submit button is clicked. 10 | 2. Pass it as a parameter to the `this.props.handleSubmit` function _from inside your 11 | decorated component_. In which case, you would use `onClick={this.props.handleSubmit(mySubmit)}` 12 | inside your decorated component to cause it to fire when the submit button is clicked. 13 | 14 | The errors are displayed in the exact same way as validation errors created by 15 | [Synchronous Validation](../syncValidation), but they are returned from the `onSubmit` 16 | function wrapped in a `SubmissionError`. This is to differentiate validation errors from I/O 17 | errors, like HTTP `400` or `500` errors, which will also cause the submission promise to be 18 | rejected. 19 | 20 | Also note that a general form-wide error can be returned via the special `_error` key. 21 | 22 | ## Running this example locally 23 | 24 | To run this example locally on your machine clone the `redux-form` repository, 25 | then `cd redux-form` to change to the repo directory, and run `npm install`. 26 | 27 | Then run `npm run example:submitValidation` or manually run the 28 | following commands: 29 | ``` 30 | cd ./examples/submitValidation 31 | npm install 32 | npm start 33 | ``` 34 | 35 | ## How to use the form below: 36 | 37 | * Usernames that will pass validation: `john`, `paul`, `george`, or `ringo`. 38 | * Valid password for all users: `redux-form`. 39 | 40 | -------------------------------------------------------------------------------- /examples/material-ui/src/MaterialUi.md: -------------------------------------------------------------------------------- 1 | # Material UI Example 2 | 3 | This is a simple demonstration of how to connect all the standard 4 | [material-ui](https://github.com/callemall/material-ui) form elements to `redux-form`. 5 | 6 | For the most part, it is a matter of wrapping each form control in a `` 7 | component as custom component. 8 | 9 | For controls like `SelectField` we need to simulate the `onChange` manually. As props 10 | have been exposed in `redux-form` you can fire `onChange` manually. 11 | Read more [here](https://redux-form.com/6.4.1/docs/api/Field.md/#usage). 12 | 13 | The delay between when you click "Submit" and when the alert dialog pops up is intentional, 14 | to simulate server latency. 15 | 16 | ## Running this example locally 17 | 18 | To run this example locally on your machine clone the `redux-form` repository, 19 | then `cd redux-form` to change to the repo directory, and run `npm install`. 20 | 21 | Then run `npm run example:material-ui` or manually run the 22 | following commands: 23 | ``` 24 | cd ./examples/material-ui 25 | npm install 26 | npm start 27 | ``` 28 | 29 | Then open [`localhost:3030`](http://localhost:3030) in your 30 | browser to view the example running locally on your machine. 31 | 32 | ## Field Renderers 33 | 34 | Notice that we define simple functions, like `renderTextField`, `renderCheckbox`, and 35 | `renderSelectField` to form a bridge between `redux-form` and the Material UI input components. 36 | You would only need to define these in one place in your application and reuse them in each form. 37 | 38 | For Material UI, `@erikras` has published a set of wrapper components to use Material UI: 39 | [`redux-form-material-ui`](https://github.com/erikras/redux-form-material-ui). 40 | 41 | ## How to use async validation in form: 42 | 43 | * Emails that will _fail_ validation: `foo@foo.com`, `bar@bar.com`. 44 | 45 | -------------------------------------------------------------------------------- /src/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const ARRAY_INSERT = '@@redux-form/ARRAY_INSERT' 2 | export const ARRAY_MOVE = '@@redux-form/ARRAY_MOVE' 3 | export const ARRAY_POP = '@@redux-form/ARRAY_POP' 4 | export const ARRAY_PUSH = '@@redux-form/ARRAY_PUSH' 5 | export const ARRAY_REMOVE = '@@redux-form/ARRAY_REMOVE' 6 | export const ARRAY_REMOVE_ALL = '@@redux-form/ARRAY_REMOVE_ALL' 7 | export const ARRAY_SHIFT = '@@redux-form/ARRAY_SHIFT' 8 | export const ARRAY_SPLICE = '@@redux-form/ARRAY_SPLICE' 9 | export const ARRAY_UNSHIFT = '@@redux-form/ARRAY_UNSHIFT' 10 | export const ARRAY_SWAP = '@@redux-form/ARRAY_SWAP' 11 | export const AUTOFILL = '@@redux-form/AUTOFILL' 12 | export const BLUR = '@@redux-form/BLUR' 13 | export const CHANGE = '@@redux-form/CHANGE' 14 | export const CLEAR_SUBMIT = '@@redux-form/CLEAR_SUBMIT' 15 | export const CLEAR_ASYNC_ERROR = '@redux-form/CLEAR_ASYNC_ERROR' 16 | export const DESTROY = '@@redux-form/DESTROY' 17 | export const FOCUS = '@@redux-form/FOCUS' 18 | export const INITIALIZE = '@@redux-form/INITIALIZE' 19 | export const REGISTER_FIELD = '@@redux-form/REGISTER_FIELD' 20 | export const RESET = '@@redux-form/RESET' 21 | export const SET_SUBMIT_FAILED = '@@redux-form/SET_SUBMIT_FAILED' 22 | export const SET_SUBMIT_SUCCEEDED = '@@redux-form/SET_SUBMIT_SUCCEEDED' 23 | export const START_ASYNC_VALIDATION = '@@redux-form/START_ASYNC_VALIDATION' 24 | export const START_SUBMIT = '@@redux-form/START_SUBMIT' 25 | export const STOP_ASYNC_VALIDATION = '@@redux-form/STOP_ASYNC_VALIDATION' 26 | export const STOP_SUBMIT = '@@redux-form/STOP_SUBMIT' 27 | export const SUBMIT = '@@redux-form/SUBMIT' 28 | export const TOUCH = '@@redux-form/TOUCH' 29 | export const UNREGISTER_FIELD = '@@redux-form/UNREGISTER_FIELD' 30 | export const UNTOUCH = '@@redux-form/UNTOUCH' 31 | export const UPDATE_SYNC_ERRORS = '@@redux-form/UPDATE_SYNC_ERRORS' 32 | export const UPDATE_SYNC_WARNINGS = '@@redux-form/UPDATE_SYNC_WARNINGS' 33 | -------------------------------------------------------------------------------- /src/createFieldArrayProps.js: -------------------------------------------------------------------------------- 1 | const createFieldArrayProps = (getIn, name, 2 | { 3 | arrayInsert, arrayMove, arrayPop, arrayPush, arrayRemove, arrayRemoveAll, arrayShift, 4 | arraySplice, arraySwap, arrayUnshift, asyncError, // eslint-disable-line no-unused-vars 5 | dirty, length, pristine, submitError, state, 6 | submitFailed, submitting, // eslint-disable-line no-unused-vars 7 | syncError, syncWarning, value, props, ...rest 8 | }) => { 9 | const error = syncError || asyncError || submitError 10 | const warning = syncWarning 11 | const finalProps = { 12 | fields: { 13 | _isFieldArray: true, 14 | forEach: callback => (value || []).forEach((item, index) => callback(`${name}[${index}]`, index, finalProps.fields)), 15 | get: index => value && getIn(value, index), 16 | getAll: () => value, 17 | insert: arrayInsert, 18 | length, 19 | map: callback => (value || []).map((item, index) => callback(`${name}[${index}]`, index, finalProps.fields)), 20 | move: arrayMove, 21 | name, 22 | pop: () => { 23 | arrayPop() 24 | return getIn(value, length - 1) 25 | }, 26 | push: arrayPush, 27 | reduce: (callback, initial) => (value || []) 28 | .reduce((accumulator, item, index) => callback(accumulator, `${name}[${index}]`, index, finalProps.fields), initial), 29 | remove: arrayRemove, 30 | removeAll: arrayRemoveAll, 31 | shift: () => { 32 | arrayShift() 33 | return getIn(value, 0) 34 | }, 35 | swap: arraySwap, 36 | unshift: arrayUnshift 37 | }, 38 | meta: { 39 | dirty, 40 | error, 41 | warning, 42 | invalid: !!error, 43 | pristine, 44 | submitting, 45 | touched: !!(state && getIn(state, 'touched')), 46 | valid: !error 47 | }, 48 | ...props, 49 | ...rest 50 | } 51 | return finalProps 52 | } 53 | 54 | export default createFieldArrayProps 55 | -------------------------------------------------------------------------------- /src/__tests__/fieldKeys.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | EXPERIMENTAL 3 | 4 | import expect from 'expect' 5 | import fieldKeys from '../fieldKeys' 6 | 7 | describe('fieldKeys', () => { 8 | it('should return an empty array if no fields or values', () => { 9 | expect([ ...fieldKeys() ]) 10 | .toEqual([]) 11 | }) 12 | 13 | it('should handle simple fields with no values', () => { 14 | expect([ ...fieldKeys( 15 | [ 'a', 'b', 'c' ] 16 | ) ]) 17 | .toEqual([ 'a', 'b', 'c' ]) 18 | }) 19 | 20 | it('should handle simple fields with values', () => { 21 | expect([ ...fieldKeys( 22 | [ 'a', 'b', 'c' ], 23 | { a: 1, b: 2, c: 3 } 24 | ) ]) 25 | .toEqual([ 'a', 'b', 'c' ]) 26 | }) 27 | 28 | it('should handle deep fields with no values', () => { 29 | expect([ ...fieldKeys( 30 | [ 'a.b.c', 'd.e.f' ] 31 | ) ]) 32 | .toEqual([ 'a.b.c', 'd.e.f' ]) 33 | }) 34 | 35 | it('should handle deep fields with values', () => { 36 | expect([ ...fieldKeys( 37 | [ 'a.b.c', 'd.e.f' ], 38 | { 39 | a: { 40 | b: { 41 | c: 42 42 | } 43 | }, 44 | d: { 45 | e: { 46 | f: 43 47 | } 48 | } 49 | } 50 | ) ]) 51 | .toEqual([ 'a.b.c', 'd.e.f' ]) 52 | }) 53 | 54 | it('should handle array fields with no values', () => { 55 | expect([ ...fieldKeys( 56 | [ 'a[]', 'b.c[]', 'd[].e' ] 57 | ) ]) 58 | .toEqual([ 'a[]', 'b.c[]', 'd[]' ]) 59 | }) 60 | 61 | it('should handle array fields with values', () => { 62 | expect([ ...fieldKeys( 63 | [ 'a[]', 'b.c[]', 'd[].e', 'f[].g' ], 64 | { 65 | a: [ 'dog', 'cat' ], 66 | b: { 67 | c: [ 'pig' ] 68 | }, 69 | d: [ 70 | { e: 'h' }, 71 | { e: 'i' } 72 | ] 73 | } 74 | ) ]) 75 | .toEqual([ 'a[0]', 'a[1]', 'b.c[0]', 'd[0].e', 'd[1].e', 'f[]' ]) 76 | }) 77 | }) 78 | 79 | */ 80 | -------------------------------------------------------------------------------- /examples/asyncValidation/src/AsyncValidation.md: -------------------------------------------------------------------------------- 1 | # Async Blur Validation Example 2 | 3 | The recommended way to provide server-side validation is to use 4 | [Submit Validation](../submitValidation), but there may be instances when you want to run 5 | server-side validation _while the form is being filled out_. The classic example of this 6 | letting someone choose a value, like a username, that must be unique within your system. 7 | 8 | To provide asynchronous validation, provide `redux-form` with an asynchronous validation 9 | function (`asyncValidate`) that takes an object of form values, and the Redux `dispatch` 10 | function, and returns a promise that either rejects with an object of errors or resolves. 11 | 12 | You will also need to specify which fields should fire the asynchronous validation when 13 | they are blurred with the `asyncBlurFields` config property. 14 | 15 | ## Important 16 | 17 | 1. Asynchronous validation _will_ be called before the `onSubmit` is fired, but if all 18 | you care about is validation `onSubmit`, you should use 19 | [Submit Validation](../submitValidation). 20 | 2. Asynchronous validation will _not_ be called if synchronous validation is failing 21 | _for the field just blurred_. 22 | 23 | The errors are displayed in the exact same way as validation errors created by 24 | [Synchronous Validation](../syncValidation). 25 | 26 | ## Running this example locally 27 | 28 | To run this example locally on your machine clone the `redux-form` repository, 29 | then `cd redux-form` to change to the repo directory, and run `npm install`. 30 | 31 | Then run `npm run example:asyncValidation` or manually run the 32 | following commands: 33 | ``` 34 | cd ./examples/asyncValidation 35 | npm install 36 | npm start 37 | ``` 38 | 39 | Then open [`localhost:3030`](http://localhost:3030) in your 40 | browser to view the example running locally on your machine. 41 | 42 | ## How to use the form below: 43 | 44 | * Usernames that will _fail_ validation: `john`, `paul`, `george` or `ringo`. 45 | 46 | -------------------------------------------------------------------------------- /examples/selectingFormValues/src/SelectingFormValues.md: -------------------------------------------------------------------------------- 1 | # Selecting Form Values Example 2 | 3 | There may be times when, in your form component, you would like to have access to the values of 4 | some of the fields in your form. To get them, you will need to `connect()` directly to the form 5 | values in the Redux store. To facilitate this common use case, `redux-form` provides a convenient 6 | selector via the 7 | [`formValueSelector`](https://redux-form.com/6.4.1/docs/api/FormValueSelector.md/) 8 | API. 9 | 10 | **WARNING**: Use this method sparingly, as it will cause your _entire_ form to re-render every 11 | time one of the values you are selecting changes. 12 | 13 | ## Running this example locally 14 | 15 | To run this example locally on your machine clone the `redux-form` repository, 16 | then `cd redux-form` to change to the repo directory, and run `npm install`. 17 | 18 | Then run `npm run example:selectingFormValues` or manually run the 19 | following commands: 20 | ``` 21 | cd ./examples/selectingFormValues 22 | npm install 23 | npm start 24 | ``` 25 | 26 | Then open [`localhost:3030`](http://localhost:3030) in your 27 | browser to view the example running locally on your machine. 28 | 29 | ## Usage 30 | 31 | The selector is used in two steps, because the first step will generally apply to all the further 32 | usages. 33 | 34 | First you create a selector by giving your form name: 35 | 36 | ```js 37 | const selector = formValueSelector('myFormName') 38 | ``` 39 | 40 | This will create a function that will get any value from that form from the global Redux state. 41 | Then you can either request a single value from the global state, with 42 | 43 | ```js 44 | const value = selector(state, 'fieldName') 45 | ``` 46 | 47 | OR, you may select multiple values, by passing additional field names. 48 | 49 | ```js 50 | const values = selector(state, 'street', 'city', 'postalCode') 51 | ``` 52 | 53 | which would give you an object, like: 54 | 55 | ```js 56 | { 57 | street: '1600 Pennsylvania Avenue', 58 | city: 'Washington, D.C.', 59 | postalCode: '20500' 60 | } 61 | ``` 62 | --------------------------------------------------------------------------------