├── .babelrc ├── .eslintrc.json ├── .flowconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── bundle ├── 1.react-material-ui-form.min.js └── react-material-ui-form.min.js ├── dist ├── components │ ├── CheckableFieldClone.js │ ├── DeleteFieldRowButton.js │ ├── FieldClone.js │ ├── Form.js │ ├── FormControlClone.js │ └── FormControlLabelClone.js ├── index.js ├── propNames.js └── validation │ ├── constants.js │ ├── index.js │ ├── messageMap.js │ └── validators │ ├── index.js │ ├── isAlias.js │ ├── isDate.js │ ├── isNumber.js │ ├── isRequired.js │ ├── isSerial.js │ ├── isSize.js │ └── isTime.js ├── examples ├── Root.js ├── index.html ├── markdown.css ├── pages │ ├── CustomValidateFunction.js │ ├── CustomValidationMessages.js │ ├── CustomValidators.js │ ├── DynamicArrayFields.js │ ├── MiscProps.js │ ├── NestedFields.js │ └── Steppers.js ├── styles.css └── styles.js ├── package-lock.json ├── package.json ├── setupTests.js ├── src ├── components │ ├── CheckableFieldClone.js │ ├── DeleteFieldRowButton.js │ ├── FieldClone.js │ ├── Form.js │ ├── FormControlClone.js │ ├── FormControlLabelClone.js │ └── __tests__ │ │ ├── CheckableFieldClone.test.js │ │ ├── FieldClone.test.js │ │ ├── Form.test.js │ │ ├── FormControlClone.test.js │ │ ├── FormControlLabelClone.test.js │ │ └── __snapshots__ │ │ ├── CheckableFieldClone.test.js.snap │ │ ├── FieldClone.test.js.snap │ │ ├── Form.test.js.snap │ │ ├── FormControlClone.test.js.snap │ │ └── FormControlLabelClone.test.js.snap ├── index.js ├── propNames.js └── validation │ ├── __tests__ │ └── index.test.js │ ├── constants.js │ ├── index.js │ ├── messageMap.js │ └── validators │ ├── __tests__ │ ├── isAlias.test.js │ ├── isDate.test.js │ ├── isNumber.test.js │ ├── isRequired.test.js │ ├── isSerial.test.js │ ├── isSize.test.js │ └── isTime.test.js │ ├── index.js │ ├── isAlias.js │ ├── isDate.js │ ├── isNumber.js │ ├── isRequired.js │ ├── isSerial.js │ ├── isSize.js │ └── isTime.js ├── tools └── windows-safe-jest.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "flow", "react", "stage-0"], 3 | "plugins": ["transform-decorators-legacy"], 4 | "env": { 5 | "production": { 6 | "ignore": [ 7 | "**/__tests__" 8 | ] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "airbnb", 5 | "plugin:import/errors", 6 | "plugin:import/warnings" 7 | ], 8 | "parser": "babel-eslint", 9 | "env": { 10 | "amd": true, 11 | "browser": true, 12 | "commonjs": true, 13 | "es6": true, 14 | "node": true 15 | }, 16 | "parserOptions": { 17 | "ecmaVersion": 6, 18 | "sourceType": "module", 19 | "codeFrame": true, 20 | "ecmaFeatures": { 21 | "jsx": true 22 | } 23 | }, 24 | "plugins": [ 25 | "react", 26 | "jsx-a11y", 27 | "import", 28 | "filenames" 29 | ], 30 | "rules": { 31 | "class-methods-use-this": 0, 32 | "comma-dangle": [2, "always-multiline"], 33 | "constructor-super": 1, 34 | "eol-last": [2, "always"], 35 | "filenames/match-exported": [2, [null]], 36 | "function-paren-newline": [2, "consistent"], 37 | "import/default": 2, 38 | "import/export": 2, 39 | "import/extensions": 0, 40 | "import/named": 2, 41 | "import/namespace": 2, 42 | "import/no-unresolved": 1, 43 | "import/prefer-default-export": 0, 44 | "import/unambiguous": 0, 45 | "indent": [2, 2], 46 | "jsx-a11y/anchor-is-valid": 0, 47 | "linebreak-style": [2, "unix"], 48 | "max-len": [1, { 49 | "code": 85, 50 | "ignoreComments": true, 51 | "tabWidth": 2, 52 | "ignoreRegExpLiterals": true 53 | }], 54 | "no-await-in-loop": 1, 55 | "no-const-assign": 1, 56 | "no-continue": 1, 57 | "no-multi-str": 0, 58 | "no-param-reassign": 0, 59 | "no-plusplus": [0, { "allowForLoopAfterthoughts": true }], 60 | "no-restricted-syntax": [2, "BinaryExpression[operator='of']"], 61 | "no-this-before-super": 1, 62 | "no-undef": 1, 63 | "no-underscore-dangle": 0, 64 | "no-unreachable": 1, 65 | "no-unused-expressions": 1, 66 | "no-unused-vars": [1, { "argsIgnorePattern": "^_" }], 67 | "object-curly-newline": [2, { "minProperties": 5, "consistent": true }], 68 | "prefer-template": 0, 69 | "quotes": [2, "single"], 70 | "react/forbid-prop-types": [2, { "forbid": ["any"] }], 71 | "react/forbid-prop-types": [2, { "forbid": ["any"]}], 72 | "react/jsx-filename-extension": 0, 73 | "react/jsx-indent": [2, 2], 74 | "react/jsx-indent-props": [2, 2], 75 | "react/jsx-no-bind": 0, 76 | "react/jsx-wrap-multilines": 1, 77 | "react/no-multi-comp": 0, 78 | "react/prefer-stateless-function": 1, 79 | "semi": 0, 80 | "space-in-parens": 0 81 | }, 82 | "globals": { 83 | "_": true, 84 | "describe": true, 85 | "expect": true, 86 | "it": true, 87 | "jest": true, 88 | "SyntheticInputEvent": true 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/webpack-cli/.* 3 | 4 | [include] 5 | 6 | [libs] 7 | 8 | [lints] 9 | 10 | [options] 11 | 12 | [strict] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.sublime-project 3 | *.sublime-workspace 4 | *~ 5 | .DS_Store 6 | .vscode 7 | coverage 8 | logs 9 | node_modules 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .travis.yml 3 | __tests__ 4 | __snapshots__ 5 | bundle 6 | eslintrc.json 7 | examples 8 | setupTests.js 9 | src 10 | webpack.config.js 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | install: 5 | - npm install -g codecov 6 | - npm install 7 | after_success: 8 | - codecov 9 | notifications: 10 | email: false 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Swaroop 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### react-material-ui-form 2 | 3 | [![MIT](https://img.shields.io/github/license/mashape/apistatus.svg)](https://opensource.org/licenses/MIT) 4 | [![npm](https://img.shields.io/npm/v/react-material-ui-form.svg)](https://www.npmjs.com/package/react-material-ui-form) 5 | [![BuildStatus](https://travis-ci.com/voletiswaroop/react-material-ui-form.svg)](https://travis-ci.com/voletiswaroop/react-material-ui-form) 6 | [![Downloads](https://img.shields.io/npm/dt/react-material-ui-form.svg)](https://www.npmjs.com/package/react-material-ui-form) 7 | [![SourceRank](https://img.shields.io/librariesio/sourcerank/npm/react-material-ui-form?color=green)](https://libraries.io/npm/react-material-ui-form) 8 | 9 | 1. [About](#about) 10 | 2. [Setup](#setup) 11 | 3. [Props](#props) 12 | - [Form props](#form-props-optional) 13 | - [Field props](#field-props) 14 | - [Other props](#other-props) 15 | 4. [Examples](#examples) 16 | - [Nested fields](#nested-fields) 17 | - [Custom validation messages](#custom-validation-messages) 18 | - [Custom validators](#custom-validators) 19 | - [Custom validation logic](#custom-validation-logic) 20 | - [Server validations](#server-validations) 21 | - [Misc form settings](#form-autocomplete-and-on-error-submission) 22 | - [Getting values on field update](#getting-form-values-on-field-update) 23 | - [Stepper](#stepper) 24 | - [Dynamic array fields](#dynamic-array-fields-notice-the-deletefieldrow-prop-on-the-remove-row-button) 25 | - [Custom component handler](#custom-components-with-custom-handlers) 26 | 5. [Contributing](#contributing) 27 | 6. [License](#license) 28 | 29 | ## About 30 | 31 | _react-material-ui-form_ is a React wrapper for [Material-UI](https://material-ui.com/getting-started/usage/) form components. Simply replace the `
` element with `` to get out-of-the-box state and validation support ***as-is***. There's no need to use any other components, alter your form's nesting structure, or write onChange handlers. 32 | 33 | Validation is done with [validator.js](https://github.com/chriso/validator.js) but you can extend/customize validation messages, validators, and use your own validation logic too. Steppers, dynamic array fields and custom components are also supported. 34 | 35 | #### use and requirements 36 | 37 | - requires React 16.3.0 or newer 38 | - supports official and unofficial Material-UI fields (other input elements are rendered without state/validation support) 39 | - every input field must have `value` and `name` props 40 | - every input field should NOT have `onChange` and `onBlur` props (unless you need custom field-specific logic) 41 | - add a `data-validators` prop to any input field (or FormControl / FormControlLabel) to specify validation rules 42 | 43 | #### extra validators 44 | 45 | _react-material-ui-form_ extends [_validator.js_ validators](https://github.com/chriso/validator.js#validators) with the following validators: 46 | 47 | - isAlias `/^[a-zA-Z0-9-_\.]*$/i` 48 | - isDate 49 | - isNumber `/^([,.\d]+)$/` 50 | - isRequired `value.length !== 0` 51 | - isSerial `/^([-\s\da-zA-Z]+)$/` 52 | - isSize `value >= min && value <= max` 53 | - isTime 54 | - [isLength(min,max)](#custom-validation-messages) `[{ isLength: { min: 2, max: 50 } }]` 55 | 56 | #### _Supported field components_ 57 | 58 | - TextField 59 | - TextField { select } 60 | - TextField { multiline, textarea } 61 | - [Checkbox](#nested-fields) 62 | - RadioGroup 63 | - Radio 64 | - FormControlLabel (control prop) 65 | - FormLabel 66 | - InputLabel 67 | 68 | ## Note 69 | Currently this package will support only till **[Material-UI v3](https://v3.material-ui.com/getting-started/installation/)** 70 | 71 | ## Setup 72 | 73 | #### install 74 | ``` 75 | npm install --save react-material-ui-form 76 | ``` 77 | 78 | #### demo 79 | 1. `$ git clone https://github.com/voletiswaroop/react-material-ui-form.git` 80 | 2. `$ cd react-material-ui-form` 81 | 3. `$ npm install && npm run dev` 82 | 83 | ## Props 84 | 85 | #### Form props (optional): 86 | 87 | Prop | Description | Default 88 | ------------------------------| --------------------------|------------ 89 | [***class***] _[string]_ | Sets `className` attribute to the form | 90 | [***id***] _[string]_ | Sets `id` attribute to the form | 91 | [***name***] _[string]_ | Sets `name` attribute to the form | 92 | [***action***] _[string]_ | Sets `action` attribute to the form | 93 | [***activeStep***](#stepper) _[number]_ | Use together with `onFieldValidation` for better Stepper support | 94 | [***autoComplete***](#form-autocomplete-and-on-error-submission) _[string]_ | Sets form _autoComplete_ prop. Accepts one of ["on", "off"] | "off" 95 | [***disableSubmitButtonOnError***](#form-autocomplete-and-on-error-submission) _[boolean]_ | Disables submit button if any errors exist | true 96 | [***onFieldValidation***](#stepper) _[func]_ | Returns _@field_ and _@errorSteps_ (if `activeStep` prop is provided) on field validation | 97 | [***onSubmit***](#nested-fields) _[func]_ | Returns _@values_ and _@pristineValues_ on form submission | 98 | [***onValuesChange***](#getting-form-values-on-field-update) _[func]_ | Returns _@values_ and _@pristineValues_ on field value change | 99 | ***validation*** _[object]_ | Object specifing validation config options (prefixed below with ↳) | 100 | ↳ [***messageMap***](#custom-validation-messages) _[object]_ | A key-value list where the key is the validator name and the value is the error message. Is exposed as a _react-material-ui-form_ export parameter | _object_ 101 | ↳ [***messageKeyPrefix***](#custom-validation-messages) _[string]_ | Optional prefix to apply to all messageMap keys. If specified, field validator names will automatically be appended the prefix | "" 102 | ↳ [***requiredValidatorName***](#custom-validation-logic) _[boolean, string]_ | Specifies the validator name and matching messegeMap key for required fields. To disable and rely on the native _required_ field prop, set to `false` | "isRequired" 103 | ↳ [***validate***](#custom-validation-logic) _[func]_ | Overrides the internal validate method. Receives the following parameters: _@fieldValue_, _@fieldValidators_, and _@...rest_ (where _@...rest_ is the **validation** prop object) | _func_ 104 | ↳ [***validators***](#custom-validators) _[object]_ | Defaults to an extended validator.js object. Is exposed as a _react-material-ui-form_ export parameter | _object_ 105 | ↳ ***validateInputOnBlur*** _[boolean]_ | Makes text input validations happen on blur instead of on change | false 106 | [***validations***](#server-validations) _[object]_ | Validations to pass to the form (i.e. from the server). Should be an object with keys representing field _name_ props and values as arrays of field error messages. The first error message will be displayed per field | 107 | 108 | #### Field props: 109 | 110 | Prop | Description | Required 111 | ------------------------------| --------------------------|------------ 112 | ***value*** _[any]_ | The value of the field. If empty set an empty string | Yes 113 | ***name*** _[string]_ | The name of the field | Yes 114 | ***data-validators*** _[string, array[object]]_ | Validators to apply to the field. Multiple validator names can be specified with a comma-delimited string | 115 | ***onBlur*** _[func]_ | A custom handler that will be called after the field's `onBlur` event. Provides _@value/checked_, _@field_ and _@event_ parameters | 116 | ***onChange*** _[func]_ | A custom handler that will be called after the field's `onChange` event. Provides _@value/checked_, _@field_ and _@event_ parameters | 117 | 118 | #### Other props: 119 | 120 | Prop | Value | Description 121 | -------------------------| ------------------|------------------------ 122 | [***deletefieldrow***](#dynamic-array-fields-notice-the-deletefieldrow-prop-on-the-remove-row-button) _[string]_ | Field **name** prop up to and including the row index (i.e. _rooms[2]_) | Add to button components that use _onClick_ to remove any array field rows 123 | 124 | ## Material-UI form production build classnames conflict issues 125 | To avoid default material-ui production build classnames conflict issues include your entire form inside 126 | Example: [Nested fields](#nested-fields) 127 | 128 | ## Examples 129 | 130 | #### Nested fields: 131 | ```jsx 132 | import MaterialUIForm from 'react-material-ui-form' 133 | import JssProvider from 'react-jss/lib/JssProvider'; 134 | 135 | class MyForm extends React.Component { 136 | submit = (values, pristineValues) => { 137 | // get all values and pristineValues on form submission 138 | } 139 | 140 | customInputHandler = (value, { name }, event) => { 141 | // the form will update the field as usual, and then call this handler 142 | // if you want to have complete control of the field, change the "value" prop to "defaultValue" 143 | } 144 | 145 | customToggleHandler = (checked, { name, value }, event) => { 146 | // the form will update the field as usual, and then call this handler 147 | // if you want to have complete control of the field, change the "value" prop to "defaultValue" 148 | } 149 | 150 | render() { 151 | return ( 152 | 153 | 154 | 155 |
156 | 157 | {/* form label is required here to perform default validations */} 158 | I love React material UI form 159 | 160 | } 161 | label='I love React material UI form'/> 162 | 163 | 164 | 165 | 166 | Age 167 | 174 | Some important helper text 175 | 176 |
177 | 178 | 179 | Gender 180 | 181 | } label="Female" /> 182 | } label="Male" /> 183 | 184 | 185 | 186 | 187 | 188 |
189 |
190 | ) 191 | } 192 | } 193 | ``` 194 | 195 | #### Custom validators: 196 | ```jsx 197 | import Form, { messageMap, validators } from 'react-material-ui-form/dist/validation/index'; 198 | 199 | 200 | validators.isBorat = value => value === 'borat' 201 | const customMessageMap = Object.assign(messageMap, { 202 | isBorat: 'NAAAAAT! You can only write "borat" lol', 203 | }) 204 | 205 | class MyForm extends React.Component { 206 | submit = (values, pristineValues) => { 207 | // get all values and pristineValues on form submission 208 | } 209 | 210 | render() { 211 | return ( 212 | 213 | 214 | 215 | 216 | 217 | ) 218 | } 219 | } 220 | 221 | ``` 222 | 223 | #### Custom validation messages: 224 | ```jsx 225 | const customFormMsg = Object.assign(messageMap, { 226 | isEmail: 'Please enter a valid email address', 227 | isLength:'Must be 2-50 characters', 228 | }) 229 | class MyForm extends React.Component { 230 | submit = (values, pristineValues) => { 231 | // get all values and pristineValues on form submission 232 | } 233 | render() { 234 | return ( 235 | 236 | 237 | 238 | 239 | 240 | ) 241 | } 242 | } 243 | 244 | ``` 245 | 246 | #### Custom validation logic: 247 | ```jsx 248 | import MaterialUIForm from 'react-material-ui-form' 249 | 250 | 251 | function validate(value, fieldValidators, options) { 252 | const fieldValidations = [] 253 | fieldValidators.forEach((validator) => { 254 | const validation = { 255 | code: String(validator), 256 | message: 'its invalid so maybe try harder...', 257 | } 258 | if (_.has(options, 'genericMessage')) { 259 | validation.message = options.genericMessage 260 | } 261 | fieldValidations.push(validation) 262 | }) 263 | return fieldValidations 264 | } 265 | 266 | const validationOptions = { 267 | genericMessage: 'yeah... *tisk*', 268 | } 269 | 270 | class MyForm extends React.Component { 271 | submit = (values, pristineValues) => { 272 | // get all values and pristineValues on form submission 273 | } 274 | 275 | render() { 276 | return ( 277 | 278 | 279 | 280 | 281 | 282 | ) 283 | } 284 | } 285 | ``` 286 | 287 | #### Server validations: 288 | ```jsx 289 | import MaterialUIForm from 'react-material-ui-form' 290 | 291 | 292 | const mockServerValidations = { 293 | name: [{ code: 'isInvalid', message: 'such invalid...' }], 294 | } 295 | 296 | class MyForm extends React.Component { 297 | state = { 298 | mockServerValidations, 299 | } 300 | 301 | componentDidMount() { 302 | let validations = { 303 | name: [{ message: 'such WOOOOOOOOOW...' }], 304 | } 305 | 306 | setTimeout(() => { 307 | this.setState({ mockServerValidations: validations }) 308 | }, 1500) 309 | 310 | setTimeout(() => { 311 | validations = { 312 | name: [{ message: 'so still haven\'t watched Italian Spiderman?' }], 313 | } 314 | this.setState({ mockServerValidations: validations }) 315 | }, 3000) 316 | } 317 | 318 | submit = (values, pristineValues) => { 319 | // get all values and pristineValues on form submission 320 | } 321 | 322 | render() { 323 | return ( 324 | 325 | 326 | 327 | 328 | 329 | ) 330 | } 331 | } 332 | ``` 333 | 334 | #### Form autoComplete and "on error" submission: 335 | ```jsx 336 | import MaterialUIForm from 'react-material-ui-form' 337 | 338 | 339 | class MyForm extends React.Component { 340 | submit = (values, pristineValues) => { 341 | // get all values and pristineValues on form submission 342 | } 343 | 344 | render() { 345 | return ( 346 | 347 | 348 | 349 | 350 | 351 | ) 352 | } 353 | } 354 | ``` 355 | 356 | #### Getting form values on field update: 357 | ```jsx 358 | import MaterialUIForm from 'react-material-ui-form' 359 | 360 | 361 | class MyForm extends React.Component { 362 | handleValuesChange = (values, pristineValues) => { 363 | // get all values and pristineValues when any field updates 364 | } 365 | 366 | handleFieldValidations = (field) => { 367 | // get field object when its validation status updates 368 | } 369 | 370 | submit = (values, pristineValues) => { 371 | // get all values and pristineValues on form submission 372 | } 373 | 374 | render() { 375 | return ( 376 | 377 | 378 | 379 | 380 | 381 | ) 382 | } 383 | } 384 | ``` 385 | 386 | #### Stepper: 387 | ```jsx 388 | import Stepper, { Step, StepLabel } from 'material-ui/Stepper' 389 | import MaterialUIForm from 'react-material-ui-form' 390 | 391 | 392 | function getSteps() { 393 | return [ 394 | 'Step 1', 395 | 'Step 2', 396 | ] 397 | } 398 | 399 | class MyForm extends React.Component { 400 | state = { 401 | activeStep: 0, 402 | errorSteps: [], 403 | } 404 | 405 | clickNext = () => { 406 | this.setState({ 407 | activeStep: this.state.activeStep + 1, 408 | }) 409 | } 410 | 411 | clickBack = () => { 412 | this.setState({ 413 | activeStep: this.state.activeStep - 1, 414 | }) 415 | } 416 | 417 | submit = (values, pristineValues) => { 418 | // get all values and pristineValues on form submission 419 | } 420 | 421 | updateErrorSteps = (field, errorSteps) => { 422 | this.setState({ errorSteps }) 423 | } 424 | 425 | render() { 426 | const steps = getSteps() 427 | const { activeStep } = this.state 428 | 429 | return ( 430 |
431 | 432 | {steps.map((label, i) => ( 433 | 434 | 435 | {label} 436 | 437 | 438 | ))} 439 | 440 | 441 | 442 | {activeStep === 0 && 443 | 444 | 445 | 446 | 447 | } 448 | 449 | {activeStep === 1 && 450 | 451 | 452 | 453 | 454 | 455 | } 456 | 457 |
458 | ) 459 | } 460 | } 461 | ``` 462 | 463 | #### Dynamic array fields (notice the `deletefieldrow` prop on the "Remove Row" button): 464 | ```jsx 465 | import MaterialUIForm from 'react-material-ui-form' 466 | import formData from 'form-data-to-object' 467 | 468 | 469 | class MyForm extends React.Component { 470 | state = { 471 | rows: [{ _id: _.uniqueId() }], 472 | onSubmitValues: null, 473 | } 474 | 475 | addRow = () => { 476 | const { rows } = this.state 477 | rows.push({ _id: _.uniqueId() }) 478 | this.setState({ rows }) 479 | } 480 | 481 | removeRow = (index) => { 482 | const { rows } = this.state 483 | if (rows.length > 1) { 484 | rows.splice(index, 1) 485 | this.setState({ rows }) 486 | } 487 | } 488 | 489 | submit = (values, pristineValues) => { 490 | // you can parse values to turn: 491 | // rows[0][label]: "label" 492 | // into: 493 | // rows: [{ label: "label" }] 494 | const parsedValues = formData.toObj(values) 495 | } 496 | 497 | render() { 498 | const steps = getSteps() 499 | 500 | return ( 501 | 502 | {this.state.rows.map((row, i) => ( 503 | 504 | 505 | 506 | { this.state.rows.length > 1 && 507 | 508 | } 509 | 510 | ))} 511 | 512 | 513 | 514 | 515 | ) 516 | } 517 | } 518 | ``` 519 | 520 | #### Custom components with custom handlers: 521 | ```jsx 522 | import MaterialUIForm from 'react-material-ui-form' 523 | 524 | 525 | class MyForm extends React.Component { 526 | uploadFile = (event) => { 527 | console.log(event.target.files) 528 | } 529 | 530 | render() { 531 | return ( 532 |
533 | 534 | {'Upload file: '} 535 | 536 | 539 | 540 |
541 | ) 542 | } 543 | } 544 | ``` 545 | 546 | ## Contributing 547 | 548 | As this is a new project, contributions are most welcome. Feel free to clone the repo and extend the module. If you find any bugs please feel free to raise a bug [open an issue](https://github.com/voletiswaroop/react-material-ui-form/issues). Collaborators are also welcome - please send an email to voleti.swaroop@gmail.com. 549 | 550 | ## License 551 | 552 | This project is licensed under the terms of the [MIT license](https://github.com/voletiswaroop/react-material-ui-form/blob/dev/LICENSE). 553 | -------------------------------------------------------------------------------- /dist/components/CheckableFieldClone.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = undefined; 7 | 8 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 9 | 10 | var _class, _temp, _initialiseProps; 11 | 12 | var _react = require('react'); 13 | 14 | var _react2 = _interopRequireDefault(_react); 15 | 16 | var _Checkbox = require('@material-ui/core/Checkbox'); 17 | 18 | var _Checkbox2 = _interopRequireDefault(_Checkbox); 19 | 20 | var _Switch = require('@material-ui/core/Switch'); 21 | 22 | var _Switch2 = _interopRequireDefault(_Switch); 23 | 24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 25 | 26 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 27 | 28 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 29 | 30 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 31 | 32 | var CheckableFieldClone = (_temp = _class = function (_React$Component) { 33 | _inherits(CheckableFieldClone, _React$Component); 34 | 35 | function CheckableFieldClone(props) { 36 | _classCallCheck(this, CheckableFieldClone); 37 | 38 | var _this = _possibleConstructorReturn(this, (CheckableFieldClone.__proto__ || Object.getPrototypeOf(CheckableFieldClone)).call(this, props)); 39 | 40 | _initialiseProps.call(_this); 41 | 42 | var fieldComp = props.fieldComp; 43 | 44 | 45 | if (![_Checkbox2.default, _Switch2.default].includes(fieldComp.type)) { 46 | throw new Error('CheckableFieldClone should be a Checkbox or Switch'); 47 | } 48 | if (fieldComp.props.name === undefined || fieldComp.props.value === undefined) { 49 | throw new Error('CheckableFieldClone name and value must be defined'); 50 | } 51 | 52 | var checked = props.field.value; 53 | if (props.field.value === undefined) { 54 | checked = fieldComp.props.checked || false; 55 | _this.props.onConstruct(fieldComp.props); 56 | } 57 | _this.state = { checked: checked }; 58 | return _this; 59 | } 60 | 61 | _createClass(CheckableFieldClone, [{ 62 | key: 'render', 63 | value: function render() { 64 | var fieldComp = this.props.fieldComp; 65 | 66 | return _react2.default.cloneElement(fieldComp, { 67 | value: fieldComp.props.value, 68 | checked: this.state.checked, 69 | onChange: this.onToggle 70 | }); 71 | } 72 | }]); 73 | 74 | return CheckableFieldClone; 75 | }(_react2.default.Component), _class.defaultProps = { 76 | field: {} 77 | }, _initialiseProps = function _initialiseProps() { 78 | var _this2 = this; 79 | 80 | this.onToggle = function (event, checked) { 81 | var _props = _this2.props, 82 | fieldComp = _props.fieldComp, 83 | _props$fieldComp$prop = _props.fieldComp.props, 84 | name = _props$fieldComp$prop.name, 85 | value = _props$fieldComp$prop.value; 86 | 87 | _this2.setState({ checked: checked }); 88 | _this2.props.onToggle(name, value, checked); 89 | if (fieldComp.props.onChange !== undefined) { 90 | fieldComp.props.onChange(checked, { name: name, value: value }, event); 91 | } 92 | }; 93 | }, _temp); 94 | exports.default = CheckableFieldClone; -------------------------------------------------------------------------------- /dist/components/DeleteFieldRowButton.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = undefined; 7 | 8 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 9 | 10 | var _class, _temp, _initialiseProps; 11 | 12 | var _react = require('react'); 13 | 14 | var _react2 = _interopRequireDefault(_react); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 19 | 20 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 21 | 22 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 23 | 24 | var DeleteFieldRowButton = (_temp = _class = function (_React$Component) { 25 | _inherits(DeleteFieldRowButton, _React$Component); 26 | 27 | function DeleteFieldRowButton(props) { 28 | _classCallCheck(this, DeleteFieldRowButton); 29 | 30 | var _this = _possibleConstructorReturn(this, (DeleteFieldRowButton.__proto__ || Object.getPrototypeOf(DeleteFieldRowButton)).call(this, props)); 31 | 32 | _initialiseProps.call(_this); 33 | 34 | var deletefieldrow = _this.props.buttonComp.props.deletefieldrow; 35 | 36 | if (deletefieldrow === undefined) { 37 | throw new Error('DeleteFieldRowButton element requires "deletefieldrow" prop'); 38 | } 39 | if (deletefieldrow.match(/\w+\[\d+\]/) === null) { 40 | throw new Error('"deletefieldrow" prop should match /\\w+\\[\\d+\\]/'); 41 | } 42 | return _this; 43 | } 44 | 45 | _createClass(DeleteFieldRowButton, [{ 46 | key: 'render', 47 | value: function render() { 48 | var buttonComp = this.props.buttonComp; 49 | 50 | return _react2.default.cloneElement(buttonComp, { 51 | onClick: this.onClick 52 | }); 53 | } 54 | }]); 55 | 56 | return DeleteFieldRowButton; 57 | }(_react2.default.Component), _initialiseProps = function _initialiseProps() { 58 | var _this2 = this; 59 | 60 | this.onClick = function () { 61 | var _props = _this2.props, 62 | onRequestRowDelete = _props.onRequestRowDelete, 63 | _props$buttonComp$pro = _props.buttonComp.props, 64 | onClick = _props$buttonComp$pro.onClick, 65 | deletefieldrow = _props$buttonComp$pro.deletefieldrow; 66 | 67 | 68 | onRequestRowDelete(deletefieldrow); 69 | onClick(); 70 | }; 71 | }, _temp); 72 | exports.default = DeleteFieldRowButton; -------------------------------------------------------------------------------- /dist/components/FieldClone.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = undefined; 7 | 8 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 9 | 10 | var _class, _temp, _initialiseProps; 11 | 12 | var _react = require('react'); 13 | 14 | var _react2 = _interopRequireDefault(_react); 15 | 16 | var _lodash = require('lodash'); 17 | 18 | var _lodash2 = _interopRequireDefault(_lodash); 19 | 20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 21 | 22 | function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } 23 | 24 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 25 | 26 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 27 | 28 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 29 | 30 | function getRequiredProp(required, useNativeRequiredValidator) { 31 | if (!useNativeRequiredValidator) { 32 | return false; 33 | } 34 | return required || false; 35 | } 36 | 37 | function makeLabel(fieldComp, props) { 38 | var label = fieldComp.props.label || ''; 39 | return props.field.isRequired && !props.useNativeRequiredValidator ? '' + label : label; 40 | } 41 | 42 | function makeErrorAndHelperText(props) { 43 | var helperText = _lodash2.default.get(props.fieldComp.props, 'helperText'); 44 | var isError = false; 45 | 46 | if (!_lodash2.default.isEmpty(props.field) && props.field.validations.length > 0) { 47 | helperText = props.field.validations[0].message; 48 | isError = true; 49 | } 50 | return { helperText: helperText, isError: isError }; 51 | } 52 | 53 | var FieldClone = (_temp = _class = function (_React$Component) { 54 | _inherits(FieldClone, _React$Component); 55 | 56 | function FieldClone(props) { 57 | _classCallCheck(this, FieldClone); 58 | 59 | var _this = _possibleConstructorReturn(this, (FieldClone.__proto__ || Object.getPrototypeOf(FieldClone)).call(this, props)); 60 | 61 | _initialiseProps.call(_this); 62 | 63 | var fieldComp = props.fieldComp; 64 | 65 | if (fieldComp.type.name === undefined || fieldComp.type.options && fieldComp.type.options.name === undefined) { 66 | throw new Error('FieldClone does not support native elements'); 67 | } 68 | if (fieldComp.props.name === undefined || fieldComp.props.value === undefined) { 69 | throw new Error('FieldClone name and value must be defined'); 70 | } 71 | 72 | var value = _lodash2.default.isEmpty(props.field) ? fieldComp.props.value : props.field.value; 73 | 74 | var _makeErrorAndHelperTe = makeErrorAndHelperText(props), 75 | helperText = _makeErrorAndHelperTe.helperText, 76 | isError = _makeErrorAndHelperTe.isError; 77 | 78 | _this.state = { 79 | helperText: helperText, 80 | isError: isError, 81 | value: value 82 | }; 83 | 84 | if (props.field.value === undefined) { 85 | _this.props.onConstruct(fieldComp.props); 86 | } 87 | return _this; 88 | } 89 | 90 | _createClass(FieldClone, [{ 91 | key: 'render', 92 | value: function render() { 93 | var _props = this.props, 94 | fieldComp = _props.fieldComp, 95 | props = _objectWithoutProperties(_props, ['fieldComp']); 96 | 97 | return _react2.default.cloneElement(fieldComp, { 98 | value: this.state.value, 99 | label: makeLabel(fieldComp, props), 100 | error: this.state.isError, 101 | helperText: this.state.helperText, 102 | onBlur: this.onBlur, 103 | onChange: this.onChange, 104 | required: getRequiredProp(fieldComp.props.required, this.props.useNativeRequiredValidator) 105 | }); 106 | } 107 | }], [{ 108 | key: 'getDerivedStateFromProps', 109 | value: function getDerivedStateFromProps(nextProps) { 110 | if (!_lodash2.default.isEmpty(nextProps.field)) { 111 | var _makeErrorAndHelperTe2 = makeErrorAndHelperText(nextProps), 112 | _helperText = _makeErrorAndHelperTe2.helperText, 113 | _isError = _makeErrorAndHelperTe2.isError; 114 | 115 | return { 116 | helperText: _helperText, 117 | isError: _isError, 118 | value: nextProps.field.value 119 | }; 120 | } 121 | return null; 122 | } 123 | }]); 124 | 125 | return FieldClone; 126 | }(_react2.default.Component), _class.defaultProps = { 127 | field: {} 128 | }, _initialiseProps = function _initialiseProps() { 129 | var _this2 = this; 130 | 131 | this.onBlur = function (event) { 132 | var _props2 = _this2.props, 133 | isDirty = _props2.field.isDirty, 134 | fieldComp = _props2.fieldComp, 135 | name = _props2.fieldComp.props.name, 136 | validateInputOnBlur = _props2.validateInputOnBlur; 137 | var value = event.target.value; 138 | // // /* TODO: create function for condition */ 139 | 140 | if ((!isDirty || validateInputOnBlur) && !fieldComp.props.select) { 141 | _this2.props.onValueChange(name, value, true); 142 | } 143 | if (fieldComp.props.onBlur !== undefined) { 144 | fieldComp.props.onBlur(value, { name: name }, event); 145 | } 146 | }; 147 | 148 | this.onChange = function (event) { 149 | var _props3 = _this2.props, 150 | fieldComp = _props3.fieldComp, 151 | name = _props3.fieldComp.props.name, 152 | validateInputOnBlur = _props3.validateInputOnBlur; 153 | var value = event.target.value; 154 | 155 | if (fieldComp.props.select || validateInputOnBlur) { 156 | var _helperText2 = _lodash2.default.get(fieldComp.props, 'helperText'); 157 | _this2.setState({ isError: false, helperText: _helperText2, value: value }); 158 | } 159 | /* TODO: create function for condition */ 160 | if (!validateInputOnBlur || fieldComp.props.select) { 161 | _this2.props.onValueChange(name, value, fieldComp.props.select); 162 | } 163 | if (fieldComp.props.onChange !== undefined) { 164 | fieldComp.props.onChange(value, { name: name }, event); 165 | } 166 | }; 167 | }, _temp); 168 | exports.default = FieldClone; -------------------------------------------------------------------------------- /dist/components/Form.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = undefined; 7 | 8 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 9 | 10 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 11 | 12 | var _class, _temp; 13 | 14 | var _react = require('react'); 15 | 16 | var _react2 = _interopRequireDefault(_react); 17 | 18 | var _lodash = require('lodash'); 19 | 20 | var _lodash2 = _interopRequireDefault(_lodash); 21 | 22 | var _FormControl = require('@material-ui/core/FormControl'); 23 | 24 | var _FormControl2 = _interopRequireDefault(_FormControl); 25 | 26 | var _FormControlLabel = require('@material-ui/core/FormControlLabel'); 27 | 28 | var _FormControlLabel2 = _interopRequireDefault(_FormControlLabel); 29 | 30 | var _FormHelperText = require('@material-ui/core/FormHelperText'); 31 | 32 | var _FormHelperText2 = _interopRequireDefault(_FormHelperText); 33 | 34 | var _FormLabel = require('@material-ui/core/FormLabel'); 35 | 36 | var _FormLabel2 = _interopRequireDefault(_FormLabel); 37 | 38 | var _InputLabel = require('@material-ui/core/InputLabel'); 39 | 40 | var _InputLabel2 = _interopRequireDefault(_InputLabel); 41 | 42 | var _Checkbox = require('@material-ui/core/Checkbox'); 43 | 44 | var _Checkbox2 = _interopRequireDefault(_Checkbox); 45 | 46 | var _Switch = require('@material-ui/core/Switch'); 47 | 48 | var _Switch2 = _interopRequireDefault(_Switch); 49 | 50 | var _FormControlClone = require('./FormControlClone'); 51 | 52 | var _FormControlClone2 = _interopRequireDefault(_FormControlClone); 53 | 54 | var _FormControlLabelClone = require('./FormControlLabelClone'); 55 | 56 | var _FormControlLabelClone2 = _interopRequireDefault(_FormControlLabelClone); 57 | 58 | var _FieldClone = require('./FieldClone'); 59 | 60 | var _FieldClone2 = _interopRequireDefault(_FieldClone); 61 | 62 | var _CheckableFieldClone = require('./CheckableFieldClone'); 63 | 64 | var _CheckableFieldClone2 = _interopRequireDefault(_CheckableFieldClone); 65 | 66 | var _DeleteFieldRowButton = require('./DeleteFieldRowButton'); 67 | 68 | var _DeleteFieldRowButton2 = _interopRequireDefault(_DeleteFieldRowButton); 69 | 70 | var _propNames = require('../propNames'); 71 | 72 | var _propNames2 = _interopRequireDefault(_propNames); 73 | 74 | var _validation2 = require('../validation'); 75 | 76 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 77 | 78 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 79 | 80 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 81 | 82 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 83 | 84 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 85 | 86 | function verifyFieldElement(component) { 87 | var whitelist = [_FormControlLabel2.default]; 88 | 89 | return whitelist.includes(component.type) || _lodash2.default.has(component, 'props.name') && _lodash2.default.has(component, 'props.value'); 90 | } 91 | 92 | function extractFieldValidators(fieldProps) { 93 | var validators = _lodash2.default.get(fieldProps, _propNames2.default.FIELD_VALIDATORS); 94 | if (validators !== undefined) { 95 | if (_lodash2.default.isString(validators)) { 96 | validators = validators.replace(/\s/g, '').split(','); 97 | } else if (!_lodash2.default.isArray(validators)) { 98 | validators = [validators]; 99 | } 100 | return validators; 101 | } 102 | return []; 103 | } 104 | 105 | function getFieldValues(fields) { 106 | var values = {}; 107 | _lodash2.default.each(fields, function (field, name) { 108 | if (_lodash2.default.get(field, 'checked') !== false) { 109 | values[name] = field.value; 110 | } 111 | }); 112 | return values; 113 | } 114 | 115 | function getPristineFieldValues(fields) { 116 | var values = {}; 117 | _lodash2.default.each(fields, function (field, name) { 118 | if (!field.isPristine && _lodash2.default.get(field, 'checked') !== false) { 119 | values[name] = field.pristineValue; 120 | } 121 | }); 122 | return values; 123 | } 124 | 125 | function getFieldTemplate() { 126 | return { 127 | isDirty: false, 128 | isPristine: true, 129 | isRequired: null, 130 | pristineValue: null, 131 | step: undefined, 132 | validations: [], 133 | validators: [], 134 | value: undefined 135 | }; 136 | } 137 | 138 | function deriveErrorSteps(fields) { 139 | var errorSteps = []; 140 | _lodash2.default.each(fields, function (field) { 141 | if (field.validations.length > 0 && !errorSteps.includes(field.step)) { 142 | errorSteps.push(field.step); 143 | } 144 | }); 145 | return errorSteps; 146 | } 147 | 148 | function isValidForm(fields) { 149 | return _lodash2.default.size(_lodash2.default.filter(fields, function (field) { 150 | return field.validations.length > 0; 151 | })) === 0; 152 | } 153 | 154 | var Form = (_temp = _class = function (_React$Component) { 155 | _inherits(Form, _React$Component); 156 | 157 | function Form(props) { 158 | _classCallCheck(this, Form); 159 | 160 | var _this = _possibleConstructorReturn(this, (Form.__proto__ || Object.getPrototypeOf(Form)).call(this, props)); 161 | 162 | _this.validation = { 163 | messageMap: _validation2.messageMap, 164 | messageMapKeyPrefix: '', 165 | requiredValidatorName: _validation2.constants.REQUIRED_VALIDATOR_NAME, 166 | validators: _validation2.validators, 167 | validate: _validation2.validate, 168 | validateInputOnBlur: false 169 | }; 170 | 171 | _this.onFieldConstruct = function (fieldProps) { 172 | var checked = fieldProps.checked, 173 | name = fieldProps.name, 174 | required = fieldProps.required, 175 | value = fieldProps.value; 176 | 177 | // checkable input 178 | 179 | if (checked === true) { 180 | _lodash2.default.defer(function () { 181 | _this.setState({ 182 | fields: _extends({}, _this.state.fields, _defineProperty({}, name, _extends({}, getFieldTemplate(), { 183 | checked: checked || false, 184 | step: _this.props.activeStep, 185 | value: value 186 | }))) 187 | }); 188 | }); 189 | // other inputs 190 | } else if (!_lodash2.default.isBoolean(checked)) { 191 | var _requiredValidatorName = _this.validation.requiredValidatorName; 192 | 193 | if (!_lodash2.default.has(_this.state.fields, name)) { 194 | var _validators = extractFieldValidators(fieldProps); 195 | 196 | if (required && !_lodash2.default.isEmpty(_requiredValidatorName)) { 197 | _validators.unshift(_requiredValidatorName); 198 | } 199 | var isRequired = required || _validators.includes(_requiredValidatorName); 200 | // set any validations on first construct 201 | var _validations = []; 202 | if (!_lodash2.default.has(_this.state.fields, name) && _lodash2.default.has(_this.props.validations, name)) { 203 | _validations = _this.props.validations[name]; 204 | } 205 | 206 | _lodash2.default.defer(function () { 207 | _this.setState({ 208 | fields: _extends({}, _this.state.fields, _defineProperty({}, name, _extends({}, getFieldTemplate(), { 209 | isRequired: isRequired, 210 | pristineValue: value, 211 | step: _this.props.activeStep, 212 | validators: _validators, 213 | validations: _validations, 214 | value: value 215 | }))) 216 | }); 217 | 218 | if (!_lodash2.default.isEmpty(value)) { 219 | _this.validateField(name, value); 220 | } 221 | }); 222 | } 223 | } 224 | }; 225 | 226 | _this.onFieldValueChange = function (name, value) { 227 | var isDirty = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; 228 | 229 | _lodash2.default.defer(function () { 230 | _this.setState({ 231 | fields: _extends({}, _this.state.fields, _defineProperty({}, name, _extends({}, _this.state.fields[name], { 232 | isDirty: isDirty || _this.state.fields[name].isDirty, 233 | isPristine: false, 234 | validations: [], 235 | value: value 236 | }))) 237 | }); 238 | 239 | if (isValidForm(_this.state.fields)) { 240 | _this.enableSubmitButton(); 241 | } 242 | 243 | if (_this.onValuesChange !== undefined) { 244 | _this.onValuesChange(getFieldValues(_this.state.fields), getPristineFieldValues(_this.state.fields)); 245 | } 246 | 247 | if (_this.state.fields[name].isDirty) { 248 | _this.validateField(name, value); 249 | } 250 | }); 251 | }; 252 | 253 | _this.onFieldToggle = function (name, value, checked) { 254 | _this.setState({ 255 | fields: _extends({}, _this.state.fields, _defineProperty({}, name, _extends({}, _this.state.fields[name], { 256 | checked: checked, 257 | isPristine: false, 258 | validations: [], 259 | value: value 260 | }))) 261 | }); 262 | }; 263 | 264 | _this.validateField = function (name, value) { 265 | var field = _this.state.fields[name]; 266 | 267 | if (!(field.value === '' && !field.isRequired) && !_lodash2.default.isEmpty(field.validators)) { 268 | var _validation = _this.validation; 269 | 270 | var _validations2 = _validation.validate(value, field.validators, _validation); 271 | 272 | // update state 273 | field.validations = _validations2; 274 | _this.setState({ 275 | fields: _extends({}, _this.state.fields, _defineProperty({}, name, field)) 276 | }); 277 | // disable submit button 278 | if (!_lodash2.default.isEmpty(_validations2)) { 279 | _this.disableSubmitButton(); 280 | } 281 | // propogate validation 282 | if (_this.props.onFieldValidation !== undefined) { 283 | var errorSteps = void 0; 284 | if (field.step !== undefined) { 285 | errorSteps = deriveErrorSteps(_this.state.fields); 286 | } 287 | _this.props.onFieldValidation(field, errorSteps); 288 | } 289 | } 290 | }; 291 | 292 | _this.reset = function () { 293 | var fields = _this.state.fields; 294 | 295 | _lodash2.default.defer(function () { 296 | _lodash2.default.each(fields, function (field, name) { 297 | _this.setState({ 298 | fields: _extends({}, _this.state.fields, _defineProperty({}, name, _extends({}, _this.state.fields[name], { 299 | isDirty: false, 300 | isPristine: true, 301 | value: '' 302 | }))) 303 | }); 304 | }); 305 | }); 306 | }; 307 | 308 | _this.submit = function (event) { 309 | event.preventDefault(); 310 | var isValid = true; 311 | var fields = _this.state.fields; 312 | 313 | 314 | _lodash2.default.each(fields, function (field, name) { 315 | if (field.isRequired && field.value === '') { 316 | _this.validateField(name, ''); 317 | isValid = false; 318 | } 319 | }); 320 | if (isValid) { 321 | _this.props.onSubmit(getFieldValues(fields), getPristineFieldValues(fields)); 322 | } 323 | }; 324 | 325 | _this.deleteRow = function (row) { 326 | var pos = row.indexOf('['); 327 | var rowName = row.substr(0, pos); 328 | var rowIndex = parseInt(row.substr(pos + 1), 10); 329 | 330 | var fields = _this.state.fields; 331 | 332 | _lodash2.default.each(fields, function (field, fieldName) { 333 | if (fieldName.startsWith(row)) { 334 | delete fields[fieldName]; 335 | } else if (fieldName.startsWith(rowName)) { 336 | var index = parseInt(fieldName.substr(pos + 1), 10); 337 | if (index > rowIndex) { 338 | var newRow = fieldName.replace(/\[\d+\]/, '[' + (index - 1) + ']'); 339 | delete fields[fieldName]; 340 | fields[newRow] = field; 341 | } 342 | } 343 | }); 344 | 345 | _this.setState({ fields: fields }); 346 | }; 347 | 348 | _this.onValuesChange = props.onValuesChange; 349 | _this.validation = Object.assign(_this.validation, props.validation); 350 | _this.state = { 351 | disableSubmitButton: false, 352 | fields: {} 353 | }; 354 | return _this; 355 | } 356 | 357 | // eslint-disable-next-line react/sort-comp 358 | 359 | 360 | _createClass(Form, [{ 361 | key: 'enableSubmitButton', 362 | value: function enableSubmitButton() { 363 | if (this.state.disableSubmitButton) { 364 | this.setState({ disableSubmitButton: false }); 365 | } 366 | } 367 | }, { 368 | key: 'disableSubmitButton', 369 | value: function disableSubmitButton() { 370 | if (this.props.disableSubmitButtonOnError) { 371 | this.setState({ disableSubmitButton: true }); 372 | } 373 | } 374 | }, { 375 | key: 'cloneChildrenRecursively', 376 | value: function cloneChildrenRecursively(children) { 377 | var _this2 = this; 378 | 379 | return _react2.default.Children.map(children, function (child) { 380 | if (_lodash2.default.isEmpty(child)) { 381 | return null; 382 | } 383 | if (_lodash2.default.isString(child)) { 384 | return child; 385 | } 386 | 387 | var isFieldElement = verifyFieldElement(child); 388 | var nestedChildren = _lodash2.default.isArray(child.props.children) && !isFieldElement ? _lodash2.default.filter(child.props.children, function (v) { 389 | return _lodash2.default.isObject(v) || _lodash2.default.isString(v); 390 | }) : false; 391 | 392 | // nested elements 393 | if (nestedChildren !== false) { 394 | // FormControl element with field/group name-value props 395 | if (child.type === _FormControl2.default) { 396 | var fieldElement = nestedChildren.find(function (el) { 397 | return ![_FormLabel2.default, _InputLabel2.default, _FormHelperText2.default].includes(el.type) && el.props.name !== undefined && el.props.value !== undefined; 398 | }); 399 | if (fieldElement !== undefined) { 400 | var _name = fieldElement.props.name; 401 | 402 | return _react2.default.createElement(_FormControlClone2.default, { 403 | key: _name, 404 | field: _this2.state.fields[_name], 405 | formControlComp: child, 406 | onConstruct: _this2.onFieldConstruct, 407 | onValueChange: _this2.onFieldValueChange 408 | }); 409 | } 410 | } 411 | // non-FormControl element 412 | return _react2.default.cloneElement(child, { 413 | children: _this2.cloneChildrenRecursively(nestedChildren) 414 | }); 415 | } 416 | // add disable functionality to submit button 417 | if (child.props.type === 'submit') { 418 | return _react2.default.cloneElement(child, { 419 | disabled: _this2.state.disableSubmitButton 420 | }); 421 | // non-interactive elements should be rendered as is 422 | } else if (!isFieldElement) { 423 | // delete row button 424 | if (child.props[_propNames2.default.DELETE_FIELD_ROW] !== undefined) { 425 | return _react2.default.createElement(_DeleteFieldRowButton2.default, { 426 | buttonComp: child, 427 | onRequestRowDelete: _this2.deleteRow 428 | }); 429 | } 430 | // any other element 431 | return child; 432 | } 433 | // clone control label 434 | if (child.type === _FormControlLabel2.default) { 435 | var _name2 = child.props.control.props.name; 436 | 437 | return _react2.default.createElement(_FormControlLabelClone2.default, { 438 | key: _name2, 439 | field: _this2.state.fields[_name2], 440 | control: child.props.control, 441 | label: child.props.label, 442 | onConstruct: _this2.onFieldConstruct, 443 | onToggle: _this2.onFieldToggle 444 | }); 445 | } 446 | // clone input element 447 | var name = child.props.name; 448 | 449 | // checkable 450 | 451 | if (child.type === _Checkbox2.default || child.type === _Switch2.default) { 452 | return _react2.default.createElement(_CheckableFieldClone2.default, { 453 | key: name, 454 | field: _this2.state.fields[name], 455 | fieldComp: child, 456 | onConstruct: _this2.onFieldConstruct, 457 | onToggle: _this2.onFieldToggle 458 | }); 459 | } 460 | 461 | return _react2.default.createElement(_FieldClone2.default, { 462 | key: name, 463 | field: _this2.state.fields[name], 464 | fieldComp: child, 465 | onConstruct: _this2.onFieldConstruct, 466 | onValueChange: _this2.onFieldValueChange, 467 | useNativeRequiredValidator: !_this2.validation.requiredValidatorName, 468 | validateInputOnBlur: _this2.validation.validateInputOnBlur 469 | }); 470 | }); 471 | } 472 | }, { 473 | key: 'render', 474 | value: function render() { 475 | return _react2.default.createElement( 476 | 'form', 477 | { 478 | autoComplete: this.props.autoComplete, 479 | className: this.props.className, 480 | onReset: this.reset, 481 | onSubmit: this.submit, 482 | style: this.props.style, 483 | id: this.props.id, 484 | method: this.props.method, 485 | action: this.props.action, 486 | name: this.props.name 487 | }, 488 | this.cloneChildrenRecursively(this.props.children) 489 | ); 490 | } 491 | }], [{ 492 | key: 'getDerivedStateFromProps', 493 | value: function getDerivedStateFromProps(nextProps, prevState) { 494 | var fields = prevState.fields; 495 | 496 | 497 | if (!_lodash2.default.isEmpty(fields)) { 498 | // add validations to fields 499 | _lodash2.default.each(nextProps.validations, function (validations, name) { 500 | if (_lodash2.default.has(fields, name)) { 501 | fields[name].validations = validations; 502 | } else { 503 | // eslint-disable-next-line no-console 504 | console.warn('validations field "' + name + '" does not exist'); 505 | } 506 | }); 507 | return { fields: fields }; 508 | } 509 | return null; 510 | } 511 | }]); 512 | 513 | return Form; 514 | }(_react2.default.Component), _class.defaultProps = { 515 | activeStep: 0, 516 | autoComplete: 'off', 517 | className: undefined, 518 | disableSubmitButtonOnError: true, 519 | onFieldValidation: undefined, 520 | onValuesChange: undefined, 521 | style: {}, 522 | validation: {}, 523 | validations: {}, 524 | id: undefined, 525 | method: undefined, 526 | action: undefined, 527 | name: undefined }, _temp); 528 | exports.default = Form; -------------------------------------------------------------------------------- /dist/components/FormControlClone.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = undefined; 7 | 8 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 9 | 10 | var _class, _temp, _initialiseProps; 11 | 12 | var _react = require('react'); 13 | 14 | var _react2 = _interopRequireDefault(_react); 15 | 16 | var _lodash = require('lodash'); 17 | 18 | var _lodash2 = _interopRequireDefault(_lodash); 19 | 20 | var _FormControl = require('@material-ui/core/FormControl'); 21 | 22 | var _FormControl2 = _interopRequireDefault(_FormControl); 23 | 24 | var _FormHelperText = require('@material-ui/core/FormHelperText'); 25 | 26 | var _FormHelperText2 = _interopRequireDefault(_FormHelperText); 27 | 28 | var _FormLabel = require('@material-ui/core/FormLabel'); 29 | 30 | var _FormLabel2 = _interopRequireDefault(_FormLabel); 31 | 32 | var _InputLabel = require('@material-ui/core/InputLabel'); 33 | 34 | var _InputLabel2 = _interopRequireDefault(_InputLabel); 35 | 36 | var _propNames = require('../propNames'); 37 | 38 | var _propNames2 = _interopRequireDefault(_propNames); 39 | 40 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 41 | 42 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 43 | 44 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 45 | 46 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 47 | 48 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 49 | 50 | function getErrorAndHelperText(field) { 51 | var helperText = void 0; 52 | var isError = false; 53 | if (!_lodash2.default.isEmpty(field) && field.validations.length > 0) { 54 | helperText = field.validations[0].message; 55 | isError = true; 56 | } 57 | return { helperText: helperText, isError: isError }; 58 | } 59 | 60 | var FormControlClone = (_temp = _class = function (_React$Component) { 61 | _inherits(FormControlClone, _React$Component); 62 | 63 | // eslint-disable-next-line react/sort-comp 64 | function FormControlClone(props) { 65 | _classCallCheck(this, FormControlClone); 66 | 67 | var _this = _possibleConstructorReturn(this, (FormControlClone.__proto__ || Object.getPrototypeOf(FormControlClone)).call(this, props)); 68 | 69 | _initialiseProps.call(_this); 70 | 71 | var _props$formControlCom = props.formControlComp.props, 72 | error = _props$formControlCom.error, 73 | required = _props$formControlCom.required; 74 | 75 | 76 | var name = void 0; 77 | var value = void 0; 78 | var helperText = void 0; 79 | var isError = error; 80 | 81 | _react2.default.Children.forEach(props.formControlComp.props.children, function (child) { 82 | if (child.type === _FormHelperText2.default) { 83 | helperText = String(child.props.children); 84 | _this.helperText = helperText; 85 | } else if (child.type !== _FormLabel2.default && child.type !== _InputLabel2.default && child.props.name !== undefined && child.props.value !== undefined) { 86 | name = child.props.name; // eslint-disable-line prefer-destructuring 87 | value = child.props.value; // eslint-disable-line prefer-destructuring 88 | } 89 | }); 90 | 91 | if (props.formControlComp.type !== _FormControl2.default || name === undefined || value === undefined) { 92 | throw new Error('invalid FormControl control children'); 93 | } 94 | 95 | if (props.field.value === undefined) { 96 | var validatorsPropName = _propNames2.default.FIELD_VALIDATORS; 97 | props.onConstruct(_defineProperty({ 98 | name: name, 99 | value: value, 100 | required: required 101 | }, validatorsPropName, props.formControlComp.props[validatorsPropName])); 102 | } else { 103 | value = props.field.value; // eslint-disable-line prefer-destructuring 104 | if (!_lodash2.default.isEmpty(props.field) && props.field.validations.length > 0) { 105 | var fieldError = getErrorAndHelperText(props.field); 106 | helperText = fieldError.helperText; // eslint-disable-line prefer-destructuring 107 | isError = fieldError.isError; // eslint-disable-line prefer-destructuring 108 | } 109 | } 110 | 111 | _this.name = name; 112 | _this.state = { 113 | helperText: helperText, 114 | isError: isError, 115 | value: value 116 | }; 117 | return _this; 118 | } 119 | 120 | // eslint-disable-next-line 121 | 122 | 123 | _createClass(FormControlClone, [{ 124 | key: 'UNSAFE_componentWillReceiveProps', 125 | value: function UNSAFE_componentWillReceiveProps(nextProps) { 126 | if (!_lodash2.default.isEmpty(nextProps.field)) { 127 | var _getErrorAndHelperTex = getErrorAndHelperText(nextProps.field), 128 | _helperText = _getErrorAndHelperTex.helperText, 129 | _isError = _getErrorAndHelperTex.isError; 130 | 131 | this.setState({ 132 | helperText: _helperText, 133 | isError: _isError, 134 | value: nextProps.field.value 135 | }); 136 | } 137 | } 138 | }, { 139 | key: 'render', 140 | value: function render() { 141 | var _this2 = this; 142 | 143 | var _props = this.props, 144 | formControlComp = _props.formControlComp, 145 | props = _props.formControlComp.props; 146 | 147 | 148 | var hasHelperText = false; 149 | var children = _react2.default.Children.map(props.children, function (child) { 150 | // label 151 | if (child.type === _FormLabel2.default || child.type === _InputLabel2.default) { 152 | return child; 153 | } 154 | // helper text 155 | if (child.type === _FormHelperText2.default) { 156 | hasHelperText = true; 157 | return _react2.default.cloneElement(child, { 158 | children: _this2.state.helperText 159 | }); 160 | } 161 | // field 162 | return _react2.default.cloneElement(child, { 163 | onChange: child.props.onChange || _this2.onChange, 164 | value: _this2.state.value 165 | }); 166 | }); 167 | // support for dynamic helper text 168 | if (!hasHelperText && this.state.helperText !== undefined) { 169 | children.push(_react2.default.createElement( 170 | _FormHelperText2.default, 171 | { key: 1 }, 172 | this.state.helperText 173 | )); 174 | } 175 | 176 | return _react2.default.cloneElement(formControlComp, { 177 | error: this.state.isError, 178 | children: children 179 | }); 180 | } 181 | }]); 182 | 183 | return FormControlClone; 184 | }(_react2.default.Component), _class.defaultProps = { 185 | field: {} }, _initialiseProps = function _initialiseProps() { 186 | var _this3 = this; 187 | 188 | this.onChange = function (event) { 189 | var value = event.target.value; 190 | 191 | _this3.setState({ isError: false, helperText: _this3.helperText, value: value }); 192 | _this3.props.onValueChange(_this3.name, value, true); 193 | }; 194 | }, _temp); 195 | exports.default = FormControlClone; -------------------------------------------------------------------------------- /dist/components/FormControlLabelClone.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = undefined; 7 | 8 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 9 | 10 | var _class, _temp, _initialiseProps; 11 | 12 | var _react = require('react'); 13 | 14 | var _react2 = _interopRequireDefault(_react); 15 | 16 | var _lodash = require('lodash'); 17 | 18 | var _lodash2 = _interopRequireDefault(_lodash); 19 | 20 | var _Checkbox = require('@material-ui/core/Checkbox'); 21 | 22 | var _Checkbox2 = _interopRequireDefault(_Checkbox); 23 | 24 | var _Switch = require('@material-ui/core/Switch'); 25 | 26 | var _Switch2 = _interopRequireDefault(_Switch); 27 | 28 | var _FormControlLabel = require('@material-ui/core/FormControlLabel'); 29 | 30 | var _FormControlLabel2 = _interopRequireDefault(_FormControlLabel); 31 | 32 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 33 | 34 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 35 | 36 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 37 | 38 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 39 | 40 | var FormControlLabelClone = (_temp = _class = function (_React$Component) { 41 | _inherits(FormControlLabelClone, _React$Component); 42 | 43 | function FormControlLabelClone(props) { 44 | _classCallCheck(this, FormControlLabelClone); 45 | 46 | var _this = _possibleConstructorReturn(this, (FormControlLabelClone.__proto__ || Object.getPrototypeOf(FormControlLabelClone)).call(this, props)); 47 | 48 | _initialiseProps.call(_this); 49 | 50 | if (![_Checkbox2.default, _Switch2.default].includes(props.control.type)) { 51 | throw new Error('invalid FormControlLabel control component'); 52 | } 53 | 54 | var checked = props.control.props.checked; 55 | var value = props.control.props.value; 56 | 57 | 58 | if (props.field.value === undefined) { 59 | props.onConstruct(props.control.props); 60 | } else { 61 | checked = _lodash2.default.get(props.field, 'checked'); 62 | } 63 | 64 | _this.state = { 65 | checked: checked, 66 | value: value 67 | }; 68 | return _this; 69 | } 70 | 71 | _createClass(FormControlLabelClone, [{ 72 | key: 'render', 73 | value: function render() { 74 | var _props = this.props, 75 | control = _props.control, 76 | label = _props.label; 77 | 78 | var onChange = control.props.onChange || control.props.onToggle || this.onToggle; 79 | var controlOptions = { 80 | checked: this.state.checked, 81 | onChange: onChange, 82 | value: this.state.value 83 | }; 84 | 85 | return _react2.default.createElement(_FormControlLabel2.default, { 86 | checked: this.state.checked, 87 | control: _react2.default.cloneElement(control, controlOptions), 88 | onChange: onChange, 89 | label: label, 90 | value: this.state.value 91 | }); 92 | } 93 | }]); 94 | 95 | return FormControlLabelClone; 96 | }(_react2.default.Component), _class.defaultProps = { 97 | field: {} 98 | }, _initialiseProps = function _initialiseProps() { 99 | var _this2 = this; 100 | 101 | this.onToggle = function (event, checked) { 102 | checked = _lodash2.default.get(event, 'target.checked') || checked; 103 | var value = _this2.props.control.props.value; // eslint-disable-line react/prop-types 104 | 105 | var name = _this2.props.control.props.name; // eslint-disable-line react/prop-types 106 | 107 | value = checked ? value : ''; 108 | _this2.setState({ checked: checked, value: value }); 109 | _this2.props.onToggle(name, value, checked); 110 | }; 111 | }, _temp); 112 | exports.default = FormControlLabelClone; -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = undefined; 7 | 8 | var _validation = require('./validation'); 9 | 10 | Object.keys(_validation).forEach(function (key) { 11 | if (key === "default" || key === "__esModule") return; 12 | Object.defineProperty(exports, key, { 13 | enumerable: true, 14 | get: function get() { 15 | return _validation[key]; 16 | } 17 | }); 18 | }); 19 | 20 | var _Form = require('./components/Form'); 21 | 22 | var _Form2 = _interopRequireDefault(_Form); 23 | 24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 25 | 26 | exports.default = _Form2.default; -------------------------------------------------------------------------------- /dist/propNames.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = { 7 | ACTIVE_STEP: 'activestep', 8 | DELETE_FIELD_ROW: 'deletefieldrow', 9 | FIELD_VALIDATORS: 'data-validators' 10 | }; -------------------------------------------------------------------------------- /dist/validation/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = { 7 | REQUIRED_VALIDATOR_NAME: 'isRequired' 8 | }; -------------------------------------------------------------------------------- /dist/validation/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.validators = exports.messageMap = exports.constants = exports.validate = exports.createValidation = undefined; 7 | 8 | var _lodash = require('lodash'); 9 | 10 | var _lodash2 = _interopRequireDefault(_lodash); 11 | 12 | var _messageMap = require('./messageMap'); 13 | 14 | var _messageMap2 = _interopRequireDefault(_messageMap); 15 | 16 | var _validators = require('./validators'); 17 | 18 | var _validators2 = _interopRequireDefault(_validators); 19 | 20 | var _constants = require('./constants'); 21 | 22 | var _constants2 = _interopRequireDefault(_constants); 23 | 24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 25 | 26 | function sprintf(str, args) { 27 | var predicate = void 0; 28 | if (_lodash2.default.isString(args)) { 29 | predicate = args; 30 | } else if (_lodash2.default.isObject(args) && !_lodash2.default.isArray(args)) { 31 | args = Object.values(args); 32 | predicate = function predicate(match, number) { 33 | return args[number] !== undefined ? args[number] : match; 34 | }; 35 | } else { 36 | predicate = function predicate(match, number) { 37 | return ( 38 | // eslint-disable-next-line no-nested-ternary 39 | args[number] !== undefined ? _lodash2.default.isArray(args) ? args.join(', ') : args[number] : match 40 | ); 41 | }; 42 | } 43 | return str.replace(/{(\d+)}/g, predicate); 44 | } 45 | 46 | var validationMessageMap = _lodash2.default.clone(_messageMap2.default); 47 | 48 | var createValidation = exports.createValidation = function createValidation(validatorName, args, config) { 49 | if (!_lodash2.default.isEmpty(config.messageMap)) { 50 | validationMessageMap = config.messageMap; 51 | } 52 | 53 | var code = validatorName; 54 | // first check if prefix code exists 55 | if (!validatorName.startsWith(config.messageMapKeyPrefix)) { 56 | var prefixedCode = '' + config.messageMapKeyPrefix + validatorName; 57 | if (_lodash2.default.has(validationMessageMap, prefixedCode)) { 58 | code = prefixedCode; 59 | } 60 | } 61 | 62 | var message = validationMessageMap[code]; 63 | if (message !== undefined && (_lodash2.default.isNumber(args) || !_lodash2.default.isEmpty(args))) { 64 | message = sprintf(message, args); 65 | } 66 | return { code: code, message: message }; 67 | }; 68 | 69 | var validate = exports.validate = function validate(value, fieldValidators, config) { 70 | var validations = []; 71 | if (!_lodash2.default.isArray(fieldValidators) || _lodash2.default.isEmpty(fieldValidators)) { 72 | return []; 73 | } 74 | 75 | fieldValidators.forEach(function (validator) { 76 | var args = void 0; 77 | var validatorName = validator; 78 | if (_lodash2.default.isObject(validator) && _lodash2.default.size(validator) === 1) { 79 | args = Object.values(validator)[0]; // eslint-disable-line prefer-destructuring 80 | validatorName = Object.keys(validator)[0]; // eslint-disable-line prefer-destructuring 81 | } else if (!_lodash2.default.isString(validator)) { 82 | console.error('invalid validator:', validator); // eslint-disable-line 83 | } 84 | 85 | if (config.validators[validatorName] === undefined) { 86 | console.error('undefined validator:', validatorName); // eslint-disable-line 87 | } else { 88 | value = String(value); 89 | var validation = config.validators[validatorName](value, args); 90 | if (!validation) { 91 | validations.push(createValidation(validatorName, args, config)); 92 | } 93 | } 94 | }); 95 | 96 | return validations; 97 | }; 98 | 99 | exports.constants = _constants2.default; 100 | exports.messageMap = _messageMap2.default; 101 | exports.validators = _validators2.default; -------------------------------------------------------------------------------- /dist/validation/messageMap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = { 7 | // validator.js 8 | contains: 'Value should contain "{0}"', 9 | equals: 'Value should equal "{0}"', 10 | isAfter: 'Date should be after "{0}"', 11 | isAlpha: 'Characters should be letters', 12 | isAlphanumeric: 'Characters should be letters or numbers', 13 | isAscii: 'Characters should be Ascii', 14 | isBefore: 'Date should be before "{0}"', 15 | isBoolean: 'Invalid boolean', 16 | isCreditCard: 'Invalid credit card number', 17 | isCurrency: 'Invalid currency', 18 | isDataURI: 'Invalid data URI', 19 | isDecimal: 'Invalid decimal', 20 | isDivisibleBy: 'Value is not divisible by {0}', 21 | isEmail: 'Invalid email', 22 | isFloat: 'Invalid float', 23 | isFQDN: 'Invalid FQDN', 24 | isFullWidth: 'Characters should be full-width', 25 | isHalfWidth: 'Characters should be helf-width', 26 | isHash: 'Invalid hash', 27 | isHexadecimal: 'Invalid hexadecimal', 28 | isHexColor: 'Invalid HEX value', 29 | isIn: 'Value should be one of {0}', 30 | isInt: 'Invalid integer', 31 | isIP: 'Invalid IP', 32 | isISBN: 'Invalid ISBN', 33 | isISIN: 'Invalid ISIN', 34 | isISO31661Alpha2: 'Invalid ISO31661Alpha2', 35 | isISO8601: 'Invalid ISO8601', 36 | isISRC: 'Invalid ISRC', 37 | isISSN: 'Invalid ISSN', 38 | isJSON: 'Invalid JSON', 39 | isLatLong: 'Invalid coordinates', 40 | isLength: 'Number of characters should be more than {0} and less than {1}', 41 | isLowercase: 'Characters should be lowercase', 42 | isMACAddress: 'Invalid MAC address', 43 | isMD5: 'Invalid MD5 hash', 44 | isMimeType: 'Invalid MIME type', 45 | isMobilePhone: 'Invalid mobile number', 46 | isMongoId: 'Invalid MongoDB id', 47 | isNumeric: 'Invalid numeric value', 48 | isPort: 'Invalid port number', 49 | isPostalCode: 'Invalid port number', 50 | isSurrogatePair: 'Invalid surrogate pair', 51 | isUppercase: 'Characters should be uppercase', 52 | isUrl: 'Invalid URL', 53 | isUUID: 'Invalid UUID', 54 | isVariableWidth: 'Characters should be hald-width or full-width', 55 | isWhitelisted: 'Characters should be one of "{0}"', 56 | // material-ui-form 57 | isAlias: 'Invalid alias - use letters, numbers, dots, hyphens, underscores', 58 | isRequired: 'This field is required', 59 | isDate: 'Invalid date', 60 | isNumber: 'Invalid number', 61 | isTime: 'Invalid time', 62 | isSerial: 'Invalid serial', 63 | isSize: 'Value should be more than {0} and less than {1}' 64 | }; -------------------------------------------------------------------------------- /dist/validation/validators/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _validator = require('validator'); 8 | 9 | var _validator2 = _interopRequireDefault(_validator); 10 | 11 | var _isAlias = require('./isAlias'); 12 | 13 | var _isAlias2 = _interopRequireDefault(_isAlias); 14 | 15 | var _isDate = require('./isDate'); 16 | 17 | var _isDate2 = _interopRequireDefault(_isDate); 18 | 19 | var _isNumber = require('./isNumber'); 20 | 21 | var _isNumber2 = _interopRequireDefault(_isNumber); 22 | 23 | var _isSerial = require('./isSerial'); 24 | 25 | var _isSerial2 = _interopRequireDefault(_isSerial); 26 | 27 | var _isTime = require('./isTime'); 28 | 29 | var _isTime2 = _interopRequireDefault(_isTime); 30 | 31 | var _isSize = require('./isSize'); 32 | 33 | var _isSize2 = _interopRequireDefault(_isSize); 34 | 35 | var _isRequired = require('./isRequired'); 36 | 37 | var _isRequired2 = _interopRequireDefault(_isRequired); 38 | 39 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 40 | 41 | _validator2.default.isAlias = _isAlias2.default; 42 | _validator2.default.isDate = _isDate2.default; 43 | _validator2.default.isNumber = _isNumber2.default; 44 | _validator2.default.isSerial = _isSerial2.default; 45 | _validator2.default.isTime = _isTime2.default; 46 | _validator2.default.isSize = _isSize2.default; 47 | _validator2.default.isRequired = _isRequired2.default; 48 | 49 | exports.default = _validator2.default; -------------------------------------------------------------------------------- /dist/validation/validators/isAlias.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (value) { 8 | return (/^[a-zA-Z0-9-_\.]*$/i.test(value) 9 | ); 10 | }; // eslint-disable-line -------------------------------------------------------------------------------- /dist/validation/validators/isDate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var dateRegExp = new RegExp(['^(19|20)\\d\\d([- /.])(0[1-9]|1[012])\\2', '(0[1-9]|[12][0-9]|3[01])$'].join(''), 'i'); 7 | 8 | exports.default = function (value) { 9 | return dateRegExp.test(value); 10 | }; -------------------------------------------------------------------------------- /dist/validation/validators/isNumber.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (value) { 8 | return (/^([,.\d]+)$/.test(value) 9 | ); 10 | }; // eslint-disable-line -------------------------------------------------------------------------------- /dist/validation/validators/isRequired.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (value) { 8 | return value.length !== 0; 9 | }; -------------------------------------------------------------------------------- /dist/validation/validators/isSerial.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (value) { 8 | return (/^([-\s\da-zA-Z]+)$/.test(value) 9 | ); 10 | }; // eslint-disable-line -------------------------------------------------------------------------------- /dist/validation/validators/isSize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 8 | 9 | exports.default = isSize; 10 | 11 | var _lodash = require('lodash'); 12 | 13 | var _lodash2 = _interopRequireDefault(_lodash); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | function isSize(value, rest) { 18 | var min = void 0; 19 | var max = void 0; 20 | if (_lodash2.default.isArray(rest)) { 21 | var _rest = _slicedToArray(rest, 2); 22 | 23 | min = _rest[0]; 24 | max = _rest[1]; 25 | } else { 26 | min = rest.min || 0; 27 | max = rest.max; // eslint-disable-line 28 | } 29 | return value >= min && (max === undefined || value <= max); 30 | } -------------------------------------------------------------------------------- /dist/validation/validators/isTime.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (value) { 8 | return (/^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/i.test(value) 9 | ); 10 | }; -------------------------------------------------------------------------------- /examples/Root.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | /* eslint-disable import/no-extraneous-dependencies */ 5 | import { BrowserRouter, Link, Route } from 'react-router-dom' 6 | 7 | import CssBaseline from '@material-ui/core/CssBaseline' 8 | import AppBar from '@material-ui/core/AppBar' 9 | import Toolbar from '@material-ui/core/Toolbar' 10 | import Button from '@material-ui/core/Button' 11 | /* eslint-enable import/no-extraneous-dependencies */ 12 | 13 | import './styles.css' 14 | import './markdown.css' 15 | import NestedFields from './pages/NestedFields' 16 | import CustomValidationMessages from './pages/CustomValidationMessages' 17 | import CustomValidators from './pages/CustomValidators' 18 | import CustomValidateFunction from './pages/CustomValidateFunction' 19 | import Steppers from './pages/Steppers' 20 | import DynamicArrayFields from './pages/DynamicArrayFields' 21 | import MiscProps from './pages/MiscProps' 22 | import ReadmeHTML from '../README.md' 23 | 24 | 25 | const wrapperStyle = { 26 | backgroundColor: 'white', 27 | height: 'inherit', 28 | overflowX: 'hidden', 29 | overflowY: 'auto', 30 | } 31 | 32 | const Readme = () => ( 33 |
38 | ) 39 | 40 | const Root = () => ( 41 |
42 | 43 | 44 |
45 | 46 | 47 | 50 | 55 | 58 | 63 | 66 | 69 | 72 | 75 | 76 | 77 | 78 | 79 | 83 | 84 | 88 | 89 | 90 | 91 | 92 |
93 |
94 |
95 | ) 96 | 97 | ReactDOM.render(, document.querySelector('#root')) 98 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | material-ui-form 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/markdown.css: -------------------------------------------------------------------------------- 1 | /** 2 | * standard markdown style 3 | */ 4 | .markdown { 5 | font-family: "-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol'"; 6 | padding: 20px; 7 | line-height: 1.5; 8 | } 9 | .markdown a { 10 | text-decoration: none; 11 | vertical-align: baseline; 12 | } 13 | .markdown a:hover { 14 | text-decoration: underline; 15 | } 16 | .markdown h1 { 17 | padding-bottom: 0.3em; 18 | font-size: 2em; 19 | border-bottom: 1px solid #eaecef; 20 | } 21 | .markdown h2 { 22 | padding-bottom: 0.3em; 23 | font-size: 1.5em; 24 | border-bottom: 1px solid #eaecef; 25 | } 26 | .markdown h3 { 27 | font-size: 1.25em; 28 | font-weight: bold; 29 | margin: 1.125em 0 0.75em 0; 30 | } 31 | .markdown h4 { 32 | font-size: 1em; 33 | font-weight: bold; 34 | margin: 0.99em 0 0.66em 0; 35 | } 36 | .markdown h5 { 37 | font-size: 0.8em; 38 | font-weight: bold; 39 | margin: 0.855em 0 0.57em 0; 40 | } 41 | .markdown h6 { 42 | font-size: 0.6px; 43 | font-weight: bold; 44 | margin: 0.75em 0 0.5em 0; 45 | } 46 | .markdown h1:first-child, 47 | .markdown h2:first-child, 48 | .markdown h3:first-child, 49 | .markdown h4:first-child, 50 | .markdown h5:first-child, 51 | .markdown h6:first-child { 52 | margin-top: 0; 53 | } 54 | .markdown h1 + p, 55 | .markdown h2 + p, 56 | .markdown h3 + p, 57 | .markdown h4 + p, 58 | .markdown h5 + p, 59 | .markdown h6 + p { 60 | margin-top: 0; 61 | } 62 | .markdown hr { 63 | border: 1px solid #cccccc; 64 | } 65 | .markdown p { 66 | margin: 1em 0; 67 | word-wrap: break-word; 68 | } 69 | .markdown ol { 70 | list-style-type: decimal; 71 | } 72 | .markdown li { 73 | display: list-item; 74 | line-height: 1.8em; 75 | font-size: 15px; 76 | } 77 | .markdown blockquote { 78 | margin: 1em 20px; 79 | } 80 | .markdown blockquote > :first-child { 81 | margin-top: 0; 82 | } 83 | .markdown blockquote > :last-child { 84 | margin-bottom: 0; 85 | } 86 | .markdown blockquote cite:before { 87 | content: '\2014 \00A0'; 88 | } 89 | .markdown .code { 90 | border-radius: 3px; 91 | word-wrap: break-word; 92 | } 93 | .markdown pre { 94 | padding: 16px; 95 | overflow: auto; 96 | font-size: 15px; 97 | line-height: 1.45; 98 | background-color: #f6f8fa; 99 | border-radius: 3px; 100 | } 101 | .markdown pre code { 102 | border: 0; 103 | } 104 | .markdown pre > code { 105 | font-family: Consolas, Inconsolata, Courier, monospace; 106 | white-space: pre; 107 | margin: 0; 108 | } 109 | .markdown code { 110 | border-radius: 3px; 111 | word-wrap: break-word; 112 | border: 1px solid #cccccc; 113 | padding: 0 5px; 114 | margin: 0 2px; 115 | } 116 | .markdown img { 117 | max-width: 100%; 118 | } 119 | .markdown mark { 120 | color: #000; 121 | background-color: #fcf8e3; 122 | } 123 | .markdown table { 124 | padding: 0; 125 | border-collapse: collapse; 126 | border-spacing: 0; 127 | margin-bottom: 16px; 128 | } 129 | .markdown table tr th, 130 | .markdown table tr td { 131 | border: 1px solid #cccccc; 132 | margin: 0; 133 | padding: 6px 13px; 134 | } 135 | .markdown table tr th { 136 | font-weight: bold; 137 | } 138 | .markdown table tr th > :first-child { 139 | margin-top: 0; 140 | } 141 | .markdown table tr th > :last-child { 142 | margin-bottom: 0; 143 | } 144 | .markdown table tr td > :first-child { 145 | margin-top: 0; 146 | } 147 | .markdown table tr td > :last-child { 148 | margin-bottom: 0; 149 | } 150 | 151 | /*--------------------------------------------------- 152 | LESS Elements 0.9 153 | --------------------------------------------------- 154 | A set of useful LESS mixins 155 | More info at: http://lesselements.com 156 | ---------------------------------------------------*/ 157 | /** 158 | * https://github.com/rhiokim/markdown-css 159 | * solarized-light style 160 | * made by rhio.kim 161 | * powered by http://ethanschoonover.com/solarized 162 | */ 163 | .github { 164 | padding: 20px; 165 | font-family: "Helvetica Neue", Helvetica, "Hiragino Sans GB", "Microsoft YaHei", "STHeiti", "SimSun", "Segoe UI", AppleSDGothicNeo-Medium, 'Malgun Gothic', Arial, freesans, sans-serif; 166 | font-size: 16px; 167 | background: #ffffff; 168 | line-height: 1.6; 169 | -webkit-font-smoothing: antialiased; 170 | 171 | max-width: 1020px; 172 | padding: 45px; 173 | color: rgb(36, 41, 46); 174 | margin: 0 auto; 175 | border: 1px solid #ddd; 176 | } 177 | .github a { 178 | color: #3269a0; 179 | } 180 | .github a:hover { 181 | color: #4183c4; 182 | } 183 | .github h2 { 184 | border-bottom: 1px solid #e6e6e6; 185 | line-height: 1.6; 186 | } 187 | .github h6 { 188 | color: #777; 189 | } 190 | .github hr { 191 | border: 1px solid #e6e6e6; 192 | } 193 | .github pre > code { 194 | font-size: .9em; 195 | font-family: Consolas, Inconsolata, Courier, monospace; 196 | } 197 | .github p > code, 198 | .github li > code, 199 | .github td > code, 200 | .github h1 > code, 201 | .github h2 > code, 202 | .github h3 > code, 203 | .github h4 > code, 204 | .github h5 > code, 205 | .github h6 > code, 206 | .github blockquote > code { 207 | background-color: rgba(0, 0, 0, 0.07); 208 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 209 | font-size: 85%; 210 | padding: 0.2em 0.5em; 211 | border: 0; 212 | } 213 | .github blockquote { 214 | border-left: 4px solid #e6e6e6; 215 | padding: 0 15px; 216 | font-style: italic; 217 | } 218 | .github table { 219 | background-color: #fafafa; 220 | } 221 | .github table tr th, 222 | .github table tr td { 223 | border: 1px solid #e6e6e6; 224 | } 225 | .github table tr:nth-child(2n) { 226 | background-color: #f2f2f2; 227 | } 228 | /** 229 | * after less 230 | */ 231 | -------------------------------------------------------------------------------- /examples/pages/CustomValidateFunction.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import PropTypes from 'prop-types' // eslint-disable-line import/no-extraneous-dependencies 4 | import Grid from '@material-ui/core/Grid' 5 | import Button from '@material-ui/core/Button' 6 | import Divider from '@material-ui/core/Divider' 7 | import TextField from '@material-ui/core/TextField' 8 | import { withStyles } from '@material-ui/core/styles' 9 | 10 | import Form from '../../src/index' 11 | import styles from '../styles' 12 | 13 | 14 | const dividerStyle = { margin: '20px 0' } 15 | 16 | function validate(value, fieldValidators, options) { 17 | const fieldValidations = [] 18 | fieldValidators.forEach((validator) => { 19 | const validation = { 20 | code: String(validator), 21 | message: 'its invalid so maybe try harder...', 22 | } 23 | if (_.has(options, 'genericMessage')) { 24 | validation.message = options.genericMessage 25 | } 26 | fieldValidations.push(validation) 27 | }) 28 | return fieldValidations 29 | } 30 | 31 | const validationOptions = { 32 | genericMessage: 'yeah... *tisk*', 33 | } 34 | 35 | @withStyles(styles) 36 | export default class CustomValidateFunction extends React.Component { 37 | static propTypes = { 38 | classes: PropTypes.object.isRequired, 39 | } 40 | 41 | state = { 42 | onSubmitValues: null, 43 | } 44 | 45 | submit = (values, pristineValues) => { 46 | // eslint-disable-next-line no-console 47 | console.log('submit values:', values, 'pristine values:', pristineValues) 48 | this.setState({ onSubmitValues: values }) 49 | } 50 | 51 | render() { 52 | const { classes } = this.props 53 | 54 | return ( 55 | 60 | 61 | 69 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |
85 |             {this.state.onSubmitValues &&
86 |               JSON.stringify(this.state.onSubmitValues, null, 2)
87 |             }
88 |           
89 |
90 |
91 | ) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /examples/pages/CustomValidationMessages.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' // eslint-disable-line import/no-extraneous-dependencies 3 | 4 | import Grid from '@material-ui/core/Grid' 5 | import Button from '@material-ui/core/Button' 6 | import Divider from '@material-ui/core/Divider' 7 | import TextField from '@material-ui/core/TextField' 8 | import { withStyles } from '@material-ui/core/styles' 9 | 10 | import Form, { messageMap } from '../../src/index' 11 | import styles from '../styles' 12 | 13 | const dividerStyle = { margin: '20px 0' } 14 | 15 | const customFormMsg = Object.assign(messageMap, { 16 | isRequired: 'This field is required', 17 | isEmail: 'Please enter a valid email address', 18 | isLength:'Must be 2-50 characters', 19 | }) 20 | 21 | 22 | @withStyles(styles) 23 | export default class CustomValidationMessages extends React.Component { 24 | static propTypes = { 25 | classes: PropTypes.object.isRequired, 26 | } 27 | 28 | state = { 29 | onSubmitValues: null, 30 | } 31 | 32 | submit = (values, pristineValues) => { 33 | // eslint-disable-next-line no-console 34 | console.log('submit values:', values, 'pristine values:', pristineValues) 35 | this.setState({ onSubmitValues: values }) 36 | } 37 | 38 | render() { 39 | const { classes } = this.props 40 | 41 | return ( 42 | 47 | 48 |
55 | 63 | 64 | 65 | 73 | 74 | 75 | 83 | 84 | 85 | 93 | 94 | 95 | 103 | 104 | 105 | 106 | 107 |
108 | 109 |
110 |             {this.state.onSubmitValues &&
111 |               JSON.stringify(this.state.onSubmitValues, null, 2)
112 |             }
113 |           
114 |
115 |
116 | ) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /examples/pages/CustomValidators.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' // eslint-disable-line import/no-extraneous-dependencies 3 | 4 | import Grid from '@material-ui/core/Grid' 5 | import Button from '@material-ui/core/Button' 6 | import Divider from '@material-ui/core/Divider' 7 | import TextField from '@material-ui/core/TextField' 8 | import { withStyles } from '@material-ui/core/styles' 9 | 10 | import Form, { messageMap, validators } from '../../src/index' 11 | import styles from '../styles' 12 | 13 | 14 | const dividerStyle = { margin: '20px 0' } 15 | 16 | validators.isBorat = value => value === 'borat' 17 | const customMessageMap = Object.assign(messageMap, { 18 | isBorat: 'NAAAAAT! You can only write "borat" lol', 19 | }) 20 | 21 | @withStyles(styles) 22 | export default class CustomValidators extends React.Component { 23 | static propTypes = { 24 | classes: PropTypes.object.isRequired, 25 | } 26 | 27 | state = { 28 | onSubmitValues: null, 29 | } 30 | 31 | submit = (values, pristineValues) => { 32 | // eslint-disable-next-line no-console 33 | console.log('submit values:', values, 'pristine values:', pristineValues) 34 | this.setState({ onSubmitValues: values }) 35 | } 36 | 37 | render() { 38 | const { classes } = this.props 39 | 40 | return ( 41 | 46 | 47 |
54 | 63 | 64 | 65 | 66 | 67 |
68 | 69 |
70 |             {this.state.onSubmitValues &&
71 |               JSON.stringify(this.state.onSubmitValues, null, 2)
72 |             }
73 |           
74 |
75 |
76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/pages/DynamicArrayFields.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import PropTypes from 'prop-types' // eslint-disable-line import/no-extraneous-dependencies 3 | 4 | import Grid from '@material-ui/core/Grid' 5 | import Button from '@material-ui/core/Button' 6 | import TextField from '@material-ui/core/TextField' 7 | import { withStyles } from '@material-ui/core/styles' 8 | import Divider from '@material-ui/core/Divider' 9 | 10 | import Form from '../../src/index' 11 | import styles from '../styles' 12 | 13 | 14 | const inputStyle = { 15 | marginRight: '20px', 16 | width: '250px', 17 | } 18 | 19 | @withStyles(styles) 20 | export default class DynamicArrayFields extends Component { 21 | static propTypes = { 22 | classes: PropTypes.object.isRequired, 23 | } 24 | 25 | state = { 26 | rows: [{ _id: _.uniqueId() }], 27 | onSubmitValues: null, 28 | } 29 | 30 | addRow = () => { 31 | const { rows } = this.state 32 | rows.push({ _id: _.uniqueId() }) 33 | this.setState({ rows }) 34 | } 35 | 36 | removeRow = (index) => { 37 | const { rows } = this.state 38 | if (rows.length > 1) { 39 | rows.splice(index, 1) 40 | this.setState({ rows }) 41 | } 42 | } 43 | 44 | submit = (values, pristineValues) => { 45 | // eslint-disable-next-line no-console 46 | console.log('submit values:', values, 'pristine values:', pristineValues) 47 | this.setState({ onSubmitValues: values }) 48 | } 49 | 50 | render() { 51 | const { classes } = this.props 52 | 53 | return ( 54 | 59 | 60 |
61 | {this.state.rows.map((row, i) => ( 62 | 63 | 70 | 76 | { this.state.rows.length > 1 && 77 | 83 | } 84 | 85 | ))} 86 |

87 | 88 | 89 | 90 | 91 |
92 | 93 |
 94 |             {this.state.onSubmitValues &&
 95 |               JSON.stringify(this.state.onSubmitValues, null, 2)
 96 |             }
 97 |           
98 |
99 |
100 | ) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /examples/pages/MiscProps.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' // eslint-disable-line import/no-extraneous-dependencies 3 | 4 | import Grid from '@material-ui/core/Grid' 5 | import Button from '@material-ui/core/Button' 6 | import Divider from '@material-ui/core/Divider' 7 | import TextField from '@material-ui/core/TextField' 8 | import { withStyles } from '@material-ui/core/styles' 9 | 10 | import Form from '../../src/index' 11 | import styles from '../styles' 12 | 13 | 14 | const dividerStyle = { margin: '20px 0' } 15 | 16 | const mockServerValidations = { 17 | firstName: [{ code: 'isInvalid', message: 'such invalid...' }], 18 | } 19 | 20 | @withStyles(styles) 21 | export default class MiscProps extends React.Component { 22 | static propTypes = { 23 | classes: PropTypes.object.isRequired, 24 | } 25 | 26 | state = { 27 | onSubmitValues: null, 28 | mockServerValidations, 29 | } 30 | 31 | componentDidMount() { 32 | let validations = { 33 | firstName: [{ message: 'such WOOOOOOOOOW...' }], 34 | } 35 | 36 | setTimeout(() => { 37 | this.setState({ mockServerValidations: validations }) 38 | }, 1500) 39 | 40 | setTimeout(() => { 41 | validations = { 42 | firstName: [{ message: 'so still haven\'t watched Italian Spiderman?' }], 43 | } 44 | this.setState({ mockServerValidations: validations }) 45 | }, 3000) 46 | } 47 | 48 | handleValuesChange = (values, pristineValues) => { 49 | // eslint-disable-next-line no-console 50 | console.log('new values:', values, 'pristine values:', pristineValues) 51 | } 52 | 53 | submit = (values, pristineValues) => { 54 | // eslint-disable-next-line no-console 55 | console.log('submit values:', values, 'pristine values:', pristineValues) 56 | this.setState({ onSubmitValues: values }) 57 | } 58 | 59 | render() { 60 | const { classes } = this.props 61 | 62 | return ( 63 | 68 | 69 |
76 | 83 | 84 | 85 | 93 | 94 | 95 | 96 | 97 | 98 |
99 | 100 |
101 |             {this.state.onSubmitValues &&
102 |               JSON.stringify(this.state.onSubmitValues, null, 2)
103 |             }
104 |           
105 |
106 |
107 | ) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /examples/pages/NestedFields.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' // eslint-disable-line import/no-extraneous-dependencies 3 | 4 | import Grid from '@material-ui/core/Grid' 5 | import Button from '@material-ui/core/Button' 6 | import TextField from '@material-ui/core/TextField' 7 | import { withStyles } from '@material-ui/core/styles' 8 | import Checkbox from '@material-ui/core/Checkbox' 9 | import FormControl from '@material-ui/core/FormControl' 10 | import FormControlLabel from '@material-ui/core/FormControlLabel' 11 | import FormHelperText from '@material-ui/core/FormHelperText' 12 | import FormLabel from '@material-ui/core/FormLabel' 13 | import Radio from '@material-ui/core/Radio' 14 | import RadioGroup from '@material-ui/core/RadioGroup' 15 | 16 | import InputLabel from '@material-ui/core/InputLabel' 17 | import Select from '@material-ui/core/Select' 18 | import MenuItem from '@material-ui/core/MenuItem' 19 | 20 | import MaterialUIForm from '../../src/index' 21 | import styles from '../styles' 22 | 23 | 24 | const formControlStyle = { 25 | padding: 'inherit', 26 | margin: 'inherit', 27 | display: 'inherit', 28 | border: 'inherit', 29 | } 30 | 31 | @withStyles(styles) 32 | export default class NestedFields extends React.Component { 33 | static propTypes = { 34 | classes: PropTypes.object.isRequired, 35 | } 36 | 37 | state = { 38 | onSubmitValues: null, 39 | } 40 | 41 | uploadFile = (event) => { 42 | console.log(event.target.files) // eslint-disable-line 43 | } 44 | 45 | submit = (values, pristineValues) => { 46 | // eslint-disable-next-line no-console 47 | console.log('submit values:', values, 'pristine values:', pristineValues) 48 | this.setState({ onSubmitValues: values }) 49 | } 50 | 51 | customInputHandler = (value, { name }, event) => { 52 | // eslint-disable-next-line no-console 53 | console.log(value, name, event) 54 | } 55 | 56 | customToggleHandler = (checked, { name, value }, event) => { 57 | // eslint-disable-next-line no-console 58 | console.log(checked, name, value, event) 59 | } 60 | 61 | render() { 62 | const { classes } = this.props 63 | 64 | return ( 65 | 70 | 71 | 76 | {'Please fill in the required fields (*)'} 77 | 85 | 92 | 93 |
94 | Custom controlled component 95 | {'Upload file:'} 96 | 104 | 109 |
110 | 111 |
112 | FormControl Select 113 | 114 | Age 115 | 122 | Some important helper text 123 | 124 | 125 |
126 | TextField native select 127 | 136 | 137 | 138 | 139 | 140 |
141 | FormControlLabel and plain Checkbox 142 | } 144 | label="I will use them wisely" 145 | /> 146 | 147 | 153 | I will use them awesomely 154 | 155 | 160 | 161 | RadioGroup FormControl 162 | 163 | 167 | } 170 | label="I swear" 171 | /> 172 | } 175 | label="Probably" 176 | /> 177 | } 180 | label="Maybe" 181 | /> 182 | 183 | 184 |
185 |
186 |
187 | 188 | 189 |
190 |
191 | 192 |
193 |             {this.state.onSubmitValues &&
194 |               JSON.stringify(this.state.onSubmitValues, null, 2)
195 |             }
196 |           
197 |
198 |
199 | ) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /examples/pages/Steppers.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import PropTypes from 'prop-types' // eslint-disable-line import/no-extraneous-dependencies 3 | 4 | import Grid from '@material-ui/core/Grid' 5 | import Button from '@material-ui/core/Button' 6 | import MenuItem from '@material-ui/core/MenuItem' 7 | import TextField from '@material-ui/core/TextField' 8 | import Stepper from '@material-ui/core/Stepper' 9 | import Step from '@material-ui/core/Step' 10 | import StepLabel from '@material-ui/core/StepLabel' 11 | import { withStyles } from '@material-ui/core/styles' 12 | import Divider from '@material-ui/core/Divider' 13 | 14 | import Form from '../../src/index' 15 | import styles from '../styles' 16 | 17 | 18 | const dividerStyle = { margin: '20px 0' } 19 | 20 | function getSteps() { 21 | return [ 22 | 'Step 1', 23 | 'Step 2', 24 | ] 25 | } 26 | 27 | @withStyles(styles) 28 | export default class Steppers extends Component { 29 | static propTypes = { 30 | classes: PropTypes.object.isRequired, 31 | } 32 | 33 | state = { 34 | activeStep: 0, 35 | amounts: [true], // hack 36 | onSubmitValues: null, 37 | errorSteps: [], 38 | } 39 | 40 | clickNext = () => { 41 | this.setState({ 42 | activeStep: this.state.activeStep + 1, 43 | }) 44 | } 45 | 46 | clickBack = () => { 47 | this.setState({ 48 | activeStep: this.state.activeStep - 1, 49 | }) 50 | } 51 | 52 | addAmount = () => { 53 | const amounts = _.clone(this.state.amounts) 54 | amounts.push(true) 55 | this.setState({ amounts }) 56 | } 57 | 58 | submit = (values, pristineValues) => { 59 | // eslint-disable-next-line no-console 60 | console.log('submit values:', values, 'pristine values:', pristineValues) 61 | this.setState({ onSubmitValues: values }) 62 | } 63 | 64 | updateErrorSteps = (field, errorSteps) => { 65 | this.setState({ errorSteps }) 66 | } 67 | 68 | render() { 69 | const steps = getSteps() 70 | const { classes } = this.props 71 | const { activeStep, errorSteps } = this.state 72 | 73 | return ( 74 | 79 | 80 | 81 | {steps.map((label, i) => ( 82 | 83 | 84 | {label} 85 | 86 | 87 | ))} 88 | 89 | 90 |
95 | {activeStep === 0 && 96 | 97 | 105 | 106 | 109 | 110 | } 111 | 112 | {activeStep === 1 && 113 | 114 | {this.state.amounts.map((amount, i) => ( 115 | // eslint-disable-next-line react/no-array-index-key 116 | 117 | 128 | Zero 129 | Ten and a half 130 | Twenty 131 | Thirty 132 | 133 | 134 | 135 | ))} 136 | 137 | 138 | 139 | 140 | } 141 |
142 |
143 | 144 |
145 |             {this.state.onSubmitValues &&
146 |               JSON.stringify(this.state.onSubmitValues, null, 2)
147 |             }
148 |           
149 |
150 |
151 | ) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /examples/styles.css: -------------------------------------------------------------------------------- 1 | html, 2 | body{ 3 | height: 100%; 4 | width: 100%; 5 | overflow: hidden; 6 | } 7 | 8 | header a{ 9 | text-decoration: none; 10 | color: white; 11 | 12 | &:visited{ 13 | color: white; 14 | } 15 | 16 | &:hover{ 17 | color: white; 18 | } 19 | } 20 | 21 | #root{ 22 | height: inherit; 23 | } 24 | -------------------------------------------------------------------------------- /examples/styles.js: -------------------------------------------------------------------------------- 1 | const styles = () => ({ 2 | form: { 3 | borderTop: '1px dotted #ccc', 4 | }, 5 | gridItem: { 6 | margin: '100px', 7 | '& pre': { 8 | color: '#999', 9 | fontSize: '15px', 10 | }, 11 | }, 12 | }) 13 | 14 | export default styles 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-material-ui-form", 3 | "version": "1.1.7", 4 | "description": "State and validation management for Material-UI form components", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "webpack -p && rm -Rf ./dist && cross-env NODE_ENV=production babel ./src -d ./dist", 8 | "check": "npm run lint && npm run flow && npm run test", 9 | "dev": "webpack-dev-server --mode development --entry=./examples/Root --open", 10 | "flow": "flow", 11 | "lint": "eslint -c .eslintrc.json ./src --no-eslintrc", 12 | "prepare": "npm run check && npm run build", 13 | "test": "node ./tools/windows-safe-jest.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/voletiswaroop/react-material-ui-form.git" 18 | }, 19 | "keywords": [ 20 | "react-material-ui-form-validation", 21 | "react-material-ui-form", 22 | "react-form-validation", 23 | "react-form", 24 | "material-form", 25 | "material-ui-form", 26 | "form", 27 | "forms", 28 | "react-form-radio-button", 29 | "react-form-dropdown", 30 | "react-form-select", 31 | "react-form-checkbox", 32 | "validation" 33 | ], 34 | "files": [ 35 | "dist" 36 | ], 37 | "author": "voletiswaroop", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/voletiswaroop/react-material-ui-form/issues" 41 | }, 42 | "homepage": "https://github.com/voletiswaroop/react-material-ui-form#readme", 43 | "jest": { 44 | "coverageDirectory": "./coverage/", 45 | "collectCoverage": true, 46 | "globals": { 47 | "_": true 48 | }, 49 | "setupTestFrameworkScriptFile": "/setupTests.js", 50 | "snapshotSerializers": [ 51 | "enzyme-to-json/serializer" 52 | ] 53 | }, 54 | "dependencies": { 55 | "@material-ui/core": "^3.1.1", 56 | "lodash": "^4.17.10", 57 | "react": "^16.4.1", 58 | "react-dom": "^16.4.1", 59 | "validator": "^9.4.1", 60 | "react-jss": "^8.6.1" 61 | }, 62 | "devDependencies": { 63 | "babel-cli": "^6.26.0", 64 | "babel-core": "^6.26.3", 65 | "babel-eslint": "^8.2.3", 66 | "babel-jest": "^23.0.1", 67 | "babel-loader": "^7.1.4", 68 | "babel-plugin-transform-decorators-legacy": "^1.3.5", 69 | "babel-preset-env": "^1.7.0", 70 | "babel-preset-flow": "^6.23.0", 71 | "babel-preset-react": "^6.24.1", 72 | "babel-preset-stage-0": "^6.24.1", 73 | "cross-env": "^5.2.0", 74 | "css-loader": "^0.28.10", 75 | "enzyme": "^3.3.0", 76 | "enzyme-adapter-react-16": "^1.1.1", 77 | "enzyme-to-json": "^3.3.4", 78 | "eslint": "^4.18.2", 79 | "eslint-config-airbnb": "^16.1.0", 80 | "eslint-plugin-filenames": "^1.3.2", 81 | "eslint-plugin-import": "^2.12.0", 82 | "eslint-plugin-jsx-a11y": "^6.0.3", 83 | "eslint-plugin-react": "^7.9.1", 84 | "flow-bin": "^0.68.0", 85 | "html-loader": "^0.5.5", 86 | "html-webpack-plugin": "^3.2.0", 87 | "jest": "^22.4.4", 88 | "jest-enzyme": "^6.0.1", 89 | "markdown-loader": "^2.0.2", 90 | "prop-types": "^15.6.1", 91 | "react-router-dom": "^4.3.1", 92 | "shelljs": "^0.8.2", 93 | "strip-loader": "^0.1.2", 94 | "style-loader": "^0.20.3", 95 | "webpack": "^4.19.0", 96 | "webpack-cli": "^2.1.5", 97 | "webpack-dev-server": "^3.1.4", 98 | "yargs": "^11.0.0" 99 | } 100 | } -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme' // eslint-disable-line import/no-extraneous-dependencies 2 | import Adapter from 'enzyme-adapter-react-16' // eslint-disable-line import/no-extraneous-dependencies 3 | 4 | configure({ adapter: new Adapter() }) 5 | -------------------------------------------------------------------------------- /src/components/CheckableFieldClone.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | 5 | import Checkbox from '@material-ui/core/Checkbox' 6 | import Switch from '@material-ui/core/Switch' 7 | 8 | 9 | type Props = { 10 | field?: Object, 11 | fieldComp: Object, 12 | onConstruct: Function, 13 | onToggle: Function, 14 | }; 15 | 16 | type State = { 17 | checked: boolean, 18 | }; 19 | 20 | export default class CheckableFieldClone extends React.Component { 21 | static defaultProps = { 22 | field: {}, 23 | } 24 | 25 | constructor(props: Object) { 26 | super(props) 27 | const { fieldComp } = props 28 | 29 | if (![Checkbox, Switch].includes(fieldComp.type)) { 30 | throw new Error('CheckableFieldClone should be a Checkbox or Switch') 31 | } 32 | if (fieldComp.props.name === undefined || fieldComp.props.value === undefined) { 33 | throw new Error('CheckableFieldClone name and value must be defined') 34 | } 35 | 36 | let checked = props.field.value 37 | if (props.field.value === undefined) { 38 | checked = fieldComp.props.checked || false 39 | this.props.onConstruct(fieldComp.props) 40 | } 41 | this.state = { checked } 42 | } 43 | 44 | onToggle = (event: Event, checked: boolean) => { 45 | const { fieldComp, fieldComp: { props: { name, value } } } = this.props 46 | this.setState({ checked }) 47 | this.props.onToggle(name, value, checked) 48 | if (fieldComp.props.onChange !== undefined) { 49 | fieldComp.props.onChange(checked, { name, value }, event) 50 | } 51 | } 52 | 53 | render() { 54 | const { fieldComp } = this.props 55 | return React.cloneElement(fieldComp, { 56 | value: fieldComp.props.value, 57 | checked: this.state.checked, 58 | onChange: this.onToggle, 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/DeleteFieldRowButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | 5 | 6 | type Props = { 7 | buttonComp: Object, 8 | onRequestRowDelete: Function, 9 | }; 10 | 11 | export default class DeleteFieldRowButton extends React.Component { 12 | constructor(props: Object) { 13 | super(props) 14 | 15 | const { buttonComp: { props: { deletefieldrow } } } = this.props 16 | if (deletefieldrow === undefined) { 17 | throw new Error('DeleteFieldRowButton element requires "deletefieldrow" prop') 18 | } 19 | if (deletefieldrow.match(/\w+\[\d+\]/) === null) { 20 | throw new Error('"deletefieldrow" prop should match /\\w+\\[\\d+\\]/') 21 | } 22 | } 23 | 24 | onClick = () => { 25 | const { 26 | onRequestRowDelete, 27 | buttonComp: { 28 | props: { 29 | onClick, 30 | deletefieldrow, 31 | }, 32 | }, 33 | } = this.props 34 | 35 | onRequestRowDelete(deletefieldrow) 36 | onClick() 37 | } 38 | 39 | render() { 40 | const { buttonComp } = this.props 41 | return React.cloneElement(buttonComp, { 42 | onClick: this.onClick, 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/FieldClone.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import _ from 'lodash' 5 | 6 | 7 | function getRequiredProp( 8 | required: ?boolean, 9 | useNativeRequiredValidator: boolean 10 | ): boolean { 11 | if (!useNativeRequiredValidator) { 12 | return false 13 | } 14 | return required || false 15 | } 16 | 17 | function makeLabel(fieldComp: Object, props: Object): string { 18 | const label: string = fieldComp.props.label || '' 19 | return props.field.isRequired && !props.useNativeRequiredValidator 20 | ? `${label}` 21 | : label 22 | } 23 | 24 | function makeErrorAndHelperText(props: Object): Object { 25 | let helperText: ?string = _.get(props.fieldComp.props, 'helperText') 26 | let isError: boolean = false 27 | 28 | if (!_.isEmpty(props.field) && props.field.validations.length > 0) { 29 | helperText = props.field.validations[0].message 30 | isError = true 31 | } 32 | return { helperText, isError } 33 | } 34 | 35 | type Props = { 36 | field: Object, 37 | fieldComp: Object, 38 | onConstruct: Function, 39 | onValueChange: Function, 40 | useNativeRequiredValidator: boolean, 41 | validateInputOnBlur: boolean, 42 | }; 43 | 44 | type State = { 45 | helperText: ?string, 46 | isError: boolean, 47 | value: any, 48 | }; 49 | 50 | export default class FieldClone extends React.Component { 51 | static defaultProps = { 52 | field: {}, 53 | } 54 | 55 | constructor(props: Object) { 56 | super(props) 57 | const { fieldComp } = props 58 | if (fieldComp.type.name === undefined || (fieldComp.type.options && 59 | fieldComp.type.options.name === undefined)) { 60 | throw new Error('FieldClone does not support native elements') 61 | } 62 | if (fieldComp.props.name === undefined || fieldComp.props.value === undefined) { 63 | throw new Error('FieldClone name and value must be defined') 64 | } 65 | 66 | const value = _.isEmpty(props.field) ? fieldComp.props.value : props.field.value 67 | const { helperText, isError } = makeErrorAndHelperText(props) 68 | 69 | this.state = { 70 | helperText, 71 | isError, 72 | value, 73 | } 74 | 75 | if (props.field.value === undefined) { 76 | this.props.onConstruct(fieldComp.props) 77 | } 78 | } 79 | 80 | static getDerivedStateFromProps(nextProps: Object) { 81 | if (!_.isEmpty(nextProps.field)) { 82 | const { helperText, isError } = makeErrorAndHelperText(nextProps) 83 | return { 84 | helperText, 85 | isError, 86 | value: nextProps.field.value, 87 | } 88 | } 89 | return null 90 | } 91 | 92 | onBlur = (event: SyntheticInputEvent) => { 93 | const { 94 | field: { isDirty }, 95 | fieldComp, 96 | fieldComp: { props: { name } }, 97 | validateInputOnBlur, 98 | } = this.props 99 | const { value } = event.target 100 | // // /* TODO: create function for condition */ 101 | if ((!isDirty || validateInputOnBlur) && !fieldComp.props.select) { 102 | this.props.onValueChange(name, value, true) 103 | } 104 | if (fieldComp.props.onBlur !== undefined) { 105 | fieldComp.props.onBlur(value, { name }, event) 106 | } 107 | } 108 | 109 | onChange = (event: SyntheticInputEvent) => { 110 | const { 111 | fieldComp, 112 | fieldComp: { props: { name } }, 113 | validateInputOnBlur, 114 | } = this.props 115 | const { value } = event.target 116 | if (fieldComp.props.select || validateInputOnBlur) { 117 | const helperText: ?string = _.get(fieldComp.props, 'helperText') 118 | this.setState({ isError: false, helperText, value }) 119 | } 120 | /* TODO: create function for condition */ 121 | if (!validateInputOnBlur || fieldComp.props.select) { 122 | this.props.onValueChange(name, value, fieldComp.props.select) 123 | } 124 | if (fieldComp.props.onChange !== undefined) { 125 | fieldComp.props.onChange(value, { name }, event) 126 | } 127 | } 128 | 129 | render() { 130 | const { fieldComp, ...props } = this.props 131 | return React.cloneElement(fieldComp, { 132 | value: this.state.value, 133 | label: makeLabel(fieldComp, props), 134 | error: this.state.isError, 135 | helperText: this.state.helperText, 136 | onBlur: this.onBlur, 137 | onChange: this.onChange, 138 | required: getRequiredProp( 139 | fieldComp.props.required, 140 | this.props.useNativeRequiredValidator 141 | ), 142 | }) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/components/Form.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import _ from 'lodash' 5 | 6 | import FormControl from '@material-ui/core/FormControl' 7 | import FormControlLabel from '@material-ui/core/FormControlLabel' 8 | import FormHelperText from '@material-ui/core/FormHelperText' 9 | import FormLabel from '@material-ui/core/FormLabel' 10 | import InputLabel from '@material-ui/core/InputLabel' 11 | import Checkbox from '@material-ui/core/Checkbox' 12 | import Switch from '@material-ui/core/Switch' 13 | 14 | import FormControlClone from './FormControlClone' 15 | import FormControlLabelClone from './FormControlLabelClone' 16 | import FieldClone from './FieldClone' 17 | import CheckableFieldClone from './CheckableFieldClone' 18 | import DeleteFieldRowButton from './DeleteFieldRowButton' 19 | import propNames from '../propNames' 20 | import { 21 | messageMap, 22 | validate, 23 | validators as defaultValidators, 24 | constants as validationConstants, 25 | } from '../validation' 26 | 27 | 28 | function verifyFieldElement(component: any): boolean { 29 | const whitelist = [ 30 | FormControlLabel, 31 | ] 32 | 33 | return whitelist.includes(component.type) 34 | || (_.has(component, 'props.name') && _.has(component, 'props.value')) 35 | } 36 | 37 | function extractFieldValidators(fieldProps: Object): Array { 38 | let validators = _.get(fieldProps, propNames.FIELD_VALIDATORS) 39 | if (validators !== undefined) { 40 | if (_.isString(validators)) { 41 | validators = validators.replace(/\s/g, '').split(',') 42 | } else if (!_.isArray(validators)) { 43 | validators = [validators] 44 | } 45 | return validators 46 | } 47 | return [] 48 | } 49 | 50 | function getFieldValues(fields: Object): Object { 51 | const values = {} 52 | _.each(fields, (field, name) => { 53 | if (_.get(field, 'checked') !== false) { 54 | values[name] = field.value 55 | } 56 | }) 57 | return values 58 | } 59 | 60 | function getPristineFieldValues(fields: Object): Object { 61 | const values = {} 62 | _.each(fields, (field, name) => { 63 | if (!field.isPristine && _.get(field, 'checked') !== false) { 64 | values[name] = field.pristineValue 65 | } 66 | }) 67 | return values 68 | } 69 | 70 | function getFieldTemplate() { 71 | return { 72 | isDirty: false, 73 | isPristine: true, 74 | isRequired: null, 75 | pristineValue: null, 76 | step: undefined, 77 | validations: [], 78 | validators: [], 79 | value: undefined, 80 | } 81 | } 82 | 83 | function deriveErrorSteps(fields: Object): Array { 84 | const errorSteps = [] 85 | _.each(fields, (field) => { 86 | if (field.validations.length > 0 && !errorSteps.includes(field.step)) { 87 | errorSteps.push(field.step) 88 | } 89 | }) 90 | return errorSteps 91 | } 92 | 93 | function isValidForm(fields: Object): boolean { 94 | return _.size(_.filter(fields, field => field.validations.length > 0)) === 0 95 | } 96 | 97 | type Props = { 98 | activeStep?: number, 99 | autoComplete?: string, 100 | children: Array, 101 | className?: Object, 102 | disableSubmitButtonOnError?: boolean, 103 | onFieldValidation?: Function, 104 | onSubmit: Function, 105 | onValuesChange?: void | Function, 106 | style?: Object, 107 | validation?: { 108 | messageMap?: Object, 109 | messageMapKeyPrefix?: string, 110 | requiredValidatorName?: string | boolean, 111 | validators?: Object, 112 | validate?: Function, 113 | validateInputOnBlur?: boolean, 114 | }, 115 | validations: Object, 116 | id?: Object, 117 | method?: Object, 118 | action?: Object, 119 | name?: Object, 120 | }; 121 | 122 | type State = { 123 | disableSubmitButton: boolean, 124 | fields: Object, 125 | }; 126 | 127 | export default class Form extends React.Component { 128 | static defaultProps = { 129 | activeStep: 0, 130 | autoComplete: 'off', 131 | className: undefined, 132 | disableSubmitButtonOnError: true, 133 | onFieldValidation: undefined, 134 | onValuesChange: undefined, 135 | style: {}, 136 | validation: {}, 137 | validations: {}, 138 | id: undefined, 139 | method: undefined, 140 | action: undefined, 141 | name: undefined, 142 | } 143 | 144 | // eslint-disable-next-line react/sort-comp 145 | onValuesChange: void 146 | 147 | constructor(props: Object) { 148 | super(props) 149 | 150 | this.onValuesChange = props.onValuesChange 151 | this.validation = Object.assign(this.validation, props.validation) 152 | this.state = { 153 | disableSubmitButton: false, 154 | fields: {}, 155 | } 156 | } 157 | 158 | static getDerivedStateFromProps(nextProps: Object, prevState: Object) { 159 | const { fields } = prevState 160 | 161 | if (!_.isEmpty(fields)) { 162 | // add validations to fields 163 | _.each(nextProps.validations, (validations, name) => { 164 | if (_.has(fields, name)) { 165 | fields[name].validations = validations 166 | } else { 167 | // eslint-disable-next-line no-console 168 | console.warn(`validations field "${name}" does not exist`) 169 | } 170 | }) 171 | return { fields } 172 | } 173 | return null 174 | } 175 | 176 | validation = { 177 | messageMap, 178 | messageMapKeyPrefix: '', 179 | requiredValidatorName: validationConstants.REQUIRED_VALIDATOR_NAME, 180 | validators: defaultValidators, 181 | validate, 182 | validateInputOnBlur: false, 183 | } 184 | 185 | onFieldConstruct = (fieldProps: Object) => { 186 | const { 187 | checked, 188 | name, 189 | required, 190 | value, 191 | } = fieldProps 192 | 193 | // checkable input 194 | if (checked === true) { 195 | _.defer(() => { 196 | this.setState({ 197 | fields: { 198 | ...this.state.fields, 199 | [name]: { 200 | ...getFieldTemplate(), 201 | checked: checked || false, 202 | step: this.props.activeStep, 203 | value, 204 | }, 205 | }, 206 | }) 207 | }) 208 | // other inputs 209 | } else if (!_.isBoolean(checked)) { 210 | const { requiredValidatorName } = this.validation 211 | if (!_.has(this.state.fields, name)) { 212 | const validators = extractFieldValidators(fieldProps) 213 | 214 | if (required && !_.isEmpty(requiredValidatorName)) { 215 | validators.unshift(requiredValidatorName) 216 | } 217 | const isRequired = required || validators.includes(requiredValidatorName) 218 | // set any validations on first construct 219 | let validations = [] 220 | if (!_.has(this.state.fields, name) 221 | && _.has(this.props.validations, name) 222 | ) { 223 | validations = this.props.validations[name] 224 | } 225 | 226 | _.defer(() => { 227 | this.setState({ 228 | fields: { 229 | ...this.state.fields, 230 | [name]: { 231 | ...getFieldTemplate(), 232 | isRequired, 233 | pristineValue: value, 234 | step: this.props.activeStep, 235 | validators, 236 | validations, 237 | value, 238 | }, 239 | }, 240 | }) 241 | 242 | if (!_.isEmpty(value)) { 243 | this.validateField(name, value) 244 | } 245 | }) 246 | } 247 | } 248 | } 249 | 250 | onFieldValueChange = (name: string, value: any, isDirty: boolean = false) => { 251 | _.defer(() => { 252 | this.setState({ 253 | fields: { 254 | ...this.state.fields, 255 | [name]: { 256 | ...this.state.fields[name], 257 | isDirty: isDirty || this.state.fields[name].isDirty, 258 | isPristine: false, 259 | validations: [], 260 | value, 261 | }, 262 | }, 263 | }) 264 | 265 | if (isValidForm(this.state.fields)) { 266 | this.enableSubmitButton(); 267 | } 268 | 269 | if (this.onValuesChange !== undefined) { 270 | this.onValuesChange( 271 | getFieldValues(this.state.fields), 272 | getPristineFieldValues(this.state.fields) 273 | ) 274 | } 275 | 276 | if (this.state.fields[name].isDirty) { 277 | this.validateField(name, value) 278 | } 279 | }) 280 | } 281 | 282 | onFieldToggle = (name: string, value: any, checked: boolean) => { 283 | this.setState({ 284 | fields: { 285 | ...this.state.fields, 286 | [name]: { 287 | ...this.state.fields[name], 288 | checked, 289 | isPristine: false, 290 | validations: [], 291 | value, 292 | }, 293 | }, 294 | }) 295 | } 296 | 297 | validateField = (name: string, value: any) => { 298 | const field = this.state.fields[name] 299 | 300 | if (!(field.value === '' && !field.isRequired) && !_.isEmpty(field.validators)) { 301 | const { validation } = this 302 | const validations = validation.validate(value, field.validators, validation) 303 | 304 | // update state 305 | field.validations = validations 306 | this.setState({ 307 | fields: { 308 | ...this.state.fields, 309 | [name]: field, 310 | }, 311 | }) 312 | // disable submit button 313 | if (!_.isEmpty(validations)) { 314 | this.disableSubmitButton() 315 | } 316 | // propogate validation 317 | if (this.props.onFieldValidation !== undefined) { 318 | let errorSteps 319 | if (field.step !== undefined) { 320 | errorSteps = deriveErrorSteps(this.state.fields) 321 | } 322 | this.props.onFieldValidation(field, errorSteps) 323 | } 324 | } 325 | } 326 | 327 | reset = () => { 328 | const { fields } = this.state 329 | _.defer(() => { 330 | _.each(fields, (field, name) => { 331 | this.setState({ 332 | fields: { 333 | ...this.state.fields, 334 | [name]: { 335 | ...this.state.fields[name], 336 | isDirty: false, 337 | isPristine: true, 338 | value: '', 339 | }, 340 | }, 341 | }) 342 | }) 343 | }) 344 | } 345 | 346 | submit = (event: Event) => { 347 | event.preventDefault() 348 | let isValid = true 349 | const { fields } = this.state 350 | 351 | _.each(fields, (field, name) => { 352 | if (field.isRequired && field.value === '') { 353 | this.validateField(name, '') 354 | isValid = false 355 | } 356 | }) 357 | if (isValid) { 358 | this.props.onSubmit( 359 | getFieldValues(fields), 360 | getPristineFieldValues(fields) 361 | ) 362 | } 363 | } 364 | 365 | enableSubmitButton() { 366 | if (this.state.disableSubmitButton) { 367 | this.setState({ disableSubmitButton: false }) 368 | } 369 | } 370 | 371 | disableSubmitButton() { 372 | if (this.props.disableSubmitButtonOnError) { 373 | this.setState({ disableSubmitButton: true }) 374 | } 375 | } 376 | 377 | deleteRow = (row: string) => { 378 | const pos: number = row.indexOf('[') 379 | const rowName: string = row.substr(0, pos) 380 | const rowIndex: number = parseInt(row.substr(pos + 1), 10) 381 | 382 | const { fields } = this.state 383 | _.each(fields, (field, fieldName) => { 384 | if (fieldName.startsWith(row)) { 385 | delete fields[fieldName] 386 | } else if (fieldName.startsWith(rowName)) { 387 | const index = parseInt(fieldName.substr(pos + 1), 10) 388 | if (index > rowIndex) { 389 | const newRow = fieldName.replace(/\[\d+\]/, `[${(index - 1)}]`) 390 | delete fields[fieldName] 391 | fields[newRow] = field 392 | } 393 | } 394 | }) 395 | 396 | this.setState({ fields }) 397 | } 398 | 399 | cloneChildrenRecursively(children: any): any { 400 | return React.Children.map(children, (child) => { 401 | if (_.isEmpty(child)) { 402 | return null 403 | } 404 | if (_.isString(child)) { 405 | return child 406 | } 407 | 408 | const isFieldElement = verifyFieldElement(child) 409 | const nestedChildren = _.isArray(child.props.children) && !isFieldElement 410 | ? _.filter(child.props.children, v => (_.isObject(v) || _.isString(v))) 411 | : false 412 | 413 | // nested elements 414 | if (nestedChildren !== false) { 415 | // FormControl element with field/group name-value props 416 | if (child.type === FormControl) { 417 | const fieldElement = nestedChildren.find(el => 418 | ![FormLabel, InputLabel, FormHelperText].includes(el.type) 419 | && el.props.name !== undefined 420 | && el.props.value !== undefined) 421 | if (fieldElement !== undefined) { 422 | const { name } = fieldElement.props 423 | return ( 424 | 431 | ) 432 | } 433 | } 434 | // non-FormControl element 435 | return ( 436 | React.cloneElement(child, { 437 | children: this.cloneChildrenRecursively(nestedChildren), 438 | }) 439 | ) 440 | } 441 | // add disable functionality to submit button 442 | if (child.props.type === 'submit') { 443 | return React.cloneElement(child, { 444 | disabled: this.state.disableSubmitButton, 445 | }) 446 | // non-interactive elements should be rendered as is 447 | } else if (!isFieldElement) { 448 | // delete row button 449 | if (child.props[propNames.DELETE_FIELD_ROW] !== undefined) { 450 | return () 454 | } 455 | // any other element 456 | return child 457 | } 458 | // clone control label 459 | if (child.type === FormControlLabel) { 460 | const { name } = child.props.control.props 461 | return ( 462 | 470 | ) 471 | } 472 | // clone input element 473 | const { name } = child.props 474 | 475 | // checkable 476 | if (child.type === Checkbox || child.type === Switch) { 477 | return ( 478 | 485 | ) 486 | } 487 | 488 | return ( 489 | 498 | ) 499 | }) 500 | } 501 | 502 | render() { 503 | return ( 504 |
515 | {this.cloneChildrenRecursively(this.props.children)} 516 |
517 | ) 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /src/components/FormControlClone.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import _ from 'lodash' 5 | 6 | import FormControl from '@material-ui/core/FormControl' 7 | import FormHelperText from '@material-ui/core/FormHelperText' 8 | import FormLabel from '@material-ui/core/FormLabel' 9 | import InputLabel from '@material-ui/core/InputLabel' 10 | 11 | import propNames from '../propNames' 12 | 13 | 14 | function getErrorAndHelperText(field: Object): Object { 15 | let helperText: ?string 16 | let isError: boolean = false 17 | if (!_.isEmpty(field) && field.validations.length > 0) { 18 | helperText = field.validations[0].message 19 | isError = true 20 | } 21 | return { helperText, isError } 22 | } 23 | 24 | type Props = { 25 | field?: Object, 26 | formControlComp: Object, 27 | onValueChange: Function, 28 | onConstruct: Function, 29 | }; 30 | 31 | type State = { 32 | helperText: ?string, 33 | isError: boolean, 34 | value: mixed, 35 | }; 36 | 37 | export default class FormControlClone extends React.Component { 38 | static defaultProps = { 39 | field: {}, 40 | } 41 | 42 | // eslint-disable-next-line react/sort-comp 43 | helperText: string 44 | name: string 45 | 46 | constructor(props: Object) { 47 | super(props) 48 | 49 | const { error, required } = props.formControlComp.props 50 | 51 | let name 52 | let value 53 | let helperText 54 | let isError = error 55 | 56 | React.Children.forEach(props.formControlComp.props.children, (child) => { 57 | if (child.type === FormHelperText) { 58 | helperText = String(child.props.children) 59 | this.helperText = helperText 60 | } else if (child.type !== FormLabel 61 | && child.type !== InputLabel 62 | && child.props.name !== undefined 63 | && child.props.value !== undefined 64 | ) { 65 | name = child.props.name // eslint-disable-line prefer-destructuring 66 | value = child.props.value // eslint-disable-line prefer-destructuring 67 | } 68 | }) 69 | 70 | if (props.formControlComp.type !== FormControl 71 | || name === undefined 72 | || value === undefined 73 | ) { 74 | throw new Error('invalid FormControl control children') 75 | } 76 | 77 | if (props.field.value === undefined) { 78 | const validatorsPropName = propNames.FIELD_VALIDATORS 79 | props.onConstruct({ 80 | name, 81 | value, 82 | required, 83 | [validatorsPropName]: props.formControlComp.props[validatorsPropName], 84 | }) 85 | } else { 86 | value = props.field.value // eslint-disable-line prefer-destructuring 87 | if (!_.isEmpty(props.field) && props.field.validations.length > 0) { 88 | const fieldError = getErrorAndHelperText(props.field) 89 | helperText = fieldError.helperText // eslint-disable-line prefer-destructuring 90 | isError = fieldError.isError // eslint-disable-line prefer-destructuring 91 | } 92 | } 93 | 94 | this.name = name 95 | this.state = { 96 | helperText, 97 | isError, 98 | value, 99 | } 100 | } 101 | 102 | // eslint-disable-next-line 103 | UNSAFE_componentWillReceiveProps(nextProps: Object) { 104 | if (!_.isEmpty(nextProps.field)) { 105 | const { helperText, isError } = getErrorAndHelperText(nextProps.field) 106 | this.setState({ 107 | helperText, 108 | isError, 109 | value: nextProps.field.value, 110 | }) 111 | } 112 | } 113 | 114 | onChange = (event: SyntheticInputEvent) => { 115 | const { value } = event.target 116 | this.setState({ isError: false, helperText: this.helperText, value }) 117 | this.props.onValueChange(this.name, value, true) 118 | } 119 | 120 | render() { 121 | const { formControlComp, formControlComp: { props } } = this.props 122 | 123 | let hasHelperText = false 124 | const children = React.Children.map(props.children, (child) => { 125 | // label 126 | if (child.type === FormLabel || child.type === InputLabel) { 127 | return child 128 | } 129 | // helper text 130 | if (child.type === FormHelperText) { 131 | hasHelperText = true 132 | return React.cloneElement(child, { 133 | children: this.state.helperText, 134 | }) 135 | } 136 | // field 137 | return React.cloneElement(child, { 138 | onChange: child.props.onChange || this.onChange, 139 | value: this.state.value, 140 | }) 141 | }) 142 | // support for dynamic helper text 143 | if (!hasHelperText && this.state.helperText !== undefined) { 144 | children.push({this.state.helperText}) 145 | } 146 | 147 | return ( 148 | React.cloneElement(formControlComp, { 149 | error: this.state.isError, 150 | children, 151 | }) 152 | ) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/components/FormControlLabelClone.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import _ from 'lodash' 5 | 6 | import Checkbox from '@material-ui/core/Checkbox' 7 | import Switch from '@material-ui/core/Switch' 8 | import FormControlLabel from '@material-ui/core/FormControlLabel' 9 | 10 | 11 | type Props = { 12 | control: Object, 13 | field?: Object, 14 | label: string, 15 | onToggle: Function, 16 | onConstruct: Function, 17 | }; 18 | 19 | type State = { 20 | checked: boolean, 21 | value: string, 22 | }; 23 | 24 | export default class FormControlLabelClone extends React.Component { 25 | static defaultProps = { 26 | field: {}, 27 | } 28 | 29 | constructor(props: Object) { 30 | super(props) 31 | 32 | if (![Checkbox, Switch].includes(props.control.type)) { 33 | throw new Error('invalid FormControlLabel control component') 34 | } 35 | 36 | let { checked } = props.control.props 37 | const { value } = props.control.props 38 | 39 | if (props.field.value === undefined) { 40 | props.onConstruct(props.control.props) 41 | } else { 42 | checked = _.get(props.field, 'checked') 43 | } 44 | 45 | this.state = { 46 | checked, 47 | value, 48 | } 49 | } 50 | 51 | onToggle = (event: SyntheticInputEvent, checked: boolean) => { 52 | checked = _.get(event, 'target.checked') || checked 53 | let { value } = this.props.control.props // eslint-disable-line react/prop-types 54 | const { name } = this.props.control.props // eslint-disable-line react/prop-types 55 | value = checked ? value : '' 56 | this.setState({ checked, value }) 57 | this.props.onToggle(name, value, checked) 58 | } 59 | 60 | render() { 61 | const { control, label } = this.props 62 | const onChange = ( 63 | control.props.onChange || control.props.onToggle || this.onToggle 64 | ) 65 | const controlOptions = { 66 | checked: this.state.checked, 67 | onChange, 68 | value: this.state.value, 69 | } 70 | 71 | return ( 72 | 79 | ) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/components/__tests__/CheckableFieldClone.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import toJson from 'enzyme-to-json' 4 | 5 | import Checkbox from '@material-ui/core/Checkbox' 6 | 7 | import CheckableFieldClone from '../CheckableFieldClone' 8 | 9 | 10 | const field = { 11 | isPristine: true, 12 | isRequired: null, 13 | pristineValue: null, 14 | validations: [], 15 | validators: [], 16 | value: undefined, 17 | } 18 | 19 | describe(':', () => { 20 | const wrapper = shallow( 21 | )} 29 | onConstruct={jest.fn()} 30 | onToggle={jest.fn()} 31 | /> 32 | ) 33 | 34 | it('should render', () => { 35 | expect(toJson(wrapper)).toMatchSnapshot() 36 | }) 37 | 38 | it('should handle onToggle events', () => { 39 | const checked = false 40 | const event = { target: { checked } } 41 | wrapper.simulate('change', event) 42 | expect(wrapper.instance().props.onToggle).toHaveBeenCalled() 43 | }) 44 | }) 45 | 46 | describe(' Invalid props', () => { 47 | it('should throw if control type is other than Checkbox or Switch', () => { 48 | let error 49 | try { 50 | shallow( 51 | } 54 | onConstruct={jest.fn()} 55 | onToggle={jest.fn()} 56 | /> 57 | ) 58 | } catch (e) { 59 | error = e 60 | } 61 | expect(error).toBeInstanceOf(Error) 62 | }) 63 | 64 | it('should throw if control type fieldComp has no name or value props', () => { 65 | let error 66 | try { 67 | shallow( 68 | )} 74 | onConstruct={jest.fn()} 75 | onToggle={jest.fn()} 76 | /> 77 | ) 78 | } catch (e) { 79 | error = e 80 | } 81 | expect(error).toBeInstanceOf(Error) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /src/components/__tests__/FieldClone.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import toJson from 'enzyme-to-json' 4 | 5 | import TextField from '@material-ui/core/TextField' 6 | 7 | import FieldClone from '../FieldClone' 8 | 9 | 10 | describe(':', () => { 11 | const field = { 12 | isPristine: true, 13 | isRequired: null, 14 | pristineValue: null, 15 | validations: [], 16 | validators: [], 17 | value: undefined, 18 | } 19 | 20 | const wrapper = shallow( 21 | 33 | )} 34 | onConstruct={jest.fn()} 35 | onValueChange={jest.fn()} 36 | useNativeRequiredValidator={false} 37 | validateInputOnBlur 38 | /> 39 | ) 40 | 41 | it('should render', () => { 42 | expect(toJson(wrapper)).toMatchSnapshot() 43 | }) 44 | 45 | it('should not throw if field type prop is undefined', () => { 46 | function checkTypeName() { 47 | if (wrapper.instance().props.fieldComp.type.name === undefined) { 48 | throw new Error('FieldClone does not support native elements') 49 | } 50 | } 51 | expect(checkTypeName).not.toThrow() 52 | }) 53 | 54 | it('should not throw if field component name and value are defined', () => { 55 | function testNameAndValueProps() { 56 | const { name, value } = wrapper.instance().props.fieldComp.props 57 | if (name === undefined || value === undefined) { 58 | throw new Error('FieldClone name and value must be defined') 59 | } 60 | } 61 | expect(testNameAndValueProps).not.toThrow() 62 | }) 63 | 64 | it('should set state', () => { 65 | expect(wrapper.state()).toMatchObject({ 66 | helperText: undefined, 67 | isError: false, 68 | value: undefined, 69 | }) 70 | }) 71 | 72 | it('should call onConstruct', () => { 73 | expect(wrapper.instance().props.onConstruct).toHaveBeenCalled() 74 | }) 75 | 76 | it('should have a rendered child with an undefined value prop', () => { 77 | expect(wrapper.prop('value')).toBeUndefined() 78 | }) 79 | 80 | it('should have a rendered label', () => { 81 | expect(wrapper.prop('label')).toBeDefined() 82 | }) 83 | 84 | it('should handle onChange events', () => { 85 | const value = undefined 86 | const event = { target: { value } } 87 | wrapper.find(TextField).simulate('change', event) 88 | expect(wrapper.state()).toMatchObject({ 89 | helperText: undefined, 90 | isError: false, 91 | value, 92 | }) 93 | }) 94 | 95 | it('should handle onBlur events', () => { 96 | const value = 'x' 97 | const event = { target: { value } } 98 | const { onValueChange } = wrapper.instance().props 99 | wrapper.find(TextField).simulate('blur', event) 100 | expect(onValueChange).toBeCalledWith(wrapper.prop('name'), value, true) 101 | }) 102 | 103 | it('should update props', () => { 104 | const value = 'x' 105 | wrapper.setProps({ field: { value, validations: [] } }) 106 | expect(wrapper.instance().props.field.validations).toBeDefined() 107 | expect(wrapper.state('value')).toEqual(value) 108 | }) 109 | }) 110 | 111 | describe(': 24 | 25 | ) 26 | 27 | it('should render', () => { 28 | expect(toJson(wrapper)).toMatchSnapshot() 29 | }) 30 | }) 31 | 32 | describe('
(no props)', () => { 33 | const validations = { name: [{ code: 'isAlpha', message: 'invalid' }] } 34 | const wrapper = shallow( 35 | 39 | 47 | 48 | 49 | 50 | ) 51 | 52 | it('should render', () => { 53 | expect(toJson(wrapper)).toMatchSnapshot() 54 | }) 55 | 56 | it('should have submit button enabled', () => { 57 | expect(wrapper.instance().props.disableSubmitButtonOnError).toEqual(true) 58 | }) 59 | }) 60 | 61 | describe('
(all props)', () => { 62 | validators.isBorat = value => value === 'borat' 63 | const customMessageMap = Object.assign(messageMap, { 64 | isBorat: 'NAAAAAT! You can only write "borat" lol', 65 | }) 66 | const validation = { 67 | messageMap: customMessageMap, 68 | requiredValidatorName: false, 69 | validators, 70 | } 71 | const validations = { name: [{ code: 'isAlpha', message: 'invalid' }] } 72 | 73 | const wrapper = shallow( 74 | 82 | {'fill out the form!'} 83 | 90 |
91 | 92 | FooBar 93 | } 95 | label="I love love" 96 | /> 97 | 98 |
99 | 100 | Age 101 | 108 | 109 | 110 | 111 | ) 112 | 113 | it('should render', () => { 114 | expect(toJson(wrapper)).toMatchSnapshot() 115 | }) 116 | 117 | it('should have set validation', () => { 118 | expect(wrapper.instance().validation).toHaveProperty('messageMap') 119 | }) 120 | 121 | it('should set state', () => { 122 | expect(wrapper.state()).toMatchObject({ 123 | disableSubmitButton: false, 124 | fields: {}, 125 | }) 126 | }) 127 | 128 | it('should have 2 children', () => { 129 | expect(wrapper.children()).toHaveLength(5) 130 | }) 131 | 132 | it('should handle submit event', () => { 133 | const { onSubmit } = wrapper.instance().props 134 | const event = new Event('submit') 135 | wrapper.simulate('submit', event) 136 | expect(onSubmit).toHaveBeenCalled() 137 | }) 138 | 139 | it('should have submit button disabled', () => { 140 | expect(wrapper.instance().props.disableSubmitButtonOnError).toEqual(false) 141 | expect(wrapper.state('disableSubmitButton')).toEqual(false) 142 | }) 143 | }) 144 | -------------------------------------------------------------------------------- /src/components/__tests__/FormControlClone.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import toJson from 'enzyme-to-json' 4 | 5 | import FormControl from '@material-ui/core/FormControl' 6 | import FormControlLabel from '@material-ui/core/FormControlLabel' 7 | import FormHelperText from '@material-ui/core/FormHelperText' 8 | import FormLabel from '@material-ui/core/FormLabel' 9 | import Radio from '@material-ui/core/Radio' 10 | import RadioGroup from '@material-ui/core/RadioGroup' 11 | import InputLabel from '@material-ui/core/InputLabel' 12 | import Select from '@material-ui/core/Select' 13 | import MenuItem from '@material-ui/core/MenuItem' 14 | 15 | import FormControlClone from '../FormControlClone' 16 | 17 | 18 | const field = { 19 | isPristine: true, 20 | isRequired: null, 21 | pristineValue: null, 22 | validations: [], 23 | validators: [], 24 | value: undefined, 25 | } 26 | 27 | describe(': 32 | Please select your age ... 33 | Teens 34 | Twenties 35 | Thirties 36 | Fourties + 37 | 38 | Some important helper text 39 | 40 | ) 41 | 42 | const wrapper = shallow( 43 | 49 | ) 50 | 51 | it('should render', () => { 52 | expect(toJson(wrapper)).toMatchSnapshot() 53 | }) 54 | 55 | /* TODO: how to simulate on children? */ 56 | // it('should handle onChange events of children', () => { 57 | // const value = '10' 58 | // const event = { target: { value } } 59 | // wrapper.find(FormControlClone).simulate('change', event) 60 | // expect(wrapper.state()).toMatchObject({ 61 | // isError: false, 62 | // value, 63 | // }) 64 | // expect(wrapper.instance().props.onValueChange).toHaveBeenCalled() 65 | // }) 66 | }) 67 | 68 | describe(':', () => { 69 | const formControlComp = ( 70 | 74 | 75 | RadioGroup FormControl 76 | 77 | 81 | } 84 | label="I swear" 85 | /> 86 | } 89 | label="Probably" 90 | /> 91 | } 94 | label="Maybe" 95 | /> 96 | 97 | 98 | ) 99 | 100 | const wrapper = shallow( 101 | 107 | ) 108 | 109 | it('should render', () => { 110 | expect(toJson(wrapper)).toMatchSnapshot() 111 | }) 112 | }) 113 | 114 | describe(' Invalid props', () => { 115 | it('should throw if formControlComp type is other than FormControl', () => { 116 | let error 117 | try { 118 | shallow( 119 | } 122 | onConstruct={jest.fn()} 123 | onValueChange={jest.fn()} 124 | /> 125 | ) 126 | } catch (e) { 127 | error = e 128 | } 129 | expect(error).toBeInstanceOf(Error) 130 | }) 131 | 132 | it('should throw if formControlComp group has no name or value props', () => { 133 | const formControlComp = ( 134 | 135 | Age 136 | 140 | Some important helper text 141 | 142 | ) 143 | 144 | let error 145 | try { 146 | shallow( 147 | 153 | ) 154 | } catch (e) { 155 | error = e 156 | } 157 | expect(error).toBeInstanceOf(Error) 158 | }) 159 | }) 160 | -------------------------------------------------------------------------------- /src/components/__tests__/FormControlLabelClone.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import toJson from 'enzyme-to-json' 4 | 5 | import Checkbox from '@material-ui/core/Checkbox' 6 | import Switch from '@material-ui/core/Switch' 7 | 8 | import FormControlLabelClone from '../FormControlLabelClone' 9 | 10 | 11 | const field = { 12 | isPristine: true, 13 | isRequired: null, 14 | pristineValue: null, 15 | validations: [], 16 | validators: [], 17 | value: undefined, 18 | } 19 | 20 | describe(':', () => { 21 | const wrapper = shallow( 22 | } 24 | field={field} 25 | label="I love love" 26 | onConstruct={jest.fn()} 27 | onToggle={jest.fn()} 28 | /> 29 | ) 30 | 31 | it('should render', () => { 32 | expect(toJson(wrapper)).toMatchSnapshot() 33 | }) 34 | 35 | it('should not throw if field type prop is Checkbox or Switch', () => { 36 | function checkType() { 37 | if (![Checkbox, Switch].includes(wrapper.instance().props.control.type)) { 38 | throw new Error('FieldClone does not support native elements') 39 | } 40 | } 41 | expect(checkType).not.toThrow() 42 | }) 43 | 44 | it('should call onConstruct', () => { 45 | expect(wrapper.instance().props.onConstruct).toHaveBeenCalled() 46 | }) 47 | 48 | it('should set state', () => { 49 | const { control } = wrapper.instance().props 50 | expect(wrapper.state()).toMatchObject({ 51 | checked: control.props.checked, 52 | value: control.props.value, 53 | }) 54 | }) 55 | 56 | it('should handle onToggle events', () => { 57 | const checked = true 58 | const event = { target: { checked } } 59 | wrapper.simulate('change', event) 60 | expect(wrapper.state()).toMatchObject({ 61 | checked, 62 | value: wrapper.state('value'), 63 | }) 64 | expect(wrapper.instance().props.onToggle).toHaveBeenCalled() 65 | }) 66 | }) 67 | 68 | describe(': (value defined)', () => { 69 | field.value = 'x' 70 | shallow( 71 | } 73 | field={field} 74 | label="I love love" 75 | onConstruct={jest.fn()} 76 | onToggle={jest.fn()} 77 | /> 78 | ) 79 | }) 80 | 81 | describe(' Invalid props', () => { 82 | it('should throw if control type is other than Checkbox or Switch', () => { 83 | let error 84 | try { 85 | shallow( 86 | } 88 | field={field} 89 | label="I love love" 90 | onConstruct={jest.fn()} 91 | onToggle={jest.fn()} 92 | /> 93 | ) 94 | } catch (e) { 95 | error = e 96 | } 97 | expect(error).toBeInstanceOf(Error) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/CheckableFieldClone.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`: should render 1`] = ` 4 | 10 | `; 11 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/FieldClone.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`: 184 | 185 | `; 186 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/FormControlClone.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`: should render 1`] = ` 4 | 8 | 12 | RadioGroup FormControl 13 | 14 | 20 | } 22 | label="I swear" 23 | value="high" 24 | /> 25 | } 27 | label="Probably" 28 | value="soso" 29 | /> 30 | } 32 | label="Maybe" 33 | value="low" 34 | /> 35 | 36 | 37 | `; 38 | 39 | exports[`: