├── .editorconfig ├── .gitignore ├── .prettierrc ├── .travis.yml ├── HISTORY.md ├── LICENSE.md ├── README.md ├── examples ├── across-router.html ├── across-router.js ├── async-init.html ├── async-init.js ├── dynamic-fields.html ├── dynamic-fields.js ├── dynamic.html ├── dynamic.js ├── file-input.html ├── file-input.js ├── getFieldDecorator.html ├── getFieldDecorator.js ├── input-array.html ├── input-array.js ├── modal.html ├── modal.js ├── nested-field.html ├── nested-field.js ├── normalize.html ├── normalize.js ├── overview.html ├── overview.js ├── parallel-form.html ├── parallel-form.js ├── promise-validate.html ├── promise-validate.js ├── react-native │ ├── App.js │ └── expo.jpg ├── redux.html ├── redux.js ├── server-validate.html ├── server-validate.js ├── setFieldsValue.html ├── setFieldsValue.js ├── start-end-date.html ├── start-end-date.js ├── styles.js ├── suggest.html ├── suggest.js ├── validateFirst.html ├── validateFirst.js ├── validateTrigger.html └── validateTrigger.js ├── index.js ├── package.json ├── scripts └── index.js ├── src ├── FieldElemWrapper.js ├── createBaseForm.js ├── createDOMForm.js ├── createFieldsStore.js ├── createForm.js ├── createFormField.js ├── index.js ├── propTypes.js └── utils.js └── tests ├── FieldElemWrapper.spec.js ├── async-validation.spec.js ├── clean-field.spec.js ├── createDOMForm.spec.js ├── createForm.spec.js ├── dynamic-binding.spec.js ├── dynamic-rule.spec.js ├── form.spec.js ├── getFieldDecorator.spec.js ├── getFieldProps.spec.js ├── index.js ├── overview.spec.js ├── setup.js ├── switch-field.spec.js ├── utils.spec.js ├── validateArray.spec.js ├── validateFields.spec.js └── validateFieldsAndScroll.spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*.{js,css}] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.log 3 | .idea 4 | .ipr 5 | .iws 6 | *~ 7 | ~* 8 | *.diff 9 | *.patch 10 | *.bak 11 | .DS_Store 12 | Thumbs.db 13 | .project 14 | .*proj 15 | .svn 16 | *.swp 17 | *.swo 18 | *.pyc 19 | *.pyo 20 | node_modules 21 | .cache 22 | *.css 23 | build 24 | dist 25 | lib 26 | coverage 27 | yarn.lock -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: false 4 | 5 | node_js: 6 | - 11 7 | 8 | script: 9 | - | 10 | if [ "$TEST_TYPE" = test ]; then 11 | npm test 12 | else 13 | npm run $TEST_TYPE 14 | fi 15 | env: 16 | matrix: 17 | - TEST_TYPE=lint 18 | - TEST_TYPE=test 19 | - TEST_TYPE=coverage 20 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | ---- 3 | 4 | ## 2.4.10 / 2019-11-09 5 | 6 | - There should not be any lifecycle warning now! 7 | 8 | ## 2.4.0 / 2018-12-21 9 | 10 | - Field option add `preserve` to enable you keep value even if field removed. 11 | 12 | ## 2.3.0 / 2018-12-17 13 | 14 | - `createForm` add `name` prop. It will be as prefix with `fieldNameProp` 15 | 16 | ## 2.2.1 / 2018-07-11 17 | 18 | - `validateFieldsAndScroll` should ignore `input[type=hidden]` [#169](https://github.com/react-component/form/pull/169) [@HsuTing](https://github.com/HsuTing) 19 | - `mapPropsToFields` and so on support field with `null` value [#159](https://github.com/react-component/form/pull/159) [@normanrz](https://github.com/normanrz) 20 | 21 | ## 2.2.0 / 2018-04-04 22 | 23 | - Support callback in setFieldsValue [#144](https://github.com/react-component/form/pull/144) 24 | - Expose formShape [#154](https://github.com/react-component/form/pull/154) [@sylvainar](https://github.com/sylvainar) 25 | - Fix bug with disordered array [#143](https://github.com/react-component/form/pull/143) 26 | 27 | ## 2.1.0 / 2017-11-22 28 | 29 | - Support switching from different components with same field name. [#117](https://github.com/react-component/form/pull/117) 30 | 31 | ## 2.0.0 / 2017-11-07 32 | 33 | - Remove `option.exclusive` of `getFieldProps` and `getFieldDecorator`, just use something like [`antd.Radio.Group`](https://ant.design/components/radio/#components-radio-demo-radiogroup) or [`antd.Checkbox.Group`](https://ant.design/components/checkbox/#components-checkbox-demo-group) as workaround. 34 | - Add `createFormField`, and you must use it to wrap field data in `option.mapPropsToFields` of `createForm` or `createDOMForm`: 35 | Before rc-form@2.0.0: 36 | ```jsx 37 | import { createForm } from 'rc-form'; 38 | createFrom({ 39 | mapPropsToFields() { 40 | return { 41 | name: { value: 'rc-form' }, 42 | }; 43 | }, 44 | }) 45 | ``` 46 | After rc-form@2.0.0: 47 | ```jsx 48 | import { createForm, createFormField } from 'rc-form'; 49 | createFrom({ 50 | mapPropsToFields() { 51 | return { 52 | name: createFormField({ value: 'rc-form' }), 53 | }; 54 | }, 55 | }) 56 | ``` 57 | - Deprecate `form.isSubmitting` and `form.submit`, just handle submit status in your own code. 58 | 59 | 60 | ## 1.4.0 / 2017-06-13 61 | 62 | - support wrappedComponentRef and deprecate withRef [#87](https://github.com/react-component/form/pull/87) 63 | 64 | ## 1.3.0 / 2017-01-07 65 | 66 | - support touch checking: https://github.com/react-component/form/pull/56 67 | 68 | ## 1.2.0 / 2017-01-05 69 | 70 | - support onValuesChange: https://github.com/react-component/form/pull/55 71 | 72 | ## 1.1.0 / 2016-12-28 73 | 74 | - support nested field: https://github.com/react-component/form/pull/48 75 | 76 | ## 1.0.0 / 2016-08-29 77 | 78 | - support getFieldDecorator. stable. 79 | 80 | ## 0.17.0 / 2016-06-12 81 | 82 | - support checkbox radio https://github.com/react-component/form/pull/21 83 | - add exclusive config 84 | 85 | ## 0.16.0 / 2016-05-19 86 | 87 | - move instance to this.instances 88 | 89 | ## 0.15.0 / 2016-03-28 90 | 91 | - add getValueFromEvent/getValueProps 92 | 93 | 94 | ## 0.14.0 / 2016-02-27 95 | 96 | - remove refComponent prop.(defaults to true), so you must use getFieldInstance method to get instance instead of ref 97 | 98 | ## 0.13.0 / 2016-02-14 99 | 100 | - support rc-form/lib/createDOMForm 101 | 102 | ## 0.12.0 / 2016-02-02 103 | 104 | - support refComponent/mapProps option for createForm to scroll error fields into view. 105 | 106 | ## 0.11.0 / 2016-02-02 107 | 108 | - support validateMessages of createForm option. 109 | 110 | ## 0.10.0 / 2016-01-27 111 | 112 | - support setFieldsInitialValue/submit/isFieldsValidating/isSubmitting method for this.props.form 113 | 114 | ## 0.9.0 / 2016-01-18 115 | 116 | - support force, force to revalidate. 117 | 118 | ``` 119 | this.props.validateFields(['xx'], {force: true}). 120 | ``` 121 | 122 | ## 0.8.0 / 2016-01-13 123 | 124 | - support validate/validateFirst option for getFieldProps 125 | 126 | ## 0.7.0 / 2015-12-29 127 | 128 | - support this.props.form.resetFields 129 | 130 | ## 0.6.0 / 2015-12-28 131 | 132 | - support normalize in getFieldProps option 133 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-present yiminghe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rc-form 2 | 3 | React High Order Form Component. 4 | 5 | [![NPM version][npm-image]][npm-url] 6 | [![build status][travis-image]][travis-url] 7 | [![Test coverage][coveralls-image]][coveralls-url] 8 | [![gemnasium deps][gemnasium-image]][gemnasium-url] 9 | [![node version][node-image]][node-url] 10 | [![npm download][download-image]][download-url] 11 | [![Code Quality: Javascript][lgtm-badge]][lgtm-badge-url] 12 | [![Total alerts][lgtm-alerts]][lgtm-alerts-url] 13 | 14 | [npm-image]: http://img.shields.io/npm/v/rc-form.svg?style=flat-square 15 | [npm-url]: http://npmjs.org/package/rc-form 16 | [travis-image]: https://img.shields.io/travis/react-component/form.svg?style=flat-square 17 | [travis-url]: https://travis-ci.org/react-component/form 18 | [coveralls-image]: https://img.shields.io/coveralls/react-component/form.svg?style=flat-square 19 | [coveralls-url]: https://coveralls.io/r/react-component/form?branch=master 20 | [gemnasium-image]: http://img.shields.io/gemnasium/react-component/form.svg?style=flat-square 21 | [gemnasium-url]: https://gemnasium.com/react-component/form 22 | [node-image]: https://img.shields.io/badge/node.js-%3E=_0.10-green.svg?style=flat-square 23 | [node-url]: http://nodejs.org/download/ 24 | [download-image]: https://img.shields.io/npm/dm/rc-form.svg?style=flat-square 25 | [download-url]: https://npmjs.org/package/rc-form 26 | [lgtm-badge]: https://img.shields.io/lgtm/grade/javascript/g/react-component/form.svg?logo=lgtm&logoWidth=18 27 | [lgtm-badge-url]: https://lgtm.com/projects/g/react-component/form/context:javascript 28 | [lgtm-alerts]: https://img.shields.io/lgtm/alerts/g/react-component/form.svg?logo=lgtm&logoWidth=18 29 | [lgtm-alerts-url]: https://lgtm.com/projects/g/react-component/form/alerts 30 | 31 | ## Development 32 | 33 | ``` 34 | npm install 35 | npm start 36 | open http://localhost:8000/examples/ 37 | ``` 38 | 39 | ## Feature 40 | 41 | * Support react.js and even react-native 42 | * Validate fields with [async-validator](https://github.com/yiminghe/async-validator/) 43 | 44 | ## Install 45 | 46 | [![rc-form](https://nodei.co/npm/rc-form.png)](https://npmjs.org/package/rc-form) 47 | 48 | ## Usage 49 | 50 | ```js 51 | import { createForm, formShape } from 'rc-form'; 52 | 53 | class Form extends React.Component { 54 | static propTypes = { 55 | form: formShape, 56 | }; 57 | 58 | submit = () => { 59 | this.props.form.validateFields((error, value) => { 60 | console.log(error, value); 61 | }); 62 | } 63 | 64 | render() { 65 | let errors; 66 | const { getFieldProps, getFieldError } = this.props.form; 67 | return ( 68 |
69 | 70 | 74 | {(errors = getFieldError('required')) ? errors.join(',') : null} 75 | 76 |
77 | ); 78 | } 79 | } 80 | 81 | export createForm()(Form); 82 | ``` 83 | 84 | ### Use with React Native 85 | 86 | Expo preview 87 | 88 | ![avatar](./examples/react-native/expo.jpg) 89 | 90 | [View the source code](./examples/react-native/App.js) 91 | 92 | Or a quicker version: 93 | 94 | ```js 95 | import { createForm } from 'rc-form'; 96 | 97 | class Form extends React.Component { 98 | componentWillMount() { 99 | this.requiredDecorator = this.props.form.getFieldDecorator('required', { 100 | rules: [{required: true}], 101 | }); 102 | } 103 | 104 | submit = () => { 105 | this.props.form.validateFields((error, value) => { 106 | console.log(error, value); 107 | }); 108 | } 109 | 110 | render() { 111 | let errors; 112 | const { getFieldError } = this.props.form; 113 | return ( 114 |
115 | {this.requiredDecorator( 116 | 121 | )} 122 | {(errors = getFieldError('required')) ? errors.join(',') : null} 123 | 124 |
125 | ); 126 | } 127 | } 128 | 129 | export createForm()(Form); 130 | ``` 131 | 132 | ## createForm(option: Object) => (WrappedComponent: React.Component) => React.Component 133 | 134 | | Option | Description | Type | Default | 135 | |-----------|------------------------------------------|------------|---------| 136 | | option.validateMessages | Preseted messages of [async-validator](https://github.com/yiminghe/async-validator) | Object | {} | 137 | | option.onFieldsChange | Called when field changed, you can dispatch fields to redux store. | (props, changed, all): void | NOOP | 138 | | option.onValuesChange | Called when value changed. | (props, changed, all): void | NOOP | 139 | | option.mapProps | Get new props transferred to WrappedComponent. | (props): Object | props => props | 140 | | option.mapPropsToFields | Convert value from props to fields. Used for read fields from redux store. | (props): Object | NOOP | 141 | | option.fieldNameProp | Where to store the `name` argument of `getFieldProps`. | String | - | 142 | | option.fieldMetaProp | Where to store the meta data of `getFieldProps`. | String | - | 143 | | option.fieldDataProp | Where to store the field data | String | - | 144 | | option.withRef(deprecated) | Maintain an ref for wrapped component instance, use `refs.wrappedComponent` to access. | boolean | false | 145 | 146 | ### Note: use wrappedComponentRef instead of withRef after rc-form@1.4.0 147 | 148 | ```jsx 149 | class Form extends React.Component { ... } 150 | 151 | // deprecated 152 | const EnhancedForm = createForm({ withRef: true })(Form); 153 | 154 | this.refs.form.refs.wrappedComponent // => The instance of Form 155 | 156 | // Recommended 157 | const EnhancedForm = createForm()(Form); 158 | this.formRef = inst} /> 159 | this.formRef // => The instance of Form 160 | ``` 161 | 162 | ## (WrappedComponent: React.Component) => React.Component 163 | 164 | The returned function of createForm(). It will pass an object as prop `form` with the following members to WrappedComponent: 165 | 166 | ### getFieldProps(name, option): Object { [valuePropName], [trigger], [validateTrigger] } 167 | 168 | Will create props which can be set on a input/InputComponent which support value and onChange interface. 169 | 170 | After set, this will create a binding with this input. 171 | 172 | ```jsx 173 |
174 | 175 |
176 | ``` 177 | 178 | #### name: String 179 | 180 | This input's unique name. 181 | 182 | #### option: Object 183 | 184 | | Option | Description | Type | Default | 185 | |-----------|------------------------------------------|------------|---------| 186 | | option.valuePropName | Prop name of component's value field, eg: checkbox should be set to `checked` ... | String | 'value' | 187 | | option.getValueProps | Get the component props according to field value. | (value): Object | (value) => ({ value }) | 188 | | option.getValueFromEvent | Specify how to get value from event. | (e): any | See below | 189 | | option.initialValue | Initial value of current component. | any | - | 190 | | option.normalize | Return normalized value. | (value, prev, all): Object | - | 191 | | option.trigger | Event which is listened to collect form data. | String | 'onChange' | 192 | | option.validateTrigger | Event which is listened to validate. Set to falsy to only validate when call props.validateFields. | String|String[] | 'onChange' | 193 | | option.rules | Validator rules. see: [async-validator](https://github.com/yiminghe/async-validator) | Object[] | - | 194 | | option.validateFirst | Whether stop validate on first rule of error for this field. | boolean | false | 195 | | option.validate | | Object[] | - | 196 | | option.validate[n].trigger | Event which is listened to validate. Set to falsy to only validate when call props.validateFields. | String|String[] | 'onChange' | 197 | | option.validate[n].rules | Validator rules. see: [async-validator](https://github.com/yiminghe/async-validator) | Object[] | - | 198 | | option.hidden | Ignore current field while validating or gettting fields | boolean | false | 199 | | option.preserve | Whether to preserve the value. That will remain the value when the field be unmounted and be mounted again | boolean | false | 200 | 201 | ##### Default value of `getValueFromEvent` 202 | 203 | ```js 204 | function defaultGetValueFromEvent(e) { 205 | if (!e || !e.target) { 206 | return e; 207 | } 208 | const { target } = e; 209 | return target.type === 'checkbox' ? target.checked : target.value; 210 | } 211 | ``` 212 | 213 | ##### Tips 214 | 215 | ```js 216 | { 217 | validateTrigger: 'onBlur', 218 | rules: [{required: true}], 219 | } 220 | // is the shorthand of 221 | { 222 | validate: [{ 223 | trigger: 'onBlur', 224 | rules: [{required: true}], 225 | }], 226 | } 227 | ``` 228 | 229 | ### getFieldDecorator(name:String, option: Object) => (React.Node) => React.Node 230 | 231 | Similar to `getFieldProps`, but add some helper warnings and you can write onXX directly inside React.Node props: 232 | 233 | ```jsx 234 |
235 | {getFieldDecorator('name', otherOptions)()} 236 |
237 | ``` 238 | 239 | ### getFieldsValue([fieldNames: String[]]) 240 | 241 | Get fields value by fieldNames. 242 | 243 | ### getFieldValue(fieldName: String) 244 | 245 | Get field value by fieldName. 246 | 247 | ### getFieldInstance(fieldName: String) 248 | 249 | Get field react public instance by fieldName. 250 | 251 | ### setFieldsValue(obj: Object) 252 | 253 | Set fields value by kv object. 254 | 255 | ### setFieldsInitialValue(obj: Object) 256 | 257 | Set fields initialValue by kv object. use for reset and initial display/value. 258 | 259 | ### setFields(obj: Object) 260 | 261 | Set fields by kv object. each field can contain errors and value member. 262 | 263 | ### validateFields([fieldNames: String[]], [options: Object], callback: (errors, values) => void) 264 | 265 | Validate and get fields value by fieldNames. 266 | 267 | options is the same as validate method of [async-validator](https://github.com/yiminghe/async-validator). 268 | And add `force`. 269 | 270 | #### options.force: Boolean 271 | 272 | Defaults to false. Whether to validate fields which have been validated(caused by validateTrigger). 273 | 274 | ### getFieldsError(names): Object{ [name]: String[] } 275 | 276 | Get inputs' validate errors. 277 | 278 | ### getFieldError(name): String[] 279 | 280 | Get input's validate errors. 281 | 282 | ### isFieldValidating(name: String): Bool 283 | 284 | Whether this input is validating. 285 | 286 | ### isFieldsValidating(names: String[]): Bool 287 | 288 | Whether one of the inputs is validating. 289 | 290 | ### isFieldTouched(name: String): Bool 291 | 292 | Whether this input's value had been changed by user. 293 | 294 | ### isFieldsTouched(names: String[]): Bool 295 | 296 | Whether one of the inputs' values had been changed by user. 297 | 298 | ### resetFields([names: String[]]) 299 | 300 | Reset specified inputs. Defaults to all. 301 | 302 | ### isSubmitting(): Bool (Deprecated) 303 | 304 | Whether the form is submitting. 305 | 306 | ### submit(callback: Function) (Deprecated) 307 | 308 | Cause isSubmitting to return true, after callback called, isSubmitting return false. 309 | 310 | 311 | ## rc-form/lib/createDOMForm(option): Function 312 | 313 | createDOMForm enhancement, support props.form.validateFieldsAndScroll 314 | 315 | ### validateFieldsAndScroll([fieldNames: String[]], [options: Object], callback: (errors, values) => void) 316 | 317 | props.form.validateFields enhancement, support scroll to the first invalid form field, `scroll` is the same as [dom-scroll-into-view's function parameter `config`](https://github.com/yiminghe/dom-scroll-into-view#function-parameter). 318 | 319 | #### options.container: HTMLElement 320 | 321 | Defaults to first scrollable container of form field(until document). 322 | 323 | 324 | ## Notes 325 | 326 | - Do not use stateless function component inside Form component: https://github.com/facebook/react/pull/6534 327 | 328 | - you can not set same prop name as the value of validateTrigger/trigger for getFieldProps 329 | 330 | ```js 331 | 333 | })}> 334 | ``` 335 | 336 | - you can not use ref prop for getFieldProps 337 | 338 | ```js 339 | 340 | 341 | this.props.form.getFieldInstance('ref') // use this to get ref 342 | ``` 343 | 344 | or 345 | 346 | ```js 347 | (only allow function) 349 | })} /> 350 | ``` 351 | 352 | ## Test Case 353 | 354 | ``` 355 | npm test 356 | npm run chrome-test 357 | ``` 358 | 359 | ## Coverage 360 | 361 | ``` 362 | npm run coverage 363 | ``` 364 | 365 | open coverage/ dir 366 | 367 | ## License 368 | 369 | rc-form is released under the MIT license. 370 | -------------------------------------------------------------------------------- /examples/across-router.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-component/form/f02b478c59c178ea9ee28cca3326ad1c4ed37f02/examples/across-router.html -------------------------------------------------------------------------------- /examples/across-router.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0 */ 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import ReactDOM from 'react-dom'; 6 | import { createForm, formShape } from 'rc-form'; 7 | import { Router, Route, hashHistory } from 'react-router'; 8 | import { regionStyle } from './styles'; 9 | 10 | class ChildForm extends React.Component { 11 | static propTypes = { 12 | initialValue: PropTypes.object, 13 | form: formShape, 14 | onDestroy: PropTypes.func, 15 | }; 16 | componentWillUnmount() { 17 | this.props.onDestroy(this.props.form.getFieldsValue()); 18 | } 19 | onClick = () => { 20 | window.history.back(); 21 | } 22 | render() { 23 | const { getFieldProps } = this.props.form; 24 | return (
34 |

