├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── setupJest.js ├── src ├── Array.test.jsx ├── Button.jsx ├── Down.jsx ├── Element.jsx ├── Fieldset.jsx ├── Fieldset.test.jsx ├── FieldsetIndex.jsx ├── Form.jsx ├── Form.test.jsx ├── If.jsx ├── Input.jsx ├── Input.test.jsx ├── Integrate.jsx ├── Integrate.test.jsx ├── Last.jsx ├── Path.jsx ├── ProvideIndexes.jsx ├── ProvideProps.jsx ├── Remove.jsx ├── Select.jsx ├── Select.test.jsx ├── Tbody.jsx ├── Tbody.test.jsx ├── Textarea.jsx ├── Textarea.test.jsx ├── Up.jsx ├── Value.jsx ├── Word.jsx ├── Word.test.jsx ├── Working.jsx ├── index.js └── utils │ ├── markAsDirty.js │ ├── set.js │ ├── shallowCompare.js │ ├── traverse.js │ └── wait.js └── tempPolyfill.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": [ 5 | ["babel-preset-env", { 6 | "targets": { 7 | "node": "6.10" 8 | } 9 | }], 10 | "stage-0", 11 | "react" 12 | ], 13 | "plugins": [ 14 | "transform-decorators", 15 | "transform-class-properties" 16 | ] 17 | }, 18 | "test": { 19 | "presets": [ 20 | ["babel-preset-env", { 21 | "targets": { 22 | "node": "6.10" 23 | } 24 | }], 25 | "stage-0", 26 | "react" 27 | ], 28 | "plugins": [ 29 | "transform-decorators", 30 | "transform-class-properties" 31 | ] 32 | }, 33 | "browser": { 34 | "presets": [ 35 | ["babel-preset-env", { 36 | "targets": { 37 | "browsers": "last 3 versions, > 1%" 38 | }, 39 | "modules": false, 40 | "loose": true 41 | }], 42 | "stage-0", 43 | "react" 44 | ], 45 | "plugins": [ 46 | "transform-decorators", 47 | "transform-class-properties", 48 | "transform-proto-to-assign" 49 | ] 50 | }, 51 | "module": { 52 | "presets": [ 53 | ["babel-preset-env", { 54 | "targets": { 55 | "node": "6.10" 56 | }, 57 | "modules": false 58 | }], 59 | "stage-0", 60 | "react" 61 | ], 62 | "plugins": [ 63 | "transform-decorators", 64 | "transform-class-properties" 65 | ] 66 | } 67 | }, 68 | "sourceMaps": true 69 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | coverage 4 | tests -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "globals": { 5 | "it": true, 6 | "expect": true, 7 | "describe": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | lib 4 | module 5 | node_modules 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "7" 5 | - "8" 6 | script: npm test:coverage 7 | notifications: 8 | email: 9 | recipients: 10 | - zlatkofedor@cherryprojects.com 11 | on_success: change 12 | on_failure: always 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Zlatko Fedor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React controlled form 2 | 3 | Intuitive react forms for building powerful applications. 4 | 5 | All components are [controlled](https://facebook.github.io/react/docs/forms.html#why-controlled-components) 6 | That means form is always showing the current state and data are immutable. 7 | Each form has own internal state that means you can skip onChange event. 8 | If you will change the value prop of the form component it will change the state of the form immediately. 9 | 10 | [![NPM version][npm-image]][npm-url] 11 | [![build status][travis-image]][travis-url] 12 | [![Test coverage][coveralls-image]][coveralls-url] 13 | 14 | [npm-image]: https://img.shields.io/npm/v/react-form-controlled.svg?style=flat-square 15 | [npm-url]: https://www.npmjs.com/react-form-controlled 16 | [travis-image]: https://img.shields.io/travis/seeden/react-form-controlled/master.svg?style=flat-square 17 | [travis-url]: https://travis-ci.org/seeden/react-form-controlled 18 | [coveralls-image]: https://img.shields.io/coveralls/seeden/react-form-controlled/master.svg?style=flat-square 19 | [coveralls-url]: https://coveralls.io/r/seeden/react-form-controlled?branch=master 20 | [github-url]: https://github.com/seeden/react-form-controlled 21 | 22 | # Features 23 | 24 | - Immutable data 25 | - Controlled behavior (support for "uncontrolled" behaviour) 26 | - Build on latest standards ES6 and promises 27 | - Support for isomorphic application 28 | - You are able to use forms without special components 29 | - Support for arrays/lists and indexes 30 | - Standard html elements like an input, select, textarea and fieldset (arrays) 31 | - Custom components and support for 3rd party libraries 32 | - Validation 33 | - Tests and coverage 34 | 35 | 36 | # Support us 37 | 38 | Star this project on [GitHub][github-url]. 39 | 40 | # Examples 41 | 42 | ## Simple usage 43 | 44 | ```js 45 | import React, { Component } from 'react'; 46 | import Form from 'react-form-controlled'; 47 | 48 | export default class Example extends Component { 49 | constructor(props, context) { 50 | super(props, context); 51 | 52 | this.formData = { 53 | firstName: null, 54 | lastName: null 55 | }; 56 | } 57 | 58 | onSubmit = (data) => { 59 | alert(`Hi ${data.firstName} ${data.lastName}`); 60 | } 61 | 62 | render() { 63 | return ( 64 |
78 | ); 79 | } 80 | } 81 | ``` 82 | 83 | ## Where is the input value? 84 | 85 | Value is automatically added as prop to the inputs. When you will change it it will reload whole form (controlled form, but this is the work for React). 86 | 87 | ## Arrays and controlled state 88 | 89 | ```js 90 | import React, { Component } from 'react'; 91 | import Form from 'react-form-controlled'; 92 | 93 | export default class Example extends Component { 94 | constructor(props, context) { 95 | super(props, context); 96 | 97 | this.state = { 98 | users: [{ 99 | firstName: 'Zlatko' 100 | }, { 101 | firstName: 'Livia' 102 | }] 103 | }; 104 | } 105 | 106 | onChange = (data) => { 107 | this.setState(data); 108 | } 109 | 110 | onSubmit = (data) => { 111 | alert(`Hi ${data.users[0].firstName}`); 112 | } 113 | 114 | render() { 115 | return ( 116 | 129 | ); 130 | } 131 | } 132 | ``` 133 | 134 | ## You can do what do you want with value. 135 | 136 | ```js 137 | import React, { Component } from 'react'; 138 | import Form from 'react-form-controlled'; 139 | 140 | export default class Example extends Component { 141 | constructor(props, context) { 142 | super(props, context); 143 | 144 | this.state = { 145 | users: [{ 146 | firstName: 'Zlatko' 147 | }, { 148 | firstName: 'Livia' 149 | }] 150 | }; 151 | } 152 | 153 | onChange = (data) => { 154 | this.setState(data); 155 | } 156 | 157 | onSubmit = (data) => { 158 | alert(`Hi ${data.users[0].firstName}`); 159 | } 160 | 161 | render() { 162 | return ( 163 | 186 | ); 187 | } 188 | } 189 | ``` 190 | 191 | 192 | ## Simple arrays 193 | 194 | If you are using fieldset with simple array do not enter the name attribute. 195 | 196 | ```js 197 | import React, { Component } from 'react'; 198 | import Form from 'react-form-controlled'; 199 | 200 | export default class Example extends Component { 201 | constructor(props, context) { 202 | super(props, context); 203 | 204 | this.state = { 205 | items: [123, 222] 206 | }; 207 | } 208 | 209 | onSubmit = (data) => { 210 | alert(`Hi ${data.users[0].firstName}`); 211 | } 212 | 213 | render() { 214 | return ( 215 | 225 | ); 226 | } 227 | } 228 | ``` 229 | 230 | ## Complex objects 231 | 232 | If you want to use complex names you can use dot or array notation. 233 | 234 | ```js 235 | import React, { Component } from 'react'; 236 | import Form from 'react-form-controlled'; 237 | 238 | export default class Example extends Component { 239 | constructor(props, context) { 240 | super(props, context); 241 | 242 | this.state = { 243 | users: [{ 244 | firstName: 'Zlatko', 245 | stats: { 246 | followers: 10, 247 | }, 248 | }, { 249 | firstName: 'Livia', 250 | stats: { 251 | followers: 22, 252 | }, 253 | }] 254 | }; 255 | } 256 | 257 | onSubmit = (data) => { 258 | alert(`Hi ${data.users[0].firstName}`); 259 | } 260 | 261 | render() { 262 | return ( 263 | 278 | ); 279 | } 280 | } 281 | ``` 282 | 283 | or you can use one more fieldset 284 | 285 | ```js 286 | import React, { Component } from 'react'; 287 | import Form from 'react-form-controlled'; 288 | 289 | export default class Example extends Component { 290 | constructor(props, context) { 291 | super(props, context); 292 | 293 | this.state = { 294 | users: [{ 295 | firstName: 'Zlatko', 296 | stats: { 297 | followers: 10, 298 | }, 299 | }, { 300 | firstName: 'Livia', 301 | stats: { 302 | followers: 22, 303 | }, 304 | }] 305 | }; 306 | } 307 | 308 | onSubmit = (data) => { 309 | alert(`Hi ${data.users[0].firstName}`); 310 | } 311 | 312 | render() { 313 | return ( 314 | 331 | ); 332 | } 333 | } 334 | ``` 335 | 336 | ## Indexes 337 | 338 | If you are using arrays with fieldset you want to use indexes. 339 | 340 | ### Props 341 | 342 | #### render: function 343 | 344 | Instead of having a component rendered for you, you can pass in a function. Your render function will be called with the same props that are passed to the component. 345 | 346 | ```js 347 | import React, { Component } from 'react'; 348 | import Form, { Index } from 'react-form-controlled'; 349 | 350 | export default class Component extends Component { 351 | constructor(props, context) { 352 | super(props, context); 353 | 354 | this.state = { 355 | users: [{ 356 | firstName: 'Zlatko', 357 | }, { 358 | firstName: 'Livia', 359 | }] 360 | }; 361 | } 362 | 363 | onSubmit = (data) => { 364 | alert(`Hi ${data.users[0].firstName}`); 365 | } 366 | 367 | render() { 368 | return ( 369 | 386 | ); 387 | } 388 | } 389 | ``` 390 | 391 | ## Parent values 392 | 393 | You can use value from parent with dot notation. Example ".selected" 394 | 395 | ```js 396 | import React, { Component } from 'react'; 397 | import Form, { Index } from 'react-form-controlled'; 398 | 399 | export default class Component extends Component { 400 | constructor(props, context) { 401 | super(props, context); 402 | 403 | this.state = { 404 | options: ['dog', 'mouse', 'cat'], 405 | selected: 1, 406 | }; 407 | } 408 | 409 | onSubmit = (data) => { 410 | alert(`Selected option is ${data.options[data.selected]}`); 411 | } 412 | 413 | render() { 414 | return ( 415 | 430 | ); 431 | } 432 | } 433 | ``` 434 | 435 | ## Integrate with 3rd party libraries 436 | 437 | Integration is very easy you can use Integrate component. Here is example with [react-select](https://github.com/JedWatson/react-select) library. 438 | ### Props 439 | #### value: string 440 | Name of the integrated value property. 441 | #### onChange: function 442 | OnChange callback of the integrated component. 443 | #### name: string 444 | Name of the state property. You can use standard dot notation as always :) 445 | 446 | ```js 447 | import React, { Component } from 'react'; 448 | import Form, { Integrate } from 'react-form-controlled'; 449 | import Select from 'react-select'; 450 | 451 | export default class Component extends Component { 452 | onSubmit = (data) => { 453 | alert(`Selected option is ${data.selected}`); 454 | } 455 | 456 | render() { 457 | const options = [ 458 | { value: 'one', label: 'One' }, 459 | { value: 'two', label: 'Two' } 460 | ]; 461 | 462 | return ( 463 | 475 | ); 476 | } 477 | } 478 | ``` 479 | 480 | ## Remove item from array 481 | 482 | ```js 483 | import React, { Component } from 'react'; 484 | import Form, { Remove } from 'react-form-controlled'; 485 | 486 | export default class Component extends Component { 487 | constructor(props, context) { 488 | super(props, context); 489 | 490 | this.state = { 491 | users: [{ 492 | firstName: 'Zlatko', 493 | }, { 494 | firstName: 'Livia', 495 | }] 496 | }; 497 | } 498 | 499 | onSubmit = (data) => { 500 | alert(`Hi ${data.users[0].firstName}`); 501 | } 502 | 503 | render() { 504 | return ( 505 | 522 | ); 523 | } 524 | } 525 | ``` 526 | Remove, Up and Down components has same properties like index. You can use render and component property as well. 527 | 528 | ## Move item up/down in array 529 | 530 | ```js 531 | import React, { Component } from 'react'; 532 | import Form, { Up, Down } from 'react-form-controlled'; 533 | 534 | export default class Component extends Component { 535 | constructor(props, context) { 536 | super(props, context); 537 | 538 | this.state = { 539 | users: [{ 540 | firstName: 'Zlatko', 541 | }, { 542 | firstName: 'Livia', 543 | }] 544 | }; 545 | } 546 | 547 | onSubmit = (data) => { 548 | alert(`Hi ${data.users[0].firstName}`); 549 | } 550 | 551 | render() { 552 | return ( 553 | 573 | ); 574 | } 575 | } 576 | ``` 577 | 578 | ## Working state 579 | 580 | You can simply handle working state and show loading indicator. Form property onSubmit is based on promises. During you processing of this callback is form in the "isWorking" state. 581 | If the form is in the isWorking state you are not able to submit form again. 582 | 583 | ```js 584 | import React, { Component } from 'react'; 585 | import Form, { Working } from 'react-form-controlled'; 586 | 587 | export default class Component extends Component { 588 | constructor(props, context) { 589 | super(props, context); 590 | 591 | this.state = { 592 | users: [{ 593 | firstName: 'Zlatko', 594 | }, { 595 | firstName: 'Livia', 596 | }] 597 | }; 598 | } 599 | 600 | onSubmit = async (data) => { 601 | return new Promise((resolve) => { 602 | alert(`Hi ${data.users[0].firstName}`); 603 | 604 | setTimeout(resolve, 3000); 605 | }); 606 | } 607 | 608 | render() { 609 | return ( 610 | 623 | ); 624 | } 625 | } 626 | ``` 627 | 628 | ## So far so good (more complex form) 629 | 630 | Try to image simple quiz with questions and answers. Y 631 | 632 | ## Combination with other components 633 | 634 | If you want to disable autoreplace of the standard components like an input, select, textarea etc... 635 | You can disable this behavior with the form parameter skipReplace. 636 | This feature is great if you want to use this library with other 3rd libraries. 637 | You will be able to use Input, Select, Textarea and Fieldset. 638 | 639 | ```js 640 | import Form, { Input, Select, Textarea, Fieldset } from from 'react-form-controlled'; 641 | 642 | export default class Component extends Component { 643 | constructor(props, context) { 644 | super(props, context); 645 | 646 | this.state = { 647 | users: [{ 648 | firstName: 'Zlatko', 649 | }, { 650 | firstName: 'Livia', 651 | }] 652 | }; 653 | } 654 | 655 | onSubmit(data) { 656 | alert(`Hi ${data.users[0].firstName}`); 657 | } 658 | 659 | render() { 660 | return ( 661 | 675 | ); 676 | } 677 | } 678 | ``` 679 | 680 | # Support for schemas and validation? 681 | 682 | This part is moved to another library named react-form-controlled-validate 683 | 684 | Yes, you can use JSON schema as property to the form. Why JSON schema? Because it is a standard. 685 | ```js 686 | 687 | const schema = { 688 | type: "object", 689 | properties: { 690 | firstName: { 691 | type: "string" 692 | }, 693 | lastName: { 694 | type: "string" 695 | } 696 | } 697 | }; 698 | 699 |