child form

35 |
36 | name: 37 | 41 |
42 |
43 | 44 |
45 |
); 46 | } 47 | } 48 | 49 | ChildForm = createForm()(ChildForm); 50 | 51 | class Picker extends React.Component { 52 | static propTypes = { 53 | childForm: PropTypes.object, 54 | onChange: PropTypes.func, 55 | value: PropTypes.object, 56 | }; 57 | onClick = () => { 58 | window.location.hash = '/open'; 59 | } 60 | render() { 61 | const { value, childForm } = this.props; 62 | const valueEl = value ? 63 |
{value.name}
: 64 |
please select
; 65 | return (
66 | {valueEl} 67 | {childForm ? React.cloneElement(childForm, { 68 | onDestroy: this.props.onChange, 69 | initialValue: value || {}, 70 | }) : null} 71 |
); 72 | } 73 | } 74 | 75 | class ParentForm extends React.Component { 76 | static propTypes = { 77 | initialValue: PropTypes.object, 78 | form: formShape, 79 | children: PropTypes.any, 80 | }; 81 | onClick = () => { 82 | console.log(this.props.form.getFieldsValue()); 83 | } 84 | render() { 85 | const { getFieldProps } = this.props.form; 86 | return (
87 |

parent form

88 |
89 | family: 90 |
91 |
92 | 93 |
94 | 95 |
96 | 97 |
98 |
); 99 | } 100 | } 101 | 102 | ParentForm = createForm()(ParentForm); 103 | 104 | ReactDOM.render((
110 | 111 | 112 | 113 | 114 | 115 |
), document.getElementById('__react-content')); 116 | -------------------------------------------------------------------------------- /examples/async-init.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /examples/async-init.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0 */ 2 | 3 | import { createForm, formShape } from 'rc-form'; 4 | import React, { Component } from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import { regionStyle, errorStyle } from './styles'; 7 | 8 | class Email extends React.Component { 9 | static propTypes = { 10 | form: formShape, 11 | }; 12 | 13 | checkSpecial = (rule, value, callback) => { 14 | setTimeout(() => { 15 | if (value === 'yiminghe@gmail.com') { 16 | callback('can not be!'); 17 | } else { 18 | callback(); 19 | } 20 | }, 1000); 21 | } 22 | 23 | render() { 24 | const { getFieldProps, getFieldError, isFieldValidating } = this.props.form; 25 | const errors = getFieldError('email'); 26 | return (
27 |
email validate onBlur
28 |
29 |
45 |
46 | {errors ? errors.join(',') : null} 47 |
48 |
49 | {isFieldValidating('email') ? 'validating' : null} 50 |
51 |
); 52 | } 53 | } 54 | 55 | class Form extends Component { 56 | static propTypes = { 57 | form: formShape, 58 | }; 59 | 60 | state = { 61 | loading: true, 62 | }; 63 | 64 | componentDidMount() { 65 | setTimeout(() => { 66 | this.setState({ 67 | loading: false, 68 | }, () => { 69 | setTimeout(() => { 70 | this.props.form.setFieldsInitialValue({ 71 | email: 'xx@gmail.com', 72 | }); 73 | }, 1000); 74 | }); 75 | }, 1000); 76 | } 77 | 78 | onSubmit = (e) => { 79 | e.preventDefault(); 80 | this.props.form.submit((callback) => { 81 | setTimeout(() => { 82 | this.props.form.validateFields((error, values) => { 83 | if (!error) { 84 | console.log('ok', values); 85 | } else { 86 | console.log('error', error, values); 87 | } 88 | callback(); 89 | }); 90 | }, 1000); 91 | }); 92 | } 93 | 94 | reset = (e) => { 95 | e.preventDefault(); 96 | this.props.form.resetFields(); 97 | } 98 | 99 | render() { 100 | if (this.state.loading) { 101 | return loading; 102 | } 103 | const { form } = this.props; 104 | const disabled = form.isFieldsValidating() || form.isSubmitting(); 105 | return (
106 |

async init field

107 |
108 | 109 | 110 |
111 | 112 |  {disabled ? disabled : null}  113 | 114 |
115 | 116 |
); 117 | } 118 | } 119 | 120 | const NewForm = createForm()(Form); 121 | 122 | ReactDOM.render(, document.getElementById('__react-content')); 123 | -------------------------------------------------------------------------------- /examples/dynamic-fields.html: -------------------------------------------------------------------------------- 1 | placeholder 2 | -------------------------------------------------------------------------------- /examples/dynamic-fields.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0 */ 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { createForm, formShape } from 'rc-form'; 6 | 7 | class Form1 extends React.Component { 8 | static propTypes = { 9 | form: formShape, 10 | }; 11 | state = { 12 | useInput: true, 13 | }; 14 | onSubmit = (e) => { 15 | e.preventDefault(); 16 | this.props.form.validateFields((error, values) => { 17 | if (!error) { 18 | console.log('ok', values); 19 | } else { 20 | console.log('error', error, values); 21 | } 22 | }); 23 | }; 24 | changeUseInput = (e) => { 25 | this.setState({ 26 | useInput: e.target.checked, 27 | }); 28 | } 29 | render() { 30 | const { getFieldError, getFieldDecorator } = this.props.form; 31 | 32 | return ( 33 |
34 |

situation 1

35 | {this.state.useInput ? getFieldDecorator('name', { 36 | initialValue: '', 37 | rules: [{ 38 | required: true, 39 | message: 'What\'s your name 1?', 40 | }], 41 | })() : null} 42 | text content 43 | {this.state.useInput ? null : getFieldDecorator('name', { 44 | initialValue: '', 45 | rules: [{ 46 | required: true, 47 | message: 'What\'s your name 2?', 48 | }], 49 | })()} 50 |
51 | 55 | {(getFieldError('name') || []).join(', ')} 56 |
57 | 58 |
59 | ); 60 | } 61 | } 62 | 63 | class Form2 extends React.Component { 64 | static propTypes = { 65 | form: formShape, 66 | }; 67 | state = { 68 | useInput: true, 69 | }; 70 | componentWillMount() { 71 | const { getFieldDecorator } = this.props.form; 72 | this.nameDecorator = getFieldDecorator('name', { 73 | initialValue: '', 74 | rules: [{ 75 | required: true, 76 | message: 'What\'s your name?', 77 | }], 78 | }); 79 | } 80 | onSubmit = (e) => { 81 | e.preventDefault(); 82 | this.props.form.validateFields((error, values) => { 83 | if (!error) { 84 | console.log('ok', values); 85 | } else { 86 | console.log('error', error, values); 87 | } 88 | }); 89 | }; 90 | changeUseInput = (e) => { 91 | this.setState({ 92 | useInput: e.target.checked, 93 | }); 94 | } 95 | render() { 96 | const { getFieldError } = this.props.form; 97 | return ( 98 |
99 |

situation 2

100 | {this.state.useInput ? this.nameDecorator() : null} 101 | text content 102 | {this.state.useInput ? null : this.nameDecorator()} 103 |
104 | 108 | {(getFieldError('name') || []).join(', ')} 109 |
110 | 111 |
112 | ); 113 | } 114 | } 115 | 116 | class Form3 extends React.Component { 117 | static propTypes = { 118 | form: formShape, 119 | }; 120 | state = { 121 | useInput: false, 122 | }; 123 | onSubmit = (e) => { 124 | e.preventDefault(); 125 | this.props.form.validateFields((error, values) => { 126 | if (!error) { 127 | console.log('ok', values); 128 | } else { 129 | console.log('error', error, values); 130 | } 131 | }); 132 | }; 133 | changeUseInput = (e) => { 134 | this.setState({ 135 | useInput: e.target.checked, 136 | }); 137 | } 138 | render() { 139 | const { getFieldError, getFieldDecorator } = this.props.form; 140 | return ( 141 |
142 |

situation 3

143 | {getFieldDecorator('name', { 144 | initialValue: '', 145 | rules: [{ 146 | required: true, 147 | message: 'What\'s your name 1?', 148 | }], 149 | })()} 150 | {this.state.useInput ? null : getFieldDecorator('name2', { 151 | initialValue: '', 152 | rules: [{ 153 | required: true, 154 | message: 'What\'s your name 2?', 155 | }], 156 | })()} 157 |
158 | 162 | {(getFieldError('name') || []).join(', ')} 163 |
164 | 165 |
166 | ); 167 | } 168 | } 169 | 170 | const WrappedForm1 = createForm()(Form1); 171 | const WrappedForm2 = createForm()(Form2); 172 | const WrappedForm3 = createForm()(Form3); 173 | 174 | ReactDOM.render( 175 |
176 | 177 | 178 | 179 |
180 | , document.getElementById('__react-content')); 181 | -------------------------------------------------------------------------------- /examples/dynamic.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /examples/dynamic.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0, react/prefer-stateless-function:0 */ 2 | 3 | import { createForm, formShape } from 'rc-form'; 4 | import React, { Component } from 'react'; 5 | import PropTypes from 'prop-types'; 6 | import ReactDOM from 'react-dom'; 7 | import { regionStyle, errorStyle } from './styles'; 8 | 9 | function Email(props) { 10 | const { hidden, form } = props; 11 | const { getFieldProps, getFieldError, isFieldValidating } = form; 12 | const errors = getFieldError('email'); 13 | const style = { 14 | ...regionStyle, 15 | display: hidden ? 'none' : '', 16 | }; 17 | return (
18 |
email: 19 |
32 | 33 | {errors ?
{errors.join(',')}
: null} 34 | 35 | {isFieldValidating('email') ?
validating
: null} 36 |
); 37 | } 38 | 39 | Email.propTypes = { 40 | form: formShape, 41 | hidden: PropTypes.bool, 42 | }; 43 | 44 | class User extends Component { 45 | static propTypes = { 46 | form: formShape, 47 | }; 48 | 49 | render() { 50 | const { getFieldProps, getFieldError, isFieldValidating } = this.props.form; 51 | const errors = getFieldError('user'); 52 | return (
53 |
user: 54 | 63 |
64 | {errors ?
{errors.join(',')}
: null} 65 | 66 | {isFieldValidating('user') ?
validating
: null} 67 |
); 68 | } 69 | } 70 | 71 | class Form extends Component { 72 | static propTypes = { 73 | form: formShape, 74 | }; 75 | 76 | onSubmit = (e) => { 77 | e.preventDefault(); 78 | this.props.form.validateFields((error, values) => { 79 | if (!error) { 80 | console.log('ok', values); 81 | } else { 82 | console.log('error', error, values); 83 | } 84 | }); 85 | }; 86 | 87 | render() { 88 | const { form } = this.props; 89 | const { getFieldProps, getFieldValue } = form; 90 | return (
91 |

overview

92 |
93 |
94 |
95 | 104 |
105 |
106 | 107 | { getFieldValue('remove_user') ? null : } 108 | 109 |
110 |
111 | 119 |
120 |
121 | 122 |
); 129 | } 130 | } 131 | 132 | const NewForm = createForm()(Form); 133 | 134 | ReactDOM.render(, document.getElementById('__react-content')); 135 | -------------------------------------------------------------------------------- /examples/file-input.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-component/form/f02b478c59c178ea9ee28cca3326ad1c4ed37f02/examples/file-input.html -------------------------------------------------------------------------------- /examples/file-input.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0, react/prefer-stateless-function:0, 2 | no-undef:0, react/no-unescaped-entities:0 */ 3 | 4 | import { createForm, formShape } from 'rc-form'; 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import { regionStyle, errorStyle } from './styles'; 8 | 9 | function getFileValueProps(value) { 10 | if (value && value.target) { 11 | return { 12 | value: value.target.value, 13 | }; 14 | } 15 | return { 16 | value, 17 | }; 18 | } 19 | 20 | function getValueFromFileEvent({ target }) { 21 | return { 22 | target, 23 | }; 24 | } 25 | 26 | class Form extends React.Component { 27 | static propTypes = { 28 | form: formShape, 29 | }; 30 | onSubmit = (e) => { 31 | e.preventDefault(); 32 | this.props.form.validateFields((error, values) => { 33 | console.log(error, values); 34 | if (!error) { 35 | const data = new FormData(); 36 | data.append('file', values.attachment.target.files[0]); 37 | fetch('/post.htm', { 38 | method: 'post', 39 | body: data, 40 | }); 41 | } 42 | }); 43 | } 44 | checkSize = (rule, value, callback) => { 45 | if (value && value.target) { 46 | const files = value.target.files; 47 | if (files[0]) { 48 | callback(files[0].size > 1000000 ? 'file size must be less than 1M' : undefined); 49 | } else { 50 | callback(); 51 | } 52 | } else { 53 | callback(); 54 | } 55 | } 56 | render() { 57 | const { getFieldProps, getFieldError } = this.props.form; 58 | const errors = getFieldError('attachment'); 59 | return (
62 |
attachment:
63 |
64 | 70 |
71 |
72 | {(errors) ? errors.join(',') : null} 73 |
74 | 75 |
); 76 | } 77 | } 78 | 79 | Form = createForm()(Form); 80 | 81 | class App extends React.Component { 82 | render() { 83 | return (
84 |

input[type="file"]

85 |
86 |
); 87 | } 88 | } 89 | 90 | ReactDOM.render(, document.getElementById('__react-content')); 91 | -------------------------------------------------------------------------------- /examples/getFieldDecorator.html: -------------------------------------------------------------------------------- 1 | placeholder 2 | -------------------------------------------------------------------------------- /examples/getFieldDecorator.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0 */ 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { createForm, formShape } from 'rc-form'; 6 | 7 | class Form extends React.Component { 8 | static propTypes = { 9 | form: formShape, 10 | }; 11 | 12 | componentWillMount() { 13 | this.nameDecorator = this.props.form.getFieldDecorator('name', { 14 | initialValue: '', 15 | rules: [{ 16 | required: true, 17 | message: 'What\'s your name?', 18 | }], 19 | }); 20 | } 21 | 22 | onSubmit = (e) => { 23 | e.preventDefault(); 24 | this.props.form.validateFields((error, values) => { 25 | if (!error) { 26 | console.log('ok', values); 27 | } else { 28 | console.log('error', error, values); 29 | } 30 | }); 31 | }; 32 | 33 | onChange = (e) => { 34 | console.log(e.target.value); 35 | } 36 | 37 | render() { 38 | const { getFieldError } = this.props.form; 39 | 40 | return ( 41 | 42 | {this.nameDecorator( 43 | 46 | )} 47 |
48 | {(getFieldError('name') || []).join(', ')} 49 |
50 | 51 | 52 | ); 53 | } 54 | } 55 | 56 | const WrappedForm = createForm()(Form); 57 | ReactDOM.render(, document.getElementById('__react-content')); 58 | -------------------------------------------------------------------------------- /examples/input-array.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-component/form/f02b478c59c178ea9ee28cca3326ad1c4ed37f02/examples/input-array.html -------------------------------------------------------------------------------- /examples/input-array.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0, react/jsx-no-bind:0 */ 2 | 3 | import { createForm, formShape } from 'rc-form'; 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import { regionStyle } from './styles'; 7 | 8 | let uuid = 0; 9 | 10 | 11 | class Form extends React.Component { 12 | static propTypes = { 13 | form: formShape, 14 | }; 15 | remove = (k) => { 16 | const { form } = this.props; 17 | // can use data-binding to get 18 | let keys = form.getFieldValue('keys'); 19 | keys = keys.filter((key) => { 20 | return key !== k; 21 | }); 22 | // can use data-binding to set 23 | form.setFieldsValue({ 24 | keys, 25 | }); 26 | } 27 | add = () => { 28 | uuid++; 29 | const { form } = this.props; 30 | // can use data-binding to get 31 | let keys = form.getFieldValue('keys'); 32 | keys = keys.concat(uuid); 33 | // can use data-binding to set 34 | // important! notify form to detect changes 35 | form.setFieldsValue({ 36 | keys, 37 | }); 38 | } 39 | submit = (e) => { 40 | e.preventDefault(); 41 | console.log(this.props.form.getFieldsValue()); 42 | } 43 | render() { 44 | const { getFieldProps, getFieldValue } = this.props.form; 45 | getFieldProps('keys', { 46 | initialValue: [], 47 | }); 48 | const inputs = getFieldValue('keys').map((k) => { 49 | return (
50 | 51 | delete
); 54 | }); 55 | return (
56 | {inputs} 57 |
58 | 59 | 60 | 61 |
62 |
); 63 | } 64 | } 65 | 66 | Form = createForm()(Form); 67 | 68 | ReactDOM.render(
, document.getElementById('__react-content')); 69 | -------------------------------------------------------------------------------- /examples/modal.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-component/form/f02b478c59c178ea9ee28cca3326ad1c4ed37f02/examples/modal.html -------------------------------------------------------------------------------- /examples/modal.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0, react/no-string-refs:0 */ 2 | 3 | import createDOMForm from 'rc-form/src/createDOMForm'; 4 | import { formShape } from 'rc-form'; 5 | import React, { Component } from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import Modal from 'antd/lib/modal'; 8 | import 'antd/dist/antd.css'; 9 | import { regionStyle, errorStyle } from './styles'; 10 | 11 | class Form extends Component { 12 | static propTypes = { 13 | form: formShape, 14 | }; 15 | 16 | constructor() { 17 | super(); 18 | this.state = { 19 | visible: false, 20 | }; 21 | } 22 | 23 | onSubmit = (e) => { 24 | e.preventDefault(); 25 | this.props.form.validateFieldsAndScroll((error, values) => { 26 | if (!error) { 27 | console.log('ok', values); 28 | } else { 29 | console.log('error', error, values); 30 | } 31 | }); 32 | }; 33 | 34 | onCancel = () => { 35 | this.setState({ 36 | visible: false, 37 | }); 38 | }; 39 | 40 | open = () => { 41 | this.setState({ 42 | visible: true, 43 | }); 44 | }; 45 | 46 | render() { 47 | const { getFieldProps, getFieldError } = this.props.form; 48 | return (
49 |

modal

50 | 59 |
60 | 61 | 69 |
70 | {getFieldError('required') ? getFieldError('required').join(',') : 71 | 1} 72 |
73 |
74 | 75 |
76 | 77 |
78 |
79 |
80 | 81 |
82 |
); 83 | } 84 | } 85 | 86 | const NewForm = createDOMForm()(Form); 87 | 88 | ReactDOM.render(, document.getElementById('__react-content')); 89 | -------------------------------------------------------------------------------- /examples/nested-field.html: -------------------------------------------------------------------------------- 1 | placeholder 2 | -------------------------------------------------------------------------------- /examples/nested-field.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0, react/prefer-stateless-function:0 */ 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { formShape } from 'rc-form'; 6 | import createForm from '../src/createDOMForm'; 7 | 8 | class Form extends React.Component { 9 | static propTypes = { 10 | form: formShape, 11 | }; 12 | 13 | onSubmit = (e) => { 14 | e.preventDefault(); 15 | console.log('Values of member[0].name.firstname and a[0][1].b.c[0]'); 16 | console.log(this.props.form.getFieldsValue(['member[0].name.firstname', 'a[0][1].b.c[0]'])); 17 | console.log('Values of all fields'); 18 | console.log(this.props.form.getFieldsValue()); 19 | 20 | this.props.form.validateFieldsAndScroll((error, values) => { 21 | if (!error) { 22 | console.log('ok', values); 23 | } else { 24 | console.log('error', error, values); 25 | } 26 | }); 27 | } 28 | 29 | onChange = (e) => { 30 | console.log(e.target.value); 31 | } 32 | 33 | setField = () => { 34 | this.props.form.setFieldsValue({ 35 | member: [ 36 | { 37 | name: { 38 | firstname: 'm1 first', 39 | lastname: 'm1 last', 40 | }, 41 | }, 42 | { 43 | name: { 44 | firstname: 'm2 first', 45 | lastname: 'm2 last', 46 | }, 47 | }, 48 | ], 49 | a: [ 50 | [undefined, { 51 | b: { 52 | c: ['Value of a[0][1].b.c[0]'], 53 | }, 54 | }], 55 | ], 56 | w: { 57 | x: { 58 | y: { 59 | z: ['Value of w.x.y.z[0]'], 60 | }, 61 | }, 62 | }, 63 | }); 64 | } 65 | 66 | resetFields = () => { 67 | console.log('reset'); 68 | this.props.form.resetFields(); 69 | } 70 | 71 | render() { 72 | const { getFieldDecorator, getFieldError } = this.props.form; 73 | 74 | return ( 75 |
76 |
Member 0 firstname
77 | {getFieldDecorator('member[0].name.firstname', { 78 | initialValue: '', 79 | rules: [{ 80 | required: true, 81 | message: 'What\'s the member_0 firstname?', 82 | }], 83 | })( 84 | 87 | )} 88 |
89 | {(getFieldError('member[0].name.firstname') || []).join(', ')} 90 |
91 | 92 |
Member 0 lastname
93 | {getFieldDecorator('member[0].name.lastname', { 94 | initialValue: '', 95 | rules: [{ 96 | required: true, 97 | message: 'What\'s the member_0 lastname?', 98 | }], 99 | })( 100 | 103 | )} 104 |
105 | {(getFieldError('member[0].name.firstname') || []).join(', ')} 106 |
107 | 108 |
Member 1 firstname
109 | {getFieldDecorator('member[1].name.firstname', { 110 | initialValue: '', 111 | rules: [{ 112 | required: true, 113 | message: 'What\'s the member_1 fistname?', 114 | }], 115 | })( 116 | 119 | )} 120 |
121 | {(getFieldError('member[1].name.firstname') || []).join(', ')} 122 |
123 | 124 |
Member 1 lastname
125 | {getFieldDecorator('member[1].name.lastname', { 126 | initialValue: '', 127 | rules: [{ 128 | required: true, 129 | message: 'What\'s the member_1 lastname?', 130 | }], 131 | })( 132 | 135 | )} 136 |
137 | {(getFieldError('member[1].name.firstname') || []).join(', ')} 138 |
139 | 140 |
a[0][1].b.c[0]
141 | {getFieldDecorator('a[0][1].b.c[0]', { 142 | initialValue: '', 143 | rules: [{ 144 | required: true, 145 | message: 'What\'s a[0][1].b.c[0]?', 146 | }], 147 | })( 148 | 151 | )} 152 |
153 | {(getFieldError('a[0][1].b.c[0]') || []).join(', ')} 154 |
155 | 156 |
w.x.y.z[0]
157 | {getFieldDecorator('w.x.y.z[0]', { 158 | initialValue: '', 159 | rules: [{ 160 | required: true, 161 | message: 'What\'s w.x.y.z[0]?', 162 | }], 163 | })( 164 | 167 | )} 168 |
169 | {(getFieldError('w.x.y.z[0]') || []).join(', ')} 170 |
171 | 172 | 173 | 174 | 175 |
176 | ); 177 | } 178 | } 179 | 180 | Form = createForm({ 181 | onFieldsChange(_, changedFields, allFields) { 182 | console.log('onFieldsChange: ', changedFields, allFields); 183 | }, 184 | onValuesChange(_, changedValues, allValues) { 185 | console.log('onValuesChange: ', changedValues, allValues); 186 | }, 187 | })(Form); 188 | 189 | class App extends React.Component { 190 | render() { 191 | return (
192 |

setFieldsValue

193 |
194 |
); 195 | } 196 | } 197 | 198 | ReactDOM.render(, document.getElementById('__react-content')); 199 | -------------------------------------------------------------------------------- /examples/normalize.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-component/form/f02b478c59c178ea9ee28cca3326ad1c4ed37f02/examples/normalize.html -------------------------------------------------------------------------------- /examples/normalize.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0 */ 2 | 3 | import { createForm, formShape } from 'rc-form'; 4 | import React, { Component } from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import 'antd/dist/antd.css'; 7 | import { regionStyle, errorStyle } from './styles'; 8 | 9 | class CustomInput extends React.Component { 10 | static propTypes = { 11 | form: formShape, 12 | }; 13 | state = {}; 14 | checkUpper = (rule, value, callback) => { 15 | if (value !== value.toUpperCase()) { 16 | callback(new Error('need to be upper!')); 17 | } else { 18 | callback(); 19 | } 20 | } 21 | toUpper = (v, prev) => { 22 | if (v === prev) { 23 | return v; 24 | } 25 | return v.toUpperCase(); 26 | } 27 | render() { 28 | const { getFieldProps, getFieldError } = this.props.form; 29 | const errors = getFieldError('upper'); 30 | return (
31 |
upper normalize
32 |
33 | 40 |
41 |
42 | {(errors) ? errors.join(',') : null} 43 |
44 |
); 45 | } 46 | } 47 | 48 | class MaxMin extends React.Component { 49 | static propTypes = { 50 | form: formShape, 51 | }; 52 | normalizeMin = (value, prevValue, allValues) => { 53 | console.log('normalizeMin', allValues.min, allValues.max); 54 | const previousAllValues = this.props.form.getFieldsValue(); 55 | if (allValues.max !== previousAllValues.max) { 56 | // max changed 57 | if (value === '' || Number(allValues.max) < Number(value)) { 58 | return allValues.max; 59 | } 60 | } 61 | return value; 62 | } 63 | normalizeMax = (value, prevValue, allValues) => { 64 | console.log('normalizeMax', allValues.min, allValues.max); 65 | const previousAllValues = this.props.form.getFieldsValue(); 66 | if (allValues.min !== previousAllValues.min) { 67 | // min changed 68 | if (value === '' || Number(allValues.min) > Number(value)) { 69 | return allValues.min; 70 | } 71 | } 72 | return value; 73 | } 74 | render() { 75 | const { getFieldProps } = this.props.form; 76 | return (
77 |
min: 90 |
91 |
max: 104 |
105 |
); 106 | } 107 | } 108 | 109 | class Form extends Component { 110 | static propTypes = { 111 | form: formShape, 112 | } 113 | 114 | onSubmit = (e) => { 115 | e.preventDefault(); 116 | this.props.form.validateFields((error, values) => { 117 | if (!error) { 118 | console.log('ok', values); 119 | } else { 120 | console.log('error', error, values); 121 | } 122 | }); 123 | } 124 | 125 | render() { 126 | const { form } = this.props; 127 | return (
128 |

normalize

129 | 130 | 131 | 132 | 133 | 134 |
135 | 136 |
137 | 138 |
); 139 | } 140 | } 141 | 142 | const NewForm = createForm()(Form); 143 | 144 | ReactDOM.render(, document.getElementById('__react-content')); 145 | -------------------------------------------------------------------------------- /examples/overview.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /examples/overview.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0 */ 2 | 3 | import createDOMForm from 'rc-form/src/createDOMForm'; 4 | import { formShape } from 'rc-form'; 5 | import React, { Component } from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import Select, { Option } from 'antd/lib/select'; 8 | import DatePicker from 'antd/lib/date-picker'; 9 | import 'antd/dist/antd.css'; 10 | import { regionStyle, errorStyle } from './styles'; 11 | 12 | function Email(props) { 13 | const { getFieldProps, getFieldError, isFieldValidating } = props.form; 14 | const errors = getFieldError('email'); 15 | return (
16 |
email sync validate
17 |
18 | 错误的 email 格式, 24 | }, 25 | ], 26 | })} 27 | />
28 |
29 | {errors} 30 |
31 |
32 | {isFieldValidating('email') ? 'validating' : null} 33 |
34 |
); 35 | } 36 | 37 | Email.propTypes = { 38 | form: formShape, 39 | }; 40 | 41 | class User extends React.Component { 42 | static propTypes = { 43 | form: formShape, 44 | }; 45 | userExists = (rule, value, callback) => { 46 | setTimeout(() => { 47 | if (value === '1') { 48 | callback([new Error('are you kidding?')]); 49 | } else if (value === 'yiminghe') { 50 | callback([new Error('forbid yiminghe')]); 51 | } else { 52 | callback(); 53 | } 54 | }, 300); 55 | } 56 | render() { 57 | const { getFieldProps, getFieldError, isFieldValidating } = this.props.form; 58 | const errors = getFieldError('user'); 59 | return (
60 |
* user async validate
61 |
62 |
75 |
76 | {(errors) ? errors.join(',') : null} 77 |
78 |
79 | {isFieldValidating('user') ? 'validating' : null} 80 |
81 |
); 82 | } 83 | } 84 | 85 | function CustomInput(props) { 86 | const { getFieldProps, getFieldError, isFieldValidating } = props.form; 87 | const errors = getFieldError('select'); 88 | return (
89 |
* custom select sync validate
90 |
104 |
105 | {(errors) ? errors.join(',') : null} 106 |
107 |
108 | {isFieldValidating('select') ? 'validating' : null} 109 |
110 |
); 111 | } 112 | 113 | CustomInput.propTypes = { 114 | form: formShape, 115 | }; 116 | 117 | function DateInput(props) { 118 | const { getFieldProps, getFieldError } = props.form; 119 | const errors = getFieldError('date'); 120 | return (
121 |
* DateInput sync validate
122 |
123 | 134 |
135 |
136 | {(errors) ? errors.join(',') : null} 137 |
138 |
); 139 | } 140 | 141 | DateInput.propTypes = { 142 | form: formShape, 143 | }; 144 | 145 | function toNumber(v) { 146 | if (v === undefined) { 147 | return v; 148 | } 149 | if (v === '') { 150 | return undefined; 151 | } 152 | if (v && v.trim() === '') { 153 | return NaN; 154 | } 155 | return Number(v); 156 | } 157 | 158 | function NumberInput(props) { 159 | const { getFieldProps, getFieldError } = props.form; 160 | const errors = getFieldError('number'); 161 | return (
162 |
number input
163 |
164 | 173 |
174 |
175 | {(errors) ? errors.join(',') : null} 176 |
177 |
); 178 | } 179 | 180 | NumberInput.propTypes = { 181 | form: formShape, 182 | }; 183 | 184 | class Form extends Component { 185 | static propTypes = { 186 | form: formShape, 187 | }; 188 | 189 | onSubmit = (e) => { 190 | console.log('submit'); 191 | e.preventDefault(); 192 | this.props.form.validateFieldsAndScroll({ scroll: { offsetTop: 20 } }, (error, values) => { 193 | if (!error) { 194 | console.log('ok', values); 195 | } else { 196 | console.log('error', error, values); 197 | } 198 | }); 199 | }; 200 | 201 | reset = (e) => { 202 | e.preventDefault(); 203 | this.props.form.resetFields(); 204 | }; 205 | 206 | render() { 207 | const { form } = this.props; 208 | const { getFieldProps, getFieldError } = form; 209 | return (
210 |

overview

211 |
212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 |
223 |
normal required input
224 |
225 | 233 |
234 |
235 | {(getFieldError('normal')) ? getFieldError('normal').join(',') : null} 236 |
237 |
238 | 239 |
240 | 241 |   242 | 243 |
244 | 245 |
); 246 | } 247 | } 248 | 249 | const NewForm = createDOMForm({ 250 | validateMessages: { 251 | required(field) { 252 | return `${field} 必填`; 253 | }, 254 | }, 255 | })(Form); 256 | 257 | ReactDOM.render(, document.getElementById('__react-content')); 258 | -------------------------------------------------------------------------------- /examples/parallel-form.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /examples/parallel-form.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0, react/prefer-stateless-function:0 */ 2 | 3 | import { createForm, formShape } from 'rc-form'; 4 | import React from 'react'; 5 | import PropTypes from 'prop-types'; 6 | import ReactDOM from 'react-dom'; 7 | import Switch from 'antd/lib/switch'; 8 | import 'antd/dist/antd.css'; 9 | import { regionStyle } from './styles'; 10 | 11 | class TopForm extends React.Component { 12 | static propTypes = { 13 | form: formShape, 14 | }; 15 | render() { 16 | const { getFieldProps } = this.props.form; 17 | return (
18 |
has email?
19 |
20 | 25 |
26 |
); 27 | } 28 | } 29 | 30 | class BottomForm extends React.Component { 31 | static propTypes = { 32 | form: formShape, 33 | on: PropTypes.bool, 34 | }; 35 | render() { 36 | const { form } = this.props; 37 | const on = form.getFieldValue('on'); 38 | const style = { 39 | ...regionStyle, 40 | display: on ? 'block' : 'none', 41 | }; 42 | return (
43 |
email:
44 |
45 | 52 |
53 |
); 54 | } 55 | } 56 | 57 | class Form extends React.Component { 58 | static propTypes = { 59 | form: formShape, 60 | }; 61 | onSubmit = (e) => { 62 | e.preventDefault(); 63 | console.log(this.props.form.getFieldsValue()); 64 | } 65 | render() { 66 | const { form } = this.props; 67 | return (
68 | 69 | 70 |
71 | 72 |
73 |
); 74 | } 75 | } 76 | 77 | Form = createForm()(Form); 78 | 79 | class App extends React.Component { 80 | render() { 81 | return (
82 |

parallel form

83 |
84 |
); 85 | } 86 | } 87 | 88 | ReactDOM.render(, document.getElementById('__react-content')); 89 | -------------------------------------------------------------------------------- /examples/promise-validate.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-component/form/f02b478c59c178ea9ee28cca3326ad1c4ed37f02/examples/promise-validate.html -------------------------------------------------------------------------------- /examples/promise-validate.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | 3 | import { createForm, formShape } from 'rc-form'; 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | 7 | class Form extends React.Component { 8 | static propTypes = { 9 | form: formShape, 10 | }; 11 | 12 | handleSubmit = (e) => { 13 | e.preventDefault(); 14 | const { validateFields } = this.props.form; 15 | validateFields() 16 | .then(console.log) 17 | .catch(console.error); 18 | } 19 | 20 | render() { 21 | const { getFieldDecorator } = this.props.form; 22 | return ( 23 | 24 | {getFieldDecorator('name', { 25 | rules: [{ 26 | required: true, 27 | }], 28 | })()} 29 | 30 | 31 | ); 32 | } 33 | } 34 | 35 | const NewForm = createForm()(Form); 36 | 37 | ReactDOM.render(, document.getElementById('__react-content')); 38 | -------------------------------------------------------------------------------- /examples/react-native/App.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0, react/no-multi-comp:0 */ 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { 6 | StyleSheet, 7 | Button, 8 | Dimensions, 9 | TextInput, 10 | Text, 11 | View, 12 | Alert, 13 | } from 'react-native'; 14 | 15 | import { createForm } from 'rc-form'; 16 | 17 | const { width } = Dimensions.get('window'); 18 | const styles = StyleSheet.create({ 19 | container: { 20 | flex: 1, 21 | backgroundColor: '#fff', 22 | alignItems: 'center', 23 | padding: 50, 24 | justifyContent: 'center', 25 | }, 26 | inputView: { 27 | width: width - 100, 28 | paddingLeft: 10, 29 | }, 30 | input: { 31 | height: 42, 32 | fontSize: 16, 33 | }, 34 | errorinfo: { 35 | marginTop: 10, 36 | }, 37 | errorinfoText: { 38 | color: 'red', 39 | }, 40 | }); 41 | 42 | class FromItem extends React.PureComponent { 43 | static propTypes = { 44 | label: PropTypes.string, 45 | onChange: PropTypes.func, 46 | value: PropTypes.string, 47 | error: PropTypes.array, 48 | }; 49 | getError = error => { 50 | if (error) { 51 | return error.map(info => { 52 | return ( 53 | 54 | {info} 55 | 56 | ); 57 | }); 58 | } 59 | return null; 60 | }; 61 | render() { 62 | const { label, onChange, value, error } = this.props; 63 | return ( 64 | 65 | 74 | {this.getError(error)} 75 | 76 | ); 77 | } 78 | } 79 | 80 | class App extends React.Component { 81 | static propTypes = { 82 | form: PropTypes.object.isRequired, 83 | }; 84 | 85 | checkUserNameOne = (value, callback) => { 86 | setTimeout(() => { 87 | if (value === '15188888888') { 88 | callback('手机号已经被注册'); 89 | } else { 90 | callback(); 91 | } 92 | }, 2000); 93 | }; 94 | submit = () => { 95 | this.props.form.validateFields((error) => { 96 | if (error) return; 97 | Alert('通过了所有验证'); // eslint-disable-line new-cap 98 | }); 99 | }; 100 | render() { 101 | const { getFieldDecorator, getFieldError } = this.props.form; 102 | return ( 103 | 104 | 简单的手机号验证 105 | {getFieldDecorator('username', { 106 | validateFirst: true, 107 | rules: [ 108 | { required: true, message: '请输入手机号!' }, 109 | { 110 | pattern: /^1\d{10}$/, 111 | message: '请输入正确的手机号!', 112 | }, 113 | { 114 | validator: (rule, value, callback) => { 115 | this.checkUserNameOne(value, callback); 116 | }, 117 | message: '手机号已经被注册!', 118 | }, 119 | ], 120 | })( 121 | 126 | )} 127 | 73 | ); 74 | } 75 | } 76 | 77 | Out = connect((state) => { 78 | return { 79 | email: state.form.email, 80 | }; 81 | })(Out); 82 | 83 | const store = createStore(combineReducers({ 84 | form, 85 | })); 86 | 87 | const NewForm = connect((state) => { 88 | return { 89 | formState: { 90 | email: state.form.email, 91 | }, 92 | }; 93 | })(createForm({ 94 | mapPropsToFields(props) { 95 | console.log('mapPropsToFields', props); 96 | return { 97 | email: createFormField(props.formState.email), 98 | }; 99 | }, 100 | onFieldsChange(props, fields) { 101 | console.log('onFieldsChange', fields); 102 | props.dispatch({ 103 | type: 'save_fields', 104 | payload: fields, 105 | }); 106 | }, 107 | })(Form)); 108 | 109 | class App extends React.Component { 110 | render() { 111 | return ( 112 |
113 |

integrate with redux

114 | 115 | 116 |
117 |
); 118 | } 119 | } 120 | 121 | ReactDOM.render(, document.getElementById('__react-content')); 122 | -------------------------------------------------------------------------------- /examples/server-validate.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /examples/server-validate.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0, react/prefer-stateless-function:0 */ 2 | 3 | import { createForm, formShape } from 'rc-form'; 4 | import React, { Component } from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import { regionStyle, errorStyle } from './styles'; 7 | 8 | function Email(props) { 9 | const { getFieldProps, getFieldError } = props.form; 10 | const errors = getFieldError('email'); 11 | return (
12 |
email sync validate
13 |
14 |
26 |
27 | {errors ? errors.join(',') : null} 28 |
29 |
); 30 | } 31 | 32 | Email.propTypes = { 33 | form: formShape, 34 | }; 35 | 36 | class User extends React.Component { 37 | static propTypes = { 38 | form: formShape, 39 | }; 40 | render() { 41 | const { getFieldProps, getFieldError } = this.props.form; 42 | const errors = getFieldError('user'); 43 | return (
44 |
user async validate
45 |
46 | 52 |
53 |
54 | {(errors) ? errors.join(',') : null} 55 |
56 |
); 57 | } 58 | } 59 | 60 | class Form extends Component { 61 | static propTypes = { 62 | form: formShape, 63 | }; 64 | 65 | onSubmit = (e) => { 66 | e.preventDefault(); 67 | this.props.form.validateFields((error, values) => { 68 | if (!error) { 69 | console.log('ok', values); 70 | setTimeout(() => { 71 | // server validate 72 | if (values.user === 'yiminghe') { 73 | this.props.form.setFields({ 74 | user: { 75 | value: values.user, 76 | errors: [new Error('forbid ha')], 77 | }, 78 | }); 79 | } 80 | }, 500); 81 | } else { 82 | console.log('error', error, values); 83 | } 84 | }); 85 | }; 86 | 87 | render() { 88 | const { form } = this.props; 89 | return (
90 |

setFields

91 |
92 | 93 | 94 | 95 | 96 |
97 | 98 |
99 | 100 |
); 101 | } 102 | } 103 | 104 | const NewForm = createForm()(Form); 105 | 106 | ReactDOM.render(, document.getElementById('__react-content')); 107 | -------------------------------------------------------------------------------- /examples/setFieldsValue.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /examples/setFieldsValue.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0, react/prefer-stateless-function:0 */ 2 | 3 | import { createForm, formShape } from 'rc-form'; 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import { regionStyle, errorStyle } from './styles'; 7 | 8 | class Form extends React.Component { 9 | static propTypes = { 10 | form: formShape, 11 | }; 12 | 13 | componentDidUpdate() { 14 | console.log('didUpdate'); 15 | } 16 | 17 | setEmail = () => { 18 | this.props.form.setFieldsValue({ 19 | email: 'yiminghe@gmail.com', 20 | }, () => console.log('after')); 21 | console.log('before'); 22 | } 23 | 24 | render() { 25 | const { getFieldProps, getFieldError } = this.props.form; 26 | const errors = getFieldError('email'); 27 | return (
28 |
email:
29 |
30 | 36 |
37 |
38 | {(errors) ? errors.join(',') : null} 39 |
40 | 41 | 42 |
); 43 | } 44 | } 45 | 46 | Form = createForm()(Form); 47 | 48 | class App extends React.Component { 49 | render() { 50 | return (
51 |

setFieldsValue

52 |
53 |
); 54 | } 55 | } 56 | 57 | ReactDOM.render(, document.getElementById('__react-content')); 58 | -------------------------------------------------------------------------------- /examples/start-end-date.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /examples/start-end-date.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | 3 | import React, { Component } from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import DatePicker from 'antd/lib/date-picker'; 6 | import { formShape } from 'rc-form'; 7 | import 'antd/dist/antd.css'; 8 | import createDOMForm from 'rc-form/src/createDOMForm'; 9 | import { regionStyle, errorStyle } from './styles'; 10 | 11 | class Form extends Component { 12 | static propTypes = { 13 | form: formShape, 14 | }; 15 | 16 | onSubmit = (e) => { 17 | console.log('submit'); 18 | e.preventDefault(); 19 | this.props.form.validateFieldsAndScroll((error, values) => { 20 | if (!error) { 21 | console.log('ok', values); 22 | } else { 23 | console.log('error', error, values); 24 | } 25 | }); 26 | }; 27 | 28 | reset = (e) => { 29 | e.preventDefault(); 30 | this.props.form.resetFields(); 31 | }; 32 | 33 | checkStart = (rule, value, callback) => { 34 | const { validateFields } = this.props.form; 35 | validateFields(['end'], { 36 | force: true, 37 | }); 38 | callback(); 39 | }; 40 | 41 | checkEnd = (rule, value, callback) => { 42 | const end = value; 43 | const { getFieldValue } = this.props.form; 44 | const start = getFieldValue('start'); 45 | if (!end || !start) { 46 | callback('please select both start and end time'); 47 | } else if (end.valueOf() < start.valueOf()) { 48 | callback('start time should be less than end time'); 49 | } else { 50 | callback(); 51 | } 52 | }; 53 | 54 | render() { 55 | const { form } = this.props; 56 | const { getFieldProps, getFieldError } = form; 57 | return (
58 |

startTime and endTime validation

59 | 60 |
61 |
start:
62 |
63 | 67 |
68 |
69 | 70 |
71 |
end:
72 |
73 | 77 |
78 |
79 | 80 |
81 | {getFieldError('end') ? getFieldError('end').join(',') : ''} 82 |
83 | 84 |
85 | 86 |   87 | 88 |
89 | 90 |
); 91 | } 92 | } 93 | 94 | const NewForm = createDOMForm()(Form); 95 | 96 | ReactDOM.render(, document.getElementById('__react-content')); 97 | -------------------------------------------------------------------------------- /examples/styles.js: -------------------------------------------------------------------------------- 1 | export const regionStyle = { 2 | border: '1px solid red', 3 | marginTop: 10, 4 | padding: 10, 5 | }; 6 | 7 | export const errorStyle = { 8 | color: 'red', 9 | marginTop: 10, 10 | padding: 10, 11 | }; 12 | -------------------------------------------------------------------------------- /examples/suggest.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-component/form/f02b478c59c178ea9ee28cca3326ad1c4ed37f02/examples/suggest.html -------------------------------------------------------------------------------- /examples/suggest.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0 */ 2 | 3 | import { createForm, formShape } from 'rc-form'; 4 | import React, { Component } from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import Select, { Option } from 'antd/lib/select'; 7 | 8 | import 'antd/dist/antd.css'; 9 | import { regionStyle, errorStyle } from './styles'; 10 | 11 | const emailTpl = ['@gmail.com', '@outlook.com', '@qq.com']; 12 | 13 | class CustomInput extends React.Component { 14 | static propTypes = { 15 | form: formShape, 16 | }; 17 | state = { 18 | data: [], 19 | }; 20 | onChange = (v) => { 21 | if (v.indexOf('@') === -1) { 22 | this.setState({ 23 | data: emailTpl.map(m => v + m), 24 | }); 25 | } else if (this.state.data.length) { 26 | this.setState({ 27 | data: [], 28 | }); 29 | } 30 | } 31 | render() { 32 | const { getFieldProps, getFieldError, isFieldValidating } = this.props.form; 33 | const errors = getFieldError('select'); 34 | return (
35 |
custom select sync validate
36 |
37 | 60 |
61 |
62 | {(errors) ? errors.join(',') : 63 | 68 | 1 69 | } 70 |
71 |
72 | {isFieldValidating('select') ? 'validating' : 77 | 1 78 | } 79 |
80 |
); 81 | } 82 | } 83 | 84 | class Form extends Component { 85 | static propTypes = { 86 | form: formShape, 87 | }; 88 | 89 | onSubmit = (e) => { 90 | e.preventDefault(); 91 | this.props.form.validateFields((error, values) => { 92 | if (!error) { 93 | console.log('ok', values); 94 | } else { 95 | console.log('error', error, values); 96 | } 97 | }); 98 | }; 99 | 100 | render() { 101 | const { form } = this.props; 102 | return (
103 |

suggest

104 |
105 | 106 | 107 |
108 | 109 |
110 | 111 |
); 112 | } 113 | } 114 | 115 | const NewForm = createForm()(Form); 116 | 117 | ReactDOM.render(, document.getElementById('__react-content')); 118 | -------------------------------------------------------------------------------- /examples/validateFirst.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /examples/validateFirst.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0 */ 2 | 3 | import { createForm, formShape } from 'rc-form'; 4 | import React, { Component } from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import 'antd/dist/antd.css'; 7 | import { regionStyle, errorStyle } from './styles'; 8 | 9 | function Email(props) { 10 | const { getFieldProps, getFieldError, isFieldValidating } = props.form; 11 | const errors = getFieldError('email'); 12 | return (
13 |
email sync validate
14 |
15 | 29 |
30 |
31 | {errors ? errors.join(',') : null} 32 |
33 |
34 | {isFieldValidating('email') ? 'validating' : null} 35 |
36 |
); 37 | } 38 | 39 | Email.propTypes = { 40 | form: formShape, 41 | }; 42 | 43 | class User extends React.Component { 44 | static propTypes = { 45 | form: formShape, 46 | }; 47 | userExists = (rule, value, callback) => { 48 | setTimeout(() => { 49 | if (value === '1') { 50 | callback([new Error('are you kidding?')]); 51 | } else if (value === 'yiminghe') { 52 | callback([new Error('forbid yiminghe')]); 53 | } else { 54 | callback(); 55 | } 56 | }, 300); 57 | } 58 | render() { 59 | const { getFieldProps, getFieldError, isFieldValidating } = this.props.form; 60 | const errors = getFieldError('user'); 61 | return (
62 |
user async validate
63 |
64 | 77 |
78 |
79 | {(errors) ? errors.join(',') : null} 80 |
81 |
82 | {isFieldValidating('user') ? 'validating' : null} 83 |
84 |
); 85 | } 86 | } 87 | 88 | class Form extends Component { 89 | static propTypes = { 90 | form: formShape, 91 | }; 92 | 93 | onSubmit = (e) => { 94 | console.log('submit'); 95 | e.preventDefault(); 96 | this.props.form.validateFields({ 97 | // firstFields: false, 98 | }, (error, values) => { 99 | if (!error) { 100 | console.log('ok', values); 101 | } else { 102 | console.log('error', error, values); 103 | } 104 | }); 105 | }; 106 | 107 | reset = (e) => { 108 | e.preventDefault(); 109 | this.props.form.resetFields(); 110 | }; 111 | 112 | render() { 113 | const { form } = this.props; 114 | return (
115 |

validateFirst

116 |
117 | 118 | 119 | 120 | 121 |
122 | 123 |   124 | 125 |
126 | 127 |
); 128 | } 129 | } 130 | 131 | const NewForm = createForm()(Form); 132 | 133 | ReactDOM.render(, document.getElementById('__react-content')); 134 | -------------------------------------------------------------------------------- /examples/validateTrigger.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /examples/validateTrigger.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0, no-console:0, react/prefer-stateless-function: 0 */ 2 | 3 | import { createForm, formShape } from 'rc-form'; 4 | import React, { Component } from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import { regionStyle, errorStyle } from './styles'; 7 | 8 | function Email(props) { 9 | const { getFieldProps, getFieldError, isFieldValidating } = props.form; 10 | const errors = getFieldError('email'); 11 | return (
12 |
email validate onBlur && onChange
13 |
14 | 29 |
30 |
31 | {errors ? errors.join(',') : null} 32 |
33 |
34 | {isFieldValidating('email') ? 'validating' : null} 35 |
36 |
); 37 | } 38 | 39 | Email.propTypes = { 40 | form: formShape, 41 | }; 42 | 43 | class User extends React.Component { 44 | static propTypes = { 45 | form: formShape, 46 | }; 47 | render() { 48 | const { getFieldProps, getFieldError, isFieldValidating } = this.props.form; 49 | const errors = getFieldError('user'); 50 | return (
51 |
user validate on submit
52 |
53 | 66 |
67 |
68 | {(errors) ? errors.join(',') : null} 69 |
70 |
71 | {isFieldValidating('user') ? 'validating' : null} 72 |
73 |
); 74 | } 75 | } 76 | 77 | class Form extends Component { 78 | static propTypes = { 79 | form: formShape, 80 | }; 81 | 82 | onSubmit = (e) => { 83 | e.preventDefault(); 84 | this.props.form.validateFields((error, values) => { 85 | if (!error) { 86 | console.log('ok', values); 87 | } else { 88 | console.log('error', error, values); 89 | } 90 | }); 91 | }; 92 | 93 | render() { 94 | const { form } = this.props; 95 | return (
96 |

use validateTrigger config

97 |
98 | 99 | 100 | 101 | 102 |
103 | 104 |
105 | 106 |
); 107 | } 108 | } 109 | 110 | const NewForm = createForm()(Form); 111 | 112 | ReactDOM.render(, document.getElementById('__react-content')); 113 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // export this package's api 2 | import { createForm, createFormField } from './src/'; 3 | export { createForm, createFormField }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rc-form", 3 | "version": "2.4.12", 4 | "description": "React High Order Form Component", 5 | "keywords": [ 6 | "react", 7 | "react-component", 8 | "react-form", 9 | "form" 10 | ], 11 | "homepage": "https://github.com/react-component/form", 12 | "author": "yiminghe@gmail.com", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/react-component/form.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/react-component/form/issues" 19 | }, 20 | "files": [ 21 | "lib", 22 | "es", 23 | "dist", 24 | "assets/*.css" 25 | ], 26 | "license": "MIT", 27 | "main": "./lib/index", 28 | "module": "./es/index", 29 | "config": { 30 | "port": 8000, 31 | "entry": { 32 | "rc-form": [ 33 | "./scripts/index.js" 34 | ] 35 | } 36 | }, 37 | "scripts": { 38 | "build": "rc-tools run build", 39 | "compile": "rc-tools run compile --babel-runtime", 40 | "gh-pages": "rc-tools run gh-pages", 41 | "start": "rc-tools run server", 42 | "pub": "rc-tools run pub --babel-runtime", 43 | "lint": "rc-tools run lint", 44 | "lint:fix": "rc-tools run lint --fix", 45 | "test": "jest", 46 | "prepublish": "rc-tools run guard", 47 | "coverage": "jest --coverage" 48 | }, 49 | "jest": { 50 | "setupFiles": [ 51 | "./tests/setup.js" 52 | ], 53 | "collectCoverageFrom": [ 54 | "src/**/*" 55 | ], 56 | "snapshotSerializers": [ 57 | "enzyme-to-json/serializer" 58 | ], 59 | "transform": { 60 | "\\.jsx?$": "./node_modules/rc-tools/scripts/jestPreprocessor.js" 61 | } 62 | }, 63 | "devDependencies": { 64 | "antd": "3.x", 65 | "async": "^3.0.0", 66 | "enzyme": "^3.1.0", 67 | "enzyme-adapter-react-16": "^1.0.2", 68 | "enzyme-to-json": "^3.1.4", 69 | "jest": "^21.2.1", 70 | "pre-commit": "1.x", 71 | "rc-tools": "8.x", 72 | "react": "^16.1.1", 73 | "react-dom": "^16.1.1", 74 | "react-test-renderer": "^16.1.1", 75 | "history": "^1.16.0", 76 | "prop-types": "^15.5.10", 77 | "react-redux": "^4.0.0", 78 | "react-router": "^3.0.0", 79 | "redux": "^3.0.4" 80 | }, 81 | "pre-commit": [ 82 | "lint" 83 | ], 84 | "dependencies": { 85 | "async-validator": "~1.11.3", 86 | "babel-runtime": "6.x", 87 | "create-react-class": "^15.5.3", 88 | "dom-scroll-into-view": "1.x", 89 | "hoist-non-react-statics": "^3.3.0", 90 | "lodash": "^4.17.4", 91 | "rc-util": "^4.15.3", 92 | "react-is": "^16.13.1", 93 | "warning": "^4.0.3" 94 | }, 95 | "peerDependencies": { 96 | "prop-types": "^15.0" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /scripts/index.js: -------------------------------------------------------------------------------- 1 | // export this package's api 2 | import createForm from '../src/createForm'; 3 | import createDOMForm from '../src/createDOMForm'; 4 | if (typeof window !== 'undefined') { 5 | window.RCForm = { createDOMForm, createForm }; 6 | } 7 | export { createDOMForm, createForm }; 8 | -------------------------------------------------------------------------------- /src/FieldElemWrapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class FieldElemWrapper extends React.Component { 5 | componentDidMount() { 6 | const { name, form } = this.props; 7 | form.domFields[name] = true; 8 | form.recoverClearedField(name); 9 | } 10 | 11 | componentWillUnmount() { 12 | const { name, form } = this.props; 13 | const fieldMeta = form.fieldsStore.getFieldMeta(name); 14 | if (!fieldMeta.preserve) { 15 | // after destroy, delete data 16 | form.clearedFieldMetaCache[name] = { 17 | field: form.fieldsStore.getField(name), 18 | meta: fieldMeta, 19 | }; 20 | form.clearField(name); 21 | } 22 | delete form.domFields[name]; 23 | } 24 | 25 | render() { 26 | return this.props.children; 27 | } 28 | } 29 | 30 | FieldElemWrapper.propTypes = { 31 | name: PropTypes.string, 32 | form: PropTypes.shape({ 33 | domFields: PropTypes.objectOf(PropTypes.bool), 34 | recoverClearedField: PropTypes.func, 35 | fieldsStore: PropTypes.shape({ 36 | getFieldMeta: PropTypes.func, 37 | getField: PropTypes.func, 38 | }), 39 | clearedFieldMetaCache: PropTypes.objectOf( 40 | PropTypes.shape({ 41 | field: PropTypes.object, 42 | meta: PropTypes.object, 43 | }), 44 | ), 45 | clearField: PropTypes.func, 46 | }), 47 | children: PropTypes.node, 48 | }; 49 | -------------------------------------------------------------------------------- /src/createBaseForm.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prefer-es6-class */ 2 | /* eslint-disable prefer-promise-reject-errors */ 3 | 4 | import React from 'react'; 5 | import createReactClass from 'create-react-class'; 6 | import unsafeLifecyclesPolyfill from 'rc-util/lib/unsafeLifecyclesPolyfill'; 7 | import AsyncValidator from 'async-validator'; 8 | import warning from 'warning'; 9 | import get from 'lodash/get'; 10 | import set from 'lodash/set'; 11 | import eq from 'lodash/eq'; 12 | import createFieldsStore from './createFieldsStore'; 13 | import { 14 | argumentContainer, 15 | identity, 16 | normalizeValidateRules, 17 | getValidateTriggers, 18 | getValueFromEvent, 19 | hasRules, 20 | getParams, 21 | isEmptyObject, 22 | flattenArray, 23 | supportRef 24 | } from './utils'; 25 | import FieldElemWrapper from './FieldElemWrapper'; 26 | 27 | const DEFAULT_TRIGGER = 'onChange'; 28 | 29 | function createBaseForm(option = {}, mixins = []) { 30 | const { 31 | validateMessages, 32 | onFieldsChange, 33 | onValuesChange, 34 | mapProps = identity, 35 | mapPropsToFields, 36 | fieldNameProp, 37 | fieldMetaProp, 38 | fieldDataProp, 39 | formPropName = 'form', 40 | name: formName, 41 | // @deprecated 42 | withRef, 43 | } = option; 44 | 45 | return function decorate(WrappedComponent) { 46 | const Form = createReactClass({ 47 | mixins, 48 | 49 | getInitialState() { 50 | const fields = mapPropsToFields && mapPropsToFields(this.props); 51 | this.fieldsStore = createFieldsStore(fields || {}); 52 | 53 | this.instances = {}; 54 | this.cachedBind = {}; 55 | this.clearedFieldMetaCache = {}; 56 | 57 | this.renderFields = {}; 58 | this.domFields = {}; 59 | 60 | // HACK: https://github.com/ant-design/ant-design/issues/6406 61 | [ 62 | 'getFieldsValue', 63 | 'getFieldValue', 64 | 'setFieldsInitialValue', 65 | 'getFieldsError', 66 | 'getFieldError', 67 | 'isFieldValidating', 68 | 'isFieldsValidating', 69 | 'isFieldsTouched', 70 | 'isFieldTouched', 71 | ].forEach(key => { 72 | this[key] = (...args) => { 73 | if (process.env.NODE_ENV !== 'production') { 74 | warning( 75 | false, 76 | 'you should not use `ref` on enhanced form, please use `wrappedComponentRef`. ' + 77 | 'See: https://github.com/react-component/form#note-use-wrappedcomponentref-instead-of-withref-after-rc-form140', 78 | ); 79 | } 80 | return this.fieldsStore[key](...args); 81 | }; 82 | }); 83 | 84 | return { 85 | submitting: false, 86 | }; 87 | }, 88 | 89 | componentDidMount() { 90 | this.cleanUpUselessFields(); 91 | }, 92 | 93 | componentWillReceiveProps(nextProps) { 94 | if (mapPropsToFields) { 95 | this.fieldsStore.updateFields(mapPropsToFields(nextProps)); 96 | } 97 | }, 98 | 99 | componentDidUpdate() { 100 | this.cleanUpUselessFields(); 101 | }, 102 | 103 | onCollectCommon(name, action, args) { 104 | const fieldMeta = this.fieldsStore.getFieldMeta(name); 105 | if (fieldMeta[action]) { 106 | fieldMeta[action](...args); 107 | } else if (fieldMeta.originalProps && fieldMeta.originalProps[action]) { 108 | fieldMeta.originalProps[action](...args); 109 | } 110 | const value = fieldMeta.getValueFromEvent 111 | ? fieldMeta.getValueFromEvent(...args) 112 | : getValueFromEvent(...args); 113 | if (onValuesChange && value !== this.fieldsStore.getFieldValue(name)) { 114 | const valuesAll = this.fieldsStore.getAllValues(); 115 | const valuesAllSet = {}; 116 | valuesAll[name] = value; 117 | Object.keys(valuesAll).forEach(key => 118 | set(valuesAllSet, key, valuesAll[key]), 119 | ); 120 | onValuesChange( 121 | { 122 | [formPropName]: this.getForm(), 123 | ...this.props, 124 | }, 125 | set({}, name, value), 126 | valuesAllSet, 127 | ); 128 | } 129 | const field = this.fieldsStore.getField(name); 130 | return { name, field: { ...field, value, touched: true }, fieldMeta }; 131 | }, 132 | 133 | onCollect(name_, action, ...args) { 134 | const { name, field, fieldMeta } = this.onCollectCommon( 135 | name_, 136 | action, 137 | args, 138 | ); 139 | const { validate } = fieldMeta; 140 | 141 | this.fieldsStore.setFieldsAsDirty(); 142 | 143 | const newField = { 144 | ...field, 145 | dirty: hasRules(validate), 146 | }; 147 | this.setFields({ 148 | [name]: newField, 149 | }); 150 | }, 151 | 152 | onCollectValidate(name_, action, ...args) { 153 | const { field, fieldMeta } = this.onCollectCommon(name_, action, args); 154 | const newField = { 155 | ...field, 156 | dirty: true, 157 | }; 158 | 159 | this.fieldsStore.setFieldsAsDirty(); 160 | 161 | this.validateFieldsInternal([newField], { 162 | action, 163 | options: { 164 | firstFields: !!fieldMeta.validateFirst, 165 | }, 166 | }); 167 | }, 168 | 169 | getCacheBind(name, action, fn) { 170 | if (!this.cachedBind[name]) { 171 | this.cachedBind[name] = {}; 172 | } 173 | const cache = this.cachedBind[name]; 174 | if (!cache[action] || cache[action].oriFn !== fn) { 175 | cache[action] = { 176 | fn: fn.bind(this, name, action), 177 | oriFn: fn, 178 | }; 179 | } 180 | return cache[action].fn; 181 | }, 182 | 183 | getFieldDecorator(name, fieldOption) { 184 | const props = this.getFieldProps(name, fieldOption); 185 | return fieldElem => { 186 | // We should put field in record if it is rendered 187 | this.renderFields[name] = true; 188 | 189 | const fieldMeta = this.fieldsStore.getFieldMeta(name); 190 | const originalProps = fieldElem.props; 191 | if (process.env.NODE_ENV !== 'production') { 192 | const valuePropName = fieldMeta.valuePropName; 193 | warning( 194 | !(valuePropName in originalProps), 195 | `\`getFieldDecorator\` will override \`${valuePropName}\`, ` + 196 | `so please don't set \`${valuePropName}\` directly ` + 197 | `and use \`setFieldsValue\` to set it.`, 198 | ); 199 | const defaultValuePropName = `default${valuePropName[0].toUpperCase()}${valuePropName.slice( 200 | 1, 201 | )}`; 202 | warning( 203 | !(defaultValuePropName in originalProps), 204 | `\`${defaultValuePropName}\` is invalid ` + 205 | `for \`getFieldDecorator\` will set \`${valuePropName}\`,` + 206 | ` please use \`option.initialValue\` instead.`, 207 | ); 208 | } 209 | fieldMeta.originalProps = originalProps; 210 | fieldMeta.ref = fieldElem.ref; 211 | const decoratedFieldElem = React.cloneElement(fieldElem, { 212 | ...props, 213 | ...this.fieldsStore.getFieldValuePropValue(fieldMeta), 214 | }); 215 | return supportRef(fieldElem) ? ( 216 | decoratedFieldElem 217 | ) : ( 218 | 219 | {decoratedFieldElem} 220 | 221 | ); 222 | }; 223 | }, 224 | 225 | getFieldProps(name, usersFieldOption = {}) { 226 | if (!name) { 227 | throw new Error('Must call `getFieldProps` with valid name string!'); 228 | } 229 | if (process.env.NODE_ENV !== 'production') { 230 | warning( 231 | this.fieldsStore.isValidNestedFieldName(name), 232 | `One field name cannot be part of another, e.g. \`a\` and \`a.b\`. Check field: ${name}`, 233 | ); 234 | warning( 235 | !('exclusive' in usersFieldOption), 236 | '`option.exclusive` of `getFieldProps`|`getFieldDecorator` had been remove.', 237 | ); 238 | } 239 | 240 | delete this.clearedFieldMetaCache[name]; 241 | 242 | const fieldOption = { 243 | name, 244 | trigger: DEFAULT_TRIGGER, 245 | valuePropName: 'value', 246 | validate: [], 247 | ...usersFieldOption, 248 | }; 249 | 250 | const { 251 | rules, 252 | trigger, 253 | validateTrigger = trigger, 254 | validate, 255 | } = fieldOption; 256 | 257 | const fieldMeta = this.fieldsStore.getFieldMeta(name); 258 | if ('initialValue' in fieldOption) { 259 | fieldMeta.initialValue = fieldOption.initialValue; 260 | } 261 | 262 | const inputProps = { 263 | ...this.fieldsStore.getFieldValuePropValue(fieldOption), 264 | ref: this.getCacheBind(name, `${name}__ref`, this.saveRef), 265 | }; 266 | if (fieldNameProp) { 267 | inputProps[fieldNameProp] = formName ? `${formName}_${name}` : name; 268 | } 269 | 270 | const validateRules = normalizeValidateRules( 271 | validate, 272 | rules, 273 | validateTrigger, 274 | ); 275 | const validateTriggers = getValidateTriggers(validateRules); 276 | validateTriggers.forEach(action => { 277 | if (inputProps[action]) return; 278 | inputProps[action] = this.getCacheBind( 279 | name, 280 | action, 281 | this.onCollectValidate, 282 | ); 283 | }); 284 | 285 | // make sure that the value will be collect 286 | if (trigger && validateTriggers.indexOf(trigger) === -1) { 287 | inputProps[trigger] = this.getCacheBind( 288 | name, 289 | trigger, 290 | this.onCollect, 291 | ); 292 | } 293 | 294 | const meta = { 295 | ...fieldMeta, 296 | ...fieldOption, 297 | validate: validateRules, 298 | }; 299 | this.fieldsStore.setFieldMeta(name, meta); 300 | if (fieldMetaProp) { 301 | inputProps[fieldMetaProp] = meta; 302 | } 303 | 304 | if (fieldDataProp) { 305 | inputProps[fieldDataProp] = this.fieldsStore.getField(name); 306 | } 307 | 308 | // This field is rendered, record it 309 | this.renderFields[name] = true; 310 | 311 | return inputProps; 312 | }, 313 | 314 | getFieldInstance(name) { 315 | return this.instances[name]; 316 | }, 317 | 318 | getRules(fieldMeta, action) { 319 | const actionRules = fieldMeta.validate 320 | .filter(item => { 321 | return !action || item.trigger.indexOf(action) >= 0; 322 | }) 323 | .map(item => item.rules); 324 | return flattenArray(actionRules); 325 | }, 326 | 327 | setFields(maybeNestedFields, callback) { 328 | const fields = this.fieldsStore.flattenRegisteredFields( 329 | maybeNestedFields, 330 | ); 331 | this.fieldsStore.setFields(fields); 332 | if (onFieldsChange) { 333 | const changedFields = Object.keys(fields).reduce( 334 | (acc, name) => set(acc, name, this.fieldsStore.getField(name)), 335 | {}, 336 | ); 337 | onFieldsChange( 338 | { 339 | [formPropName]: this.getForm(), 340 | ...this.props, 341 | }, 342 | changedFields, 343 | this.fieldsStore.getNestedAllFields(), 344 | ); 345 | } 346 | this.forceUpdate(callback); 347 | }, 348 | 349 | setFieldsValue(changedValues, callback) { 350 | const { fieldsMeta } = this.fieldsStore; 351 | const values = this.fieldsStore.flattenRegisteredFields(changedValues); 352 | const newFields = Object.keys(values).reduce((acc, name) => { 353 | const isRegistered = fieldsMeta[name]; 354 | if (process.env.NODE_ENV !== 'production') { 355 | warning( 356 | isRegistered, 357 | 'Cannot use `setFieldsValue` until ' + 358 | 'you use `getFieldDecorator` or `getFieldProps` to register it.', 359 | ); 360 | } 361 | if (isRegistered) { 362 | const value = values[name]; 363 | acc[name] = { 364 | value, 365 | }; 366 | } 367 | return acc; 368 | }, {}); 369 | this.setFields(newFields, callback); 370 | if (onValuesChange) { 371 | const allValues = this.fieldsStore.getAllValues(); 372 | onValuesChange( 373 | { 374 | [formPropName]: this.getForm(), 375 | ...this.props, 376 | }, 377 | changedValues, 378 | allValues, 379 | ); 380 | } 381 | }, 382 | 383 | saveRef(name, _, component) { 384 | if (!component) { 385 | const fieldMeta = this.fieldsStore.getFieldMeta(name); 386 | if (!fieldMeta.preserve) { 387 | // after destroy, delete data 388 | this.clearedFieldMetaCache[name] = { 389 | field: this.fieldsStore.getField(name), 390 | meta: fieldMeta, 391 | }; 392 | this.clearField(name); 393 | } 394 | delete this.domFields[name]; 395 | return; 396 | } 397 | this.domFields[name] = true; 398 | this.recoverClearedField(name); 399 | const fieldMeta = this.fieldsStore.getFieldMeta(name); 400 | if (fieldMeta) { 401 | const ref = fieldMeta.ref; 402 | if (ref) { 403 | if (typeof ref === 'string') { 404 | throw new Error(`can not set ref string for ${name}`); 405 | } else if (typeof ref === 'function') { 406 | ref(component); 407 | } else if (Object.prototype.hasOwnProperty.call(ref, 'current')) { 408 | ref.current = component; 409 | } 410 | } 411 | } 412 | this.instances[name] = component; 413 | }, 414 | 415 | cleanUpUselessFields() { 416 | const fieldList = this.fieldsStore.getAllFieldsName(); 417 | const removedList = fieldList.filter(field => { 418 | const fieldMeta = this.fieldsStore.getFieldMeta(field); 419 | return ( 420 | !this.renderFields[field] && 421 | !this.domFields[field] && 422 | !fieldMeta.preserve 423 | ); 424 | }); 425 | if (removedList.length) { 426 | removedList.forEach(this.clearField); 427 | } 428 | this.renderFields = {}; 429 | }, 430 | 431 | clearField(name) { 432 | this.fieldsStore.clearField(name); 433 | delete this.instances[name]; 434 | delete this.cachedBind[name]; 435 | }, 436 | 437 | resetFields(ns) { 438 | const newFields = this.fieldsStore.resetFields(ns); 439 | if (Object.keys(newFields).length > 0) { 440 | this.setFields(newFields); 441 | } 442 | if (ns) { 443 | const names = Array.isArray(ns) ? ns : [ns]; 444 | names.forEach(name => delete this.clearedFieldMetaCache[name]); 445 | } else { 446 | this.clearedFieldMetaCache = {}; 447 | } 448 | }, 449 | 450 | recoverClearedField(name) { 451 | if (this.clearedFieldMetaCache[name]) { 452 | this.fieldsStore.setFields({ 453 | [name]: this.clearedFieldMetaCache[name].field, 454 | }); 455 | this.fieldsStore.setFieldMeta( 456 | name, 457 | this.clearedFieldMetaCache[name].meta, 458 | ); 459 | delete this.clearedFieldMetaCache[name]; 460 | } 461 | }, 462 | 463 | validateFieldsInternal( 464 | fields, 465 | { fieldNames, action, options = {} }, 466 | callback, 467 | ) { 468 | const allRules = {}; 469 | const allValues = {}; 470 | const allFields = {}; 471 | const alreadyErrors = {}; 472 | fields.forEach(field => { 473 | const name = field.name; 474 | if (options.force !== true && field.dirty === false) { 475 | if (field.errors) { 476 | set(alreadyErrors, name, { errors: field.errors }); 477 | } 478 | return; 479 | } 480 | const fieldMeta = this.fieldsStore.getFieldMeta(name); 481 | const newField = { 482 | ...field, 483 | }; 484 | newField.errors = undefined; 485 | newField.validating = true; 486 | newField.dirty = true; 487 | allRules[name] = this.getRules(fieldMeta, action); 488 | allValues[name] = newField.value; 489 | allFields[name] = newField; 490 | }); 491 | this.setFields(allFields); 492 | // in case normalize 493 | Object.keys(allValues).forEach(f => { 494 | allValues[f] = this.fieldsStore.getFieldValue(f); 495 | }); 496 | if (callback && isEmptyObject(allFields)) { 497 | callback( 498 | isEmptyObject(alreadyErrors) ? null : alreadyErrors, 499 | this.fieldsStore.getFieldsValue(fieldNames), 500 | ); 501 | return; 502 | } 503 | const validator = new AsyncValidator(allRules); 504 | if (validateMessages) { 505 | validator.messages(validateMessages); 506 | } 507 | validator.validate(allValues, options, errors => { 508 | const errorsGroup = { 509 | ...alreadyErrors, 510 | }; 511 | if (errors && errors.length) { 512 | errors.forEach(e => { 513 | const errorFieldName = e.field; 514 | let fieldName = errorFieldName; 515 | 516 | // Handle using array validation rule. 517 | // ref: https://github.com/ant-design/ant-design/issues/14275 518 | Object.keys(allRules).some(ruleFieldName => { 519 | const rules = allRules[ruleFieldName] || []; 520 | 521 | // Exist if match rule 522 | if (ruleFieldName === errorFieldName) { 523 | fieldName = ruleFieldName; 524 | return true; 525 | } 526 | 527 | // Skip if not match array type 528 | if ( 529 | rules.every(({ type }) => type !== 'array') || 530 | errorFieldName.indexOf(`${ruleFieldName}.`) !== 0 531 | ) { 532 | return false; 533 | } 534 | 535 | // Exist if match the field name 536 | const restPath = errorFieldName.slice(ruleFieldName.length + 1); 537 | if (/^\d+$/.test(restPath)) { 538 | fieldName = ruleFieldName; 539 | return true; 540 | } 541 | 542 | return false; 543 | }); 544 | 545 | const field = get(errorsGroup, fieldName); 546 | if (typeof field !== 'object' || Array.isArray(field)) { 547 | set(errorsGroup, fieldName, { errors: [] }); 548 | } 549 | const fieldErrors = get(errorsGroup, fieldName.concat('.errors')); 550 | fieldErrors.push(e); 551 | }); 552 | } 553 | const expired = []; 554 | const nowAllFields = {}; 555 | Object.keys(allRules).forEach(name => { 556 | const fieldErrors = get(errorsGroup, name); 557 | const nowField = this.fieldsStore.getField(name); 558 | // avoid concurrency problems 559 | if (!eq(nowField.value, allValues[name])) { 560 | expired.push({ 561 | name, 562 | }); 563 | } else { 564 | nowField.errors = fieldErrors && fieldErrors.errors; 565 | nowField.value = allValues[name]; 566 | nowField.validating = false; 567 | nowField.dirty = false; 568 | nowAllFields[name] = nowField; 569 | } 570 | }); 571 | this.setFields(nowAllFields); 572 | if (callback) { 573 | if (expired.length) { 574 | expired.forEach(({ name }) => { 575 | const fieldErrors = [ 576 | { 577 | message: `${name} need to revalidate`, 578 | field: name, 579 | }, 580 | ]; 581 | set(errorsGroup, name, { 582 | expired: true, 583 | errors: fieldErrors, 584 | }); 585 | }); 586 | } 587 | 588 | callback( 589 | isEmptyObject(errorsGroup) ? null : errorsGroup, 590 | this.fieldsStore.getFieldsValue(fieldNames), 591 | ); 592 | } 593 | }); 594 | }, 595 | 596 | validateFields(ns, opt, cb) { 597 | const pending = new Promise((resolve, reject) => { 598 | const { names, options } = getParams(ns, opt, cb); 599 | let { callback } = getParams(ns, opt, cb); 600 | if (!callback || typeof callback === 'function') { 601 | const oldCb = callback; 602 | callback = (errors, values) => { 603 | if (oldCb) { 604 | oldCb(errors, values); 605 | } 606 | if (errors) { 607 | reject({ errors, values }); 608 | } else { 609 | resolve(values); 610 | } 611 | }; 612 | } 613 | const fieldNames = names 614 | ? this.fieldsStore.getValidFieldsFullName(names) 615 | : this.fieldsStore.getValidFieldsName(); 616 | const fields = fieldNames 617 | .filter(name => { 618 | const fieldMeta = this.fieldsStore.getFieldMeta(name); 619 | return hasRules(fieldMeta.validate); 620 | }) 621 | .map(name => { 622 | const field = this.fieldsStore.getField(name); 623 | field.value = this.fieldsStore.getFieldValue(name); 624 | return field; 625 | }); 626 | if (!fields.length) { 627 | callback(null, this.fieldsStore.getFieldsValue(fieldNames)); 628 | return; 629 | } 630 | if (!('firstFields' in options)) { 631 | options.firstFields = fieldNames.filter(name => { 632 | const fieldMeta = this.fieldsStore.getFieldMeta(name); 633 | return !!fieldMeta.validateFirst; 634 | }); 635 | } 636 | this.validateFieldsInternal( 637 | fields, 638 | { 639 | fieldNames, 640 | options, 641 | }, 642 | callback, 643 | ); 644 | }); 645 | pending.catch(e => { 646 | // eslint-disable-next-line no-console 647 | if (console.error && process.env.NODE_ENV !== 'production') { 648 | // eslint-disable-next-line no-console 649 | console.error(e); 650 | } 651 | return e; 652 | }); 653 | return pending; 654 | }, 655 | 656 | isSubmitting() { 657 | if ( 658 | process.env.NODE_ENV !== 'production' && 659 | process.env.NODE_ENV !== 'test' 660 | ) { 661 | warning( 662 | false, 663 | '`isSubmitting` is deprecated. ' + 664 | "Actually, it's more convenient to handle submitting status by yourself.", 665 | ); 666 | } 667 | return this.state.submitting; 668 | }, 669 | 670 | submit(callback) { 671 | if ( 672 | process.env.NODE_ENV !== 'production' && 673 | process.env.NODE_ENV !== 'test' 674 | ) { 675 | warning( 676 | false, 677 | '`submit` is deprecated. ' + 678 | "Actually, it's more convenient to handle submitting status by yourself.", 679 | ); 680 | } 681 | const fn = () => { 682 | this.setState({ 683 | submitting: false, 684 | }); 685 | }; 686 | this.setState({ 687 | submitting: true, 688 | }); 689 | callback(fn); 690 | }, 691 | 692 | render() { 693 | const { wrappedComponentRef, ...restProps } = this.props; // eslint-disable-line 694 | const formProps = { 695 | [formPropName]: this.getForm(), 696 | }; 697 | if (withRef) { 698 | if ( 699 | process.env.NODE_ENV !== 'production' && 700 | process.env.NODE_ENV !== 'test' 701 | ) { 702 | warning( 703 | false, 704 | '`withRef` is deprecated, please use `wrappedComponentRef` instead. ' + 705 | 'See: https://github.com/react-component/form#note-use-wrappedcomponentref-instead-of-withref-after-rc-form140', 706 | ); 707 | } 708 | formProps.ref = 'wrappedComponent'; 709 | } else if (wrappedComponentRef) { 710 | formProps.ref = wrappedComponentRef; 711 | } 712 | const props = mapProps.call(this, { 713 | ...formProps, 714 | ...restProps, 715 | }); 716 | return ; 717 | }, 718 | }); 719 | 720 | return argumentContainer(unsafeLifecyclesPolyfill(Form), WrappedComponent); 721 | }; 722 | } 723 | 724 | export default createBaseForm; 725 | -------------------------------------------------------------------------------- /src/createDOMForm.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import scrollIntoView from 'dom-scroll-into-view'; 3 | import has from 'lodash/has'; 4 | import createBaseForm from './createBaseForm'; 5 | import { mixin as formMixin } from './createForm'; 6 | import { getParams } from './utils'; 7 | 8 | function computedStyle(el, prop) { 9 | const getComputedStyle = window.getComputedStyle; 10 | const style = 11 | // If we have getComputedStyle 12 | getComputedStyle ? 13 | // Query it 14 | // TODO: From CSS-Query notes, we might need (node, null) for FF 15 | getComputedStyle(el) : 16 | 17 | // Otherwise, we are in IE and use currentStyle 18 | el.currentStyle; 19 | if (style) { 20 | return style 21 | [ 22 | // Switch to camelCase for CSSOM 23 | // DEV: Grabbed from jQuery 24 | // https://github.com/jquery/jquery/blob/1.9-stable/src/css.js#L191-L194 25 | // https://github.com/jquery/jquery/blob/1.9-stable/src/core.js#L593-L597 26 | prop.replace(/-(\w)/gi, (word, letter) => { 27 | return letter.toUpperCase(); 28 | }) 29 | ]; 30 | } 31 | return undefined; 32 | } 33 | 34 | function getScrollableContainer(n) { 35 | let node = n; 36 | let nodeName; 37 | /* eslint no-cond-assign:0 */ 38 | while ((nodeName = node.nodeName.toLowerCase()) !== 'body') { 39 | const overflowY = computedStyle(node, 'overflowY'); 40 | // https://stackoverflow.com/a/36900407/3040605 41 | if ( 42 | node !== n && 43 | (overflowY === 'auto' || overflowY === 'scroll') && 44 | node.scrollHeight > node.clientHeight 45 | ) { 46 | return node; 47 | } 48 | node = node.parentNode; 49 | } 50 | return nodeName === 'body' ? node.ownerDocument : node; 51 | } 52 | 53 | const mixin = { 54 | getForm() { 55 | return { 56 | ...formMixin.getForm.call(this), 57 | validateFieldsAndScroll: this.validateFieldsAndScroll, 58 | }; 59 | }, 60 | 61 | validateFieldsAndScroll(ns, opt, cb) { 62 | const { names, callback, options } = getParams(ns, opt, cb); 63 | 64 | const newCb = (error, values) => { 65 | if (error) { 66 | const validNames = this.fieldsStore.getValidFieldsName(); 67 | let firstNode; 68 | let firstTop; 69 | 70 | validNames.forEach((name) => { 71 | if (has(error, name)) { 72 | const instance = this.getFieldInstance(name); 73 | if (instance) { 74 | const node = ReactDOM.findDOMNode(instance); 75 | const top = node.getBoundingClientRect().top; 76 | if (node.type !== 'hidden' && (firstTop === undefined || firstTop > top)) { 77 | firstTop = top; 78 | firstNode = node; 79 | } 80 | } 81 | } 82 | }); 83 | 84 | if (firstNode) { 85 | const c = options.container || getScrollableContainer(firstNode); 86 | scrollIntoView(firstNode, c, { 87 | onlyScrollIfNeeded: true, 88 | ...options.scroll, 89 | }); 90 | } 91 | } 92 | 93 | if (typeof callback === 'function') { 94 | callback(error, values); 95 | } 96 | }; 97 | 98 | return this.validateFields(names, options, newCb); 99 | }, 100 | }; 101 | 102 | function createDOMForm(option) { 103 | return createBaseForm({ 104 | ...option, 105 | }, [mixin]); 106 | } 107 | 108 | export default createDOMForm; 109 | -------------------------------------------------------------------------------- /src/createFieldsStore.js: -------------------------------------------------------------------------------- 1 | import set from 'lodash/set'; 2 | import createFormField, { isFormField } from './createFormField'; 3 | import { 4 | hasRules, 5 | flattenFields, 6 | getErrorStrs, 7 | startsWith, 8 | } from './utils'; 9 | 10 | function partOf(a, b) { 11 | return b.indexOf(a) === 0 && ['.', '['].indexOf(b[a.length]) !== -1; 12 | } 13 | 14 | function internalFlattenFields(fields) { 15 | return flattenFields( 16 | fields, 17 | (_, node) => isFormField(node), 18 | 'You must wrap field data with `createFormField`.' 19 | ); 20 | } 21 | 22 | class FieldsStore { 23 | constructor(fields) { 24 | this.fields = internalFlattenFields(fields); 25 | this.fieldsMeta = {}; 26 | } 27 | 28 | updateFields(fields) { 29 | this.fields = internalFlattenFields(fields); 30 | } 31 | 32 | flattenRegisteredFields(fields) { 33 | const validFieldsName = this.getAllFieldsName(); 34 | return flattenFields( 35 | fields, 36 | path => validFieldsName.indexOf(path) >= 0, 37 | 'You cannot set a form field before rendering a field associated with the value.' 38 | ); 39 | } 40 | 41 | setFieldsInitialValue = (initialValues) => { 42 | const flattenedInitialValues = this.flattenRegisteredFields(initialValues); 43 | const fieldsMeta = this.fieldsMeta; 44 | Object.keys(flattenedInitialValues).forEach(name => { 45 | if (fieldsMeta[name]) { 46 | this.setFieldMeta(name, { 47 | ...this.getFieldMeta(name), 48 | initialValue: flattenedInitialValues[name], 49 | }); 50 | } 51 | }); 52 | } 53 | 54 | setFields(fields) { 55 | const fieldsMeta = this.fieldsMeta; 56 | const nowFields = { 57 | ...this.fields, 58 | ...fields, 59 | }; 60 | const nowValues = {}; 61 | Object.keys(fieldsMeta) 62 | .forEach((f) => { 63 | nowValues[f] = this.getValueFromFields(f, nowFields); 64 | }); 65 | Object.keys(nowValues).forEach((f) => { 66 | const value = nowValues[f]; 67 | const fieldMeta = this.getFieldMeta(f); 68 | if (fieldMeta && fieldMeta.normalize) { 69 | const nowValue = 70 | fieldMeta.normalize(value, this.getValueFromFields(f, this.fields), nowValues); 71 | if (nowValue !== value) { 72 | nowFields[f] = { 73 | ...nowFields[f], 74 | value: nowValue, 75 | }; 76 | } 77 | } 78 | }); 79 | this.fields = nowFields; 80 | } 81 | 82 | resetFields(ns) { 83 | const { fields } = this; 84 | const names = ns ? 85 | this.getValidFieldsFullName(ns) : 86 | this.getAllFieldsName(); 87 | return names.reduce((acc, name) => { 88 | const field = fields[name]; 89 | if (field && 'value' in field) { 90 | acc[name] = {}; 91 | } 92 | return acc; 93 | }, {}); 94 | } 95 | 96 | setFieldMeta(name, meta) { 97 | this.fieldsMeta[name] = meta; 98 | } 99 | 100 | setFieldsAsDirty() { 101 | Object.keys(this.fields).forEach((name) => { 102 | const field = this.fields[name]; 103 | const fieldMeta = this.fieldsMeta[name]; 104 | if (field && fieldMeta && hasRules(fieldMeta.validate)) { 105 | this.fields[name] = { 106 | ...field, 107 | dirty: true, 108 | }; 109 | } 110 | }); 111 | } 112 | 113 | getFieldMeta(name) { 114 | this.fieldsMeta[name] = this.fieldsMeta[name] || {}; 115 | return this.fieldsMeta[name]; 116 | } 117 | 118 | getValueFromFields(name, fields) { 119 | const field = fields[name]; 120 | if (field && 'value' in field) { 121 | return field.value; 122 | } 123 | const fieldMeta = this.getFieldMeta(name); 124 | return fieldMeta && fieldMeta.initialValue; 125 | } 126 | 127 | getAllValues = () => { 128 | const { fieldsMeta, fields } = this; 129 | return Object.keys(fieldsMeta) 130 | .reduce((acc, name) => set(acc, name, this.getValueFromFields(name, fields)), {}); 131 | } 132 | 133 | getValidFieldsName() { 134 | const { fieldsMeta } = this; 135 | return fieldsMeta ? 136 | Object.keys(fieldsMeta).filter(name => !this.getFieldMeta(name).hidden) : 137 | []; 138 | } 139 | 140 | getAllFieldsName() { 141 | const { fieldsMeta } = this; 142 | return fieldsMeta ? Object.keys(fieldsMeta) : []; 143 | } 144 | 145 | getValidFieldsFullName(maybePartialName) { 146 | const maybePartialNames = Array.isArray(maybePartialName) ? 147 | maybePartialName : [maybePartialName]; 148 | return this.getValidFieldsName() 149 | .filter(fullName => maybePartialNames.some(partialName => ( 150 | fullName === partialName || ( 151 | startsWith(fullName, partialName) && 152 | ['.', '['].indexOf(fullName[partialName.length]) >= 0 153 | ) 154 | ))); 155 | } 156 | 157 | getFieldValuePropValue(fieldMeta) { 158 | const { name, getValueProps, valuePropName } = fieldMeta; 159 | const field = this.getField(name); 160 | const fieldValue = 'value' in field ? 161 | field.value : fieldMeta.initialValue; 162 | if (getValueProps) { 163 | return getValueProps(fieldValue); 164 | } 165 | return { [valuePropName]: fieldValue }; 166 | } 167 | 168 | getField(name) { 169 | return { 170 | ...this.fields[name], 171 | name, 172 | }; 173 | } 174 | 175 | getNotCollectedFields() { 176 | const fieldsName = this.getValidFieldsName(); 177 | return fieldsName 178 | .filter(name => !this.fields[name]) 179 | .map(name => ({ 180 | name, 181 | dirty: false, 182 | value: this.getFieldMeta(name).initialValue, 183 | })) 184 | .reduce((acc, field) => set(acc, field.name, createFormField(field)), {}); 185 | } 186 | 187 | getNestedAllFields() { 188 | return Object.keys(this.fields) 189 | .reduce( 190 | (acc, name) => set(acc, name, createFormField(this.fields[name])), 191 | this.getNotCollectedFields() 192 | ); 193 | } 194 | 195 | getFieldMember(name, member) { 196 | return this.getField(name)[member]; 197 | } 198 | 199 | getNestedFields(names, getter) { 200 | const fields = names || this.getValidFieldsName(); 201 | return fields.reduce((acc, f) => set(acc, f, getter(f)), {}); 202 | } 203 | 204 | getNestedField(name, getter) { 205 | const fullNames = this.getValidFieldsFullName(name); 206 | if ( 207 | fullNames.length === 0 || // Not registered 208 | (fullNames.length === 1 && fullNames[0] === name) // Name already is full name. 209 | ) { 210 | return getter(name); 211 | } 212 | const isArrayValue = fullNames[0][name.length] === '['; 213 | const suffixNameStartIndex = isArrayValue ? name.length : name.length + 1; 214 | return fullNames 215 | .reduce( 216 | (acc, fullName) => set( 217 | acc, 218 | fullName.slice(suffixNameStartIndex), 219 | getter(fullName) 220 | ), 221 | isArrayValue ? [] : {} 222 | ); 223 | } 224 | 225 | getFieldsValue = (names) => { 226 | return this.getNestedFields(names, this.getFieldValue); 227 | } 228 | 229 | getFieldValue = (name) => { 230 | const { fields } = this; 231 | return this.getNestedField(name, (fullName) => this.getValueFromFields(fullName, fields)); 232 | } 233 | 234 | getFieldsError = (names) => { 235 | return this.getNestedFields(names, this.getFieldError); 236 | } 237 | 238 | getFieldError = (name) => { 239 | return this.getNestedField( 240 | name, 241 | (fullName) => getErrorStrs(this.getFieldMember(fullName, 'errors')) 242 | ); 243 | } 244 | 245 | isFieldValidating = (name) => { 246 | return this.getFieldMember(name, 'validating'); 247 | } 248 | 249 | isFieldsValidating = (ns) => { 250 | const names = ns || this.getValidFieldsName(); 251 | return names.some((n) => this.isFieldValidating(n)); 252 | } 253 | 254 | isFieldTouched = (name) => { 255 | return this.getFieldMember(name, 'touched'); 256 | } 257 | 258 | isFieldsTouched = (ns) => { 259 | const names = ns || this.getValidFieldsName(); 260 | return names.some((n) => this.isFieldTouched(n)); 261 | } 262 | 263 | // @private 264 | // BG: `a` and `a.b` cannot be use in the same form 265 | isValidNestedFieldName(name) { 266 | const names = this.getAllFieldsName(); 267 | return names.every(n => !partOf(n, name) && !partOf(name, n)); 268 | } 269 | 270 | clearField(name) { 271 | delete this.fields[name]; 272 | delete this.fieldsMeta[name]; 273 | } 274 | } 275 | 276 | export default function createFieldsStore(fields) { 277 | return new FieldsStore(fields); 278 | } 279 | -------------------------------------------------------------------------------- /src/createForm.js: -------------------------------------------------------------------------------- 1 | import createBaseForm from './createBaseForm'; 2 | 3 | export const mixin = { 4 | getForm() { 5 | return { 6 | getFieldsValue: this.fieldsStore.getFieldsValue, 7 | getFieldValue: this.fieldsStore.getFieldValue, 8 | getFieldInstance: this.getFieldInstance, 9 | setFieldsValue: this.setFieldsValue, 10 | setFields: this.setFields, 11 | setFieldsInitialValue: this.fieldsStore.setFieldsInitialValue, 12 | getFieldDecorator: this.getFieldDecorator, 13 | getFieldProps: this.getFieldProps, 14 | getFieldsError: this.fieldsStore.getFieldsError, 15 | getFieldError: this.fieldsStore.getFieldError, 16 | isFieldValidating: this.fieldsStore.isFieldValidating, 17 | isFieldsValidating: this.fieldsStore.isFieldsValidating, 18 | isFieldsTouched: this.fieldsStore.isFieldsTouched, 19 | isFieldTouched: this.fieldsStore.isFieldTouched, 20 | isSubmitting: this.isSubmitting, 21 | submit: this.submit, 22 | validateFields: this.validateFields, 23 | resetFields: this.resetFields, 24 | }; 25 | }, 26 | }; 27 | 28 | function createForm(options) { 29 | return createBaseForm(options, [mixin]); 30 | } 31 | 32 | export default createForm; 33 | -------------------------------------------------------------------------------- /src/createFormField.js: -------------------------------------------------------------------------------- 1 | class Field { 2 | constructor(fields) { 3 | Object.assign(this, fields); 4 | } 5 | } 6 | 7 | export function isFormField(obj) { 8 | return obj instanceof Field; 9 | } 10 | 11 | export default function createFormField(field) { 12 | if (isFormField(field)) { 13 | return field; 14 | } 15 | return new Field(field); 16 | } 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // export this package's api 2 | import createForm from './createForm'; 3 | import createFormField from './createFormField'; 4 | import formShape from './propTypes'; 5 | 6 | export { createFormField, formShape, createForm }; 7 | -------------------------------------------------------------------------------- /src/propTypes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const formShape = PropTypes.shape({ 4 | getFieldsValue: PropTypes.func, 5 | getFieldValue: PropTypes.func, 6 | getFieldInstance: PropTypes.func, 7 | setFieldsValue: PropTypes.func, 8 | setFields: PropTypes.func, 9 | setFieldsInitialValue: PropTypes.func, 10 | getFieldDecorator: PropTypes.func, 11 | getFieldProps: PropTypes.func, 12 | getFieldsError: PropTypes.func, 13 | getFieldError: PropTypes.func, 14 | isFieldValidating: PropTypes.func, 15 | isFieldsValidating: PropTypes.func, 16 | isFieldsTouched: PropTypes.func, 17 | isFieldTouched: PropTypes.func, 18 | isSubmitting: PropTypes.func, 19 | submit: PropTypes.func, 20 | validateFields: PropTypes.func, 21 | resetFields: PropTypes.func, 22 | }); 23 | 24 | export default formShape; 25 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import hoistStatics from 'hoist-non-react-statics'; 2 | import warning from 'warning'; 3 | import { isMemo } from 'react-is'; 4 | 5 | function getDisplayName(WrappedComponent) { 6 | return WrappedComponent.displayName || WrappedComponent.name || 'WrappedComponent'; 7 | } 8 | 9 | export function argumentContainer(Container, WrappedComponent) { 10 | /* eslint no-param-reassign:0 */ 11 | Container.displayName = `Form(${getDisplayName(WrappedComponent)})`; 12 | Container.WrappedComponent = WrappedComponent; 13 | return hoistStatics(Container, WrappedComponent); 14 | } 15 | 16 | export function identity(obj) { 17 | return obj; 18 | } 19 | 20 | export function flattenArray(arr) { 21 | return Array.prototype.concat.apply([], arr); 22 | } 23 | 24 | export function treeTraverse(path = '', tree, isLeafNode, errorMessage, callback) { 25 | if (isLeafNode(path, tree)) { 26 | callback(path, tree); 27 | } else if (tree === undefined || tree === null) { 28 | // Do nothing 29 | } else if (Array.isArray(tree)) { 30 | tree.forEach((subTree, index) => treeTraverse( 31 | `${path}[${index}]`, 32 | subTree, 33 | isLeafNode, 34 | errorMessage, 35 | callback 36 | )); 37 | } else { // It's object and not a leaf node 38 | if (typeof tree !== 'object') { 39 | warning(false, errorMessage); 40 | return; 41 | } 42 | Object.keys(tree).forEach(subTreeKey => { 43 | const subTree = tree[subTreeKey]; 44 | treeTraverse( 45 | `${path}${path ? '.' : ''}${subTreeKey}`, 46 | subTree, 47 | isLeafNode, 48 | errorMessage, 49 | callback 50 | ); 51 | }); 52 | } 53 | } 54 | 55 | export function flattenFields(maybeNestedFields, isLeafNode, errorMessage) { 56 | const fields = {}; 57 | treeTraverse(undefined, maybeNestedFields, isLeafNode, errorMessage, (path, node) => { 58 | fields[path] = node; 59 | }); 60 | return fields; 61 | } 62 | 63 | export function normalizeValidateRules(validate, rules, validateTrigger) { 64 | const validateRules = validate.map((item) => { 65 | const newItem = { 66 | ...item, 67 | trigger: item.trigger || [], 68 | }; 69 | if (typeof newItem.trigger === 'string') { 70 | newItem.trigger = [newItem.trigger]; 71 | } 72 | return newItem; 73 | }); 74 | if (rules) { 75 | validateRules.push({ 76 | trigger: validateTrigger ? [].concat(validateTrigger) : [], 77 | rules, 78 | }); 79 | } 80 | return validateRules; 81 | } 82 | 83 | export function getValidateTriggers(validateRules) { 84 | return validateRules 85 | .filter(item => !!item.rules && item.rules.length) 86 | .map(item => item.trigger) 87 | .reduce((pre, curr) => pre.concat(curr), []); 88 | } 89 | 90 | export function getValueFromEvent(e) { 91 | // To support custom element 92 | if (!e || !e.target) { 93 | return e; 94 | } 95 | const { target } = e; 96 | return target.type === 'checkbox' ? target.checked : target.value; 97 | } 98 | 99 | export function getErrorStrs(errors) { 100 | if (errors) { 101 | return errors.map((e) => { 102 | if (e && e.message) { 103 | return e.message; 104 | } 105 | return e; 106 | }); 107 | } 108 | return errors; 109 | } 110 | 111 | export function getParams(ns, opt, cb) { 112 | let names = ns; 113 | let options = opt; 114 | let callback = cb; 115 | if (cb === undefined) { 116 | if (typeof names === 'function') { 117 | callback = names; 118 | options = {}; 119 | names = undefined; 120 | } else if (Array.isArray(names)) { 121 | if (typeof options === 'function') { 122 | callback = options; 123 | options = {}; 124 | } else { 125 | options = options || {}; 126 | } 127 | } else { 128 | callback = options; 129 | options = names || {}; 130 | names = undefined; 131 | } 132 | } 133 | return { 134 | names, 135 | options, 136 | callback, 137 | }; 138 | } 139 | 140 | export function isEmptyObject(obj) { 141 | return Object.keys(obj).length === 0; 142 | } 143 | 144 | export function hasRules(validate) { 145 | if (validate) { 146 | return validate.some((item) => { 147 | return item.rules && item.rules.length; 148 | }); 149 | } 150 | return false; 151 | } 152 | 153 | export function startsWith(str, prefix) { 154 | return str.lastIndexOf(prefix, 0) === 0; 155 | } 156 | 157 | export function supportRef(nodeOrComponent) { 158 | const type = isMemo(nodeOrComponent) 159 | ? nodeOrComponent.type.type 160 | : nodeOrComponent.type; 161 | 162 | // Function component node 163 | if (typeof type === 'function' && !(type.prototype && type.prototype.render)) { 164 | return false; 165 | } 166 | 167 | // Class component 168 | if ( 169 | typeof nodeOrComponent === 'function' && 170 | !(nodeOrComponent.prototype && nodeOrComponent.prototype.render) 171 | ) { 172 | return false; 173 | } 174 | 175 | return true; 176 | } 177 | -------------------------------------------------------------------------------- /tests/FieldElemWrapper.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import FieldElemWrapper from '../src/FieldElemWrapper'; 4 | 5 | describe('FieldElemWrapper', () => { 6 | it('should register field when mount and clear field when unmount', () => { 7 | const name = 'field-name'; 8 | const mockForm = { 9 | domFields: {}, 10 | recoverClearedField: jest.fn(), 11 | fieldsStore: { 12 | getFieldMeta: jest.fn(() => ({})), 13 | getField: jest.fn(() => ({})), 14 | }, 15 | clearedFieldMetaCache: {}, 16 | clearField: jest.fn(), 17 | }; 18 | const children = children; 19 | const wrapper = mount( 20 | 21 | {children} 22 | , 23 | ); 24 | 25 | expect(wrapper.children().get(0)).toEqual(children); 26 | expect(mockForm.domFields[name]).toBe(true); 27 | expect(mockForm.recoverClearedField).toBeCalledWith(name); 28 | 29 | const mockFieldMeta = {}; 30 | const mockField = {}; 31 | mockForm.fieldsStore.getFieldMeta.mockReturnValueOnce(mockFieldMeta); 32 | mockForm.fieldsStore.getField.mockReturnValueOnce(mockField); 33 | wrapper.unmount(); 34 | expect(mockForm.clearedFieldMetaCache[name]).toEqual({ 35 | field: mockField, 36 | meta: mockFieldMeta, 37 | }); 38 | expect(mockForm.clearField).toBeCalledWith(name); 39 | expect(mockForm.domFields[name]).toBe(undefined); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/async-validation.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, react/prop-types, react/no-render-return-value */ 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { Simulate } from 'react-dom/test-utils'; 6 | import async from 'async'; 7 | import createForm from '../src/createForm'; 8 | 9 | class Test extends React.Component { 10 | check = (rule, value, callback) => { 11 | setTimeout(() => { 12 | if (value === '1') { 13 | callback(); 14 | } else { 15 | callback(new Error('must be 1')); 16 | } 17 | }, 100); 18 | } 19 | 20 | render() { 21 | const { getFieldProps } = this.props.form; 22 | return ( 23 |
24 | 25 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | Test = createForm({ 36 | withRef: true, 37 | })(Test); 38 | 39 | describe('Async Validation', () => { 40 | let container; 41 | let component; 42 | let form; 43 | 44 | beforeEach(() => { 45 | container = document.createElement('div'); 46 | document.body.appendChild(container); 47 | component = ReactDOM.render(, container); 48 | component = component.refs.wrappedComponent; 49 | form = component.props.form; 50 | }); 51 | 52 | afterEach(() => { 53 | ReactDOM.unmountComponentAtNode(container); 54 | document.body.removeChild(container); 55 | }); 56 | 57 | it('works', (done) => { 58 | async.series([ 59 | (callback) => { 60 | Simulate.change(form.getFieldInstance('async')); 61 | expect(form.getFieldValue('async')).toBe(''); 62 | expect(form.getFieldError('async')).toBeFalsy(); 63 | expect(form.isFieldValidating('async')).toBe(true); 64 | expect(form.isFieldsValidating()).toBe(true); 65 | setTimeout(callback, 200); 66 | }, 67 | (callback) => { 68 | expect(form.getFieldError('async')).toEqual(['must be 1']); 69 | expect(form.isFieldValidating('async')).toBe(false); 70 | expect(form.isFieldsValidating()).toBe(false); 71 | callback(); 72 | }, 73 | (callback) => { 74 | form.getFieldInstance('async').value = '1'; 75 | Simulate.change(form.getFieldInstance('async')); 76 | expect(form.getFieldValue('async')).toBe('1'); 77 | expect(form.getFieldError('async')).toBe(undefined); 78 | expect(form.isFieldValidating('async')).toBe(true); 79 | expect(form.isFieldsValidating()).toBe(true); 80 | setTimeout(callback, 200); 81 | }, 82 | (callback) => { 83 | expect(form.getFieldError('async')).toBeFalsy(); 84 | expect(form.isFieldValidating('async')).toBe(false); 85 | expect(form.isFieldsValidating()).toBe(false); 86 | callback(); 87 | }, 88 | ], done); 89 | }); 90 | it('validateFields works for error', (done) => { 91 | form.validateFields((errors, values) => { 92 | expect(values.normal).toBe(undefined); 93 | expect(values.async).toBe(undefined); 94 | expect(Object.keys(errors).length).toBe(1); 95 | expect(errors.async.errors.map(e => e.message)).toEqual(['must be 1']); 96 | done(); 97 | }); 98 | }); 99 | 100 | it('validateFields works for ok', (done) => { 101 | form.getFieldInstance('async').value = '1'; 102 | Simulate.change(form.getFieldInstance('async')); 103 | form.validateFields((errors, values) => { 104 | expect(values.normal).toBe(undefined); 105 | expect(values.async).toBe('1'); 106 | expect(errors).toBeFalsy(); 107 | done(); 108 | }); 109 | }); 110 | 111 | it('promise validateFields works for ok', (done) => { 112 | form.getFieldInstance('async').value = '1'; 113 | Simulate.change(form.getFieldInstance('async')); 114 | return form.validateFields() 115 | .then(values => { 116 | expect(values.normal).toBe(undefined); 117 | expect(values.async).toBe('1'); 118 | done(); 119 | }); 120 | }); 121 | 122 | it('will error if change when validating', (done) => { 123 | form.validateFields((errors) => { 124 | expect(Object.keys(errors).length).toBe(1); 125 | expect(errors.async.errors.map(e => e.message)).toEqual(['async need to revalidate']); 126 | setTimeout(() => { 127 | done(); 128 | }, 500); 129 | }); 130 | form.getFieldInstance('async').value = '1'; 131 | Simulate.change(form.getFieldInstance('async')); 132 | }); 133 | 134 | it('Whether to catch when an error occurs', (done) => { 135 | form.validateFields() 136 | .catch(({ errors }) => { 137 | expect(Object.keys(errors).length).toBe(1); 138 | expect(errors.async.errors.map(e => e.message)).toEqual(['async need to revalidate']); 139 | expect.assertions(2); 140 | done(); 141 | }); 142 | form.getFieldInstance('async').value = '1'; 143 | Simulate.change(form.getFieldInstance('async')); 144 | }); 145 | 146 | // TODO: submit and isSubmitting are deprecated 147 | it('submit works', (done) => { 148 | expect(form.isSubmitting()).toBe(false); 149 | form.submit((callback) => { 150 | expect(form.isSubmitting()).toBe(true); 151 | setTimeout(() => { 152 | callback(); 153 | expect(form.isSubmitting()).toBe(false); 154 | done(); 155 | }, 100); 156 | }); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /tests/clean-field.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, react/prop-types, react/no-multi-comp, 2 | react/prefer-stateless-function */ 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { mount } from 'enzyme'; 6 | import createDOMForm from '../src/createDOMForm'; 7 | 8 | jest.mock('dom-scroll-into-view', () => jest.fn()); 9 | 10 | describe('clean field', () => { 11 | let div; 12 | 13 | beforeEach(() => { 14 | div = document.createElement('div'); 15 | }); 16 | 17 | afterEach(() => { 18 | ReactDOM.unmountComponentAtNode(div); 19 | }); 20 | 21 | // https://github.com/ant-design/ant-design/issues/12560 22 | describe('clean field if did update removed', () => { 23 | // Prepare 24 | const Popup = ({ visible, children }) => { 25 | if (!visible) return null; 26 | return ( 27 |
28 | {children} 29 |
30 | ); 31 | }; 32 | 33 | class Demo extends React.Component { 34 | render() { 35 | const { init, show } = this.props; 36 | const { getFieldDecorator } = this.props.form; 37 | 38 | let name; 39 | let age; 40 | 41 | if (init) { 42 | name = ( 43 |
44 | name: 45 | {getFieldDecorator('name', { 46 | rules: [{ required: true }], 47 | })()} 48 |
49 | ); 50 | } else { 51 | age = ( 52 |
53 | age: 54 | {getFieldDecorator('age', { 55 | rules: [{ required: true }], 56 | })()} 57 |
58 | ); 59 | } 60 | 61 | return ( 62 | 63 | {name} 64 | {age} 65 | 66 | ); 67 | } 68 | } 69 | 70 | const FormDemo = createDOMForm({ 71 | withRef: true, 72 | })(Demo); 73 | 74 | class Test extends React.Component { 75 | state = { 76 | show: false, 77 | init: false, 78 | }; 79 | 80 | onClick = () => { 81 | this.setState({ show: true }); 82 | this.setState({ init: true }); 83 | }; 84 | 85 | setRef = (demo) => { 86 | this.demo = demo; 87 | }; 88 | 89 | render() { 90 | const { show, init } = this.state; 91 | return ( 92 |
93 | 94 | 95 |
96 | ); 97 | } 98 | } 99 | 100 | // Do the test 101 | it('Remove when mount', () => { 102 | const wrapper = mount(, { attachTo: div }); 103 | const form = wrapper.find('Demo').props().form; 104 | form.validateFields((error) => { 105 | expect(Object.keys(error)).toEqual(['age']); 106 | }); 107 | 108 | wrapper.find('.my-btn').simulate('click'); 109 | 110 | form.validateFields((error) => { 111 | expect(Object.keys(error)).toEqual(['name']); 112 | }); 113 | }); 114 | }); 115 | 116 | // https://github.com/react-component/form/issues/205 117 | describe('Do not clean if field dom exist', () => { 118 | class Demo extends React.Component { 119 | state = { 120 | visible: true, 121 | }; 122 | 123 | componentWillMount() { 124 | this.tmp = this.props.form.getFieldDecorator('age', { 125 | rules: [{ required: true }], 126 | }); 127 | } 128 | 129 | render() { 130 | const { visible } = this.state; 131 | return ( 132 |
133 | {visible && this.tmp()} 134 |
135 | ); 136 | } 137 | } 138 | 139 | const FormDemo = createDOMForm({ 140 | withRef: true, 141 | })(Demo); 142 | 143 | // Do the test 144 | it('Remove when mount', () => { 145 | const wrapper = mount(, { attachTo: div }); 146 | const form = wrapper.find('Demo').props().form; 147 | 148 | // Init 149 | form.validateFields((error) => { 150 | expect(Object.keys(error)).toEqual(['age']); 151 | }); 152 | 153 | // Refresh 154 | wrapper.update(); 155 | form.validateFields((error) => { 156 | expect(Object.keys(error)).toEqual(['age']); 157 | }); 158 | 159 | // Hide 160 | wrapper.find('Demo').setState({ visible: false }); 161 | form.validateFields((error) => { 162 | expect(Object.keys(error || {})).toEqual([]); 163 | }); 164 | }); 165 | }); 166 | 167 | describe('Don\'t clean field when has \'{ preserve: ture }\'', () => { 168 | // Prepare 169 | const Popup = ({ visible, children }) => { 170 | if (!visible) return null; 171 | return ( 172 |
173 | {children} 174 |
175 | ); 176 | }; 177 | 178 | class Demo extends React.Component { 179 | render() { 180 | const { init, show } = this.props; 181 | const { getFieldDecorator } = this.props.form; 182 | 183 | let name; 184 | let age; 185 | 186 | if (init) { 187 | name = ( 188 |
189 | name: 190 | {getFieldDecorator('name', { 191 | rules: [{ required: true }], 192 | })()} 193 |
194 | ); 195 | } else { 196 | age = ( 197 |
198 | age: 199 | {getFieldDecorator('age', { 200 | rules: [{ required: true }], 201 | preserve: true, 202 | })()} 203 |
204 | ); 205 | } 206 | 207 | return ( 208 | 209 | {name} 210 | {age} 211 | 212 | ); 213 | } 214 | } 215 | 216 | const FormDemo = createDOMForm({ 217 | withRef: true, 218 | })(Demo); 219 | 220 | class Test extends React.Component { 221 | state = { 222 | show: false, 223 | init: false, 224 | }; 225 | 226 | onClick = () => { 227 | this.setState({ show: true }); 228 | this.setState({ init: true }); 229 | }; 230 | 231 | setRef = (demo) => { 232 | this.demo = demo; 233 | }; 234 | 235 | render() { 236 | const { show, init } = this.state; 237 | return ( 238 |
239 | 240 | 241 |
242 | ); 243 | } 244 | } 245 | 246 | // Do the test 247 | it('Don\'t remove when mount', done => { 248 | const wrapper = mount(, { attachTo: div }); 249 | const form = wrapper.find('Demo').props().form; 250 | form.validateFields((error) => { 251 | expect(Object.keys(error)).toEqual(['age']); 252 | }); 253 | 254 | wrapper.find('.my-btn').simulate('click'); 255 | 256 | form.validateFields((error) => { 257 | expect(Object.keys(error)).toEqual(['age', 'name']); 258 | done(); 259 | }); 260 | }); 261 | }); 262 | }); 263 | -------------------------------------------------------------------------------- /tests/createDOMForm.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, react/prop-types, react/prefer-stateless-function */ 2 | import React from 'react'; 3 | import { mount } from 'enzyme'; 4 | import scrollIntoView from 'dom-scroll-into-view'; 5 | import createDOMForm from '../src/createDOMForm'; 6 | 7 | jest.mock('dom-scroll-into-view', () => jest.fn()); 8 | 9 | class Test extends React.Component { 10 | render() { 11 | const { form, name } = this.props; 12 | const { getFieldDecorator } = form; 13 | return ( 14 |
15 | {getFieldDecorator(name, { 16 | rules: [{ 17 | required: true, 18 | }], 19 | })(