├── .babelrc ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CNAME ├── LICENSE ├── README.md ├── _config.yml ├── docs ├── API.md ├── FAQ.md └── Recipes.md ├── example ├── .babelrc ├── Components │ ├── AdvancedDeepNestedData.js │ ├── AdvancedInstantValidation.js │ ├── AdvancedNoInstantValidation.js │ ├── AsyncForm.js │ ├── Basic.js │ ├── BasicWithIsValid.js │ ├── BasicWithoutSubmitButton.js │ ├── Form.js │ ├── FormWithoutSubmitButton.js │ ├── ServerSideErrors.js │ ├── SetAsyncErrorsExample.js │ ├── SimpleForm.js │ └── createErrorMessage.js ├── app.js ├── demo │ ├── bundle.js │ ├── bundle.js.map │ └── index.html ├── helpers.js ├── index.html ├── index.template.html ├── package.json ├── validationRules.js ├── webpack.config.demo.js ├── webpack.config.js └── yarn.lock ├── package.json ├── rollup.config.js ├── src ├── Revalidation.js ├── constants.js ├── helpers │ ├── debounce.js │ └── getValue.js ├── index.js ├── updaters │ ├── index.js │ ├── types.js │ ├── updateFormValues.js │ └── updateSyncErrors.js ├── utils │ ├── index.js │ └── isValid.js └── validate.js ├── test ├── revalidation.test.js ├── updaters │ ├── updateFormValues.test.js │ └── updateSyncErrors.test.js └── utils │ └── isValid.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": ["transform-flow-strip-types", "transform-object-rest-spread", "ramda"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "parser": "babel-eslint", 4 | "parserOptions": { 5 | "ecmaVersion": 7, 6 | "sourceType": "module", 7 | "ecmaFeatures": { 8 | "impliedStrict": true, 9 | "experimentalObjectRestSpread": true 10 | } 11 | }, 12 | "rules": { 13 | "arrow-parens": 0, 14 | "class-methods-use-this": 0, 15 | "import/no-extraneous-dependencies": 0, 16 | "no-confusing-arrow": 0, 17 | "no-duplicate-imports": 0, 18 | "no-else-return": 0, 19 | "no-prototype-builtins": 0, 20 | "no-unused-vars": [2, { "varsIgnorePattern": "^_+$" }], 21 | "quote-props": 0, 22 | "semi": [2, "never"], 23 | "space-before-function-paren": 0, 24 | "symbol-description": 0, 25 | "valid-jsdoc": 0, 26 | "max-len": 0 27 | } 28 | } -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.* 3 | 4 | [include] 5 | 6 | [libs] 7 | 8 | [options] 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | dist 4 | npm-debug.log 5 | yarn-error.log 6 | example/node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | examples 3 | test 4 | .babelrc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | node_js: 4 | - "6" 5 | sudo: false 6 | script: 7 | - npm test -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | revalidation.oss.25th-floor.com -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 25th-floor GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Revalidation 2 | 3 | ### Higher Order Component for Forms in React 4 | 5 | 6 | __Revalidation__ lets you write your forms as __stateless function components__, taking care of managing the local form state 7 | as well as the validation. Revalidation also works with classes and will support other React-like libraries like __Preact__ or __Inferno__ 8 | in the future. 9 | 10 | ### Use Case 11 | Form handling sounds trivial sometimes, but let’s just take a second to think about what is involved in the process. 12 | We need to define form fields, we need to validate the fields, 13 | we also might need to display errors according to the fact if the input validates, 14 | furthermore we need to figure out if the validation is instant or only after clicking 15 | on a submit button and so on and so forth. 16 | 17 | ### Why Revalidation? 18 | There are a number of solutions already available in __React__ land, each with there own approach on how to tackle aforementioned problems. 19 | __Revalidation__ is another approach on taming the problems that come with forms by only doing two things: managing the 20 | local form component state and validating against a defined set of rules. There is no real configuration and very 21 | little is hidden away from user land. This approach has pros and cons obviously. The benefit we gain, but declaring an initial state 22 | and a set of rules is that we can reuse parts of the spec and compose those specs to bigger specs. The downside is that 23 | Revalidation doesn't abstract away the form handling itself. The only configurations available are `validateSingle` and 24 | `validateOnChange`, while the first enables to define if the predicates functions are against all fields or only that one updated, 25 | the latter enables to turn dynamic validation on and off all together. This is it. Everything is up to the form implementer. 26 | 27 | __Revalidation__ enhances the wrapped Component by passing a `revalidation` prop containing a number of properties and functions 28 | to manage the state. There are no automatic field updates, validations or _onsubmit_ actions, Revalidation doesn't know how 29 | the form is implemented or how it should handle user interactions. 30 | 31 | Let's see an example to get a better idea on how this could work. 32 | For example we would like to define a number of validation rules for two inputs, _name_ and _random_. 33 | More often that not, inside an `onChange(name, value)` f.e, we might start to hard code some rules and verify them against 34 | the provided input: 35 | 36 | ```js 37 | 38 | onChange(name, value) { 39 | if (name === 'lastName') { 40 | if (hasCapitalLetter(lastName)) { 41 | // then do something 42 | } 43 | } 44 | // etc... 45 | } 46 | 47 | ``` 48 | 49 | This example might be exaggerated but you get the idea. 50 | Revalidation takes care of running your predicate functions against defined field inputs, enabling to decouple the actual input from the predicates. 51 | 52 | 53 | ```javascript 54 | const validationRules = { 55 | name: [ 56 | [ isGreaterThan(5), 57 | `Minimum Name length of 6 is required.` 58 | ], 59 | ], 60 | random: [ 61 | [ isGreaterThan(7), 'Minimum Random length of 8 is required.' ], 62 | [ hasCapitalLetter, 'Random should contain at least one uppercase letter.' ], 63 | ] 64 | } 65 | ``` 66 | And imagine this is our input data. 67 | 68 | ```javascript 69 | const inputData = { name: 'abcdef', random: 'z'} 70 | ``` 71 | 72 | We would like to have a result that displays any possible errors. 73 | 74 | Calling validate `validate({inputData, validationRules)` 75 | should return 76 | ```javascript 77 | {name: true, 78 | random: [ 79 | 'Minimum Random length of 8 is required.', 80 | 'Random should contain at least one uppercase letter.' 81 | ]} 82 | ``` 83 | 84 | Revalidate does exactly that, by defining an initial state and the validation rules it takes care of updating and validating 85 | any React Form Component. Revalidate also doesn't know how your form is built or if it is even a form for that matter. 86 | This also means, a form library can be built on top Revalidation, making it a sort of meta form library. 87 | 88 | 89 | ### Getting started 90 | 91 | Install revalidation via npm or yarn. 92 | 93 | 94 | ``` 95 | npm install --save revalidation 96 | ``` 97 | 98 | ### Example 99 | 100 | We might have a stateless function component that receives a prop ___form___, which include the needed field values. 101 | 102 | ```javascript 103 | 104 | import React, {Component} from 'react' 105 | 106 | const Form = ({ form, onSubmit }) => 107 | ( 108 |
109 |
110 | 111 | 115 |
116 |
117 | 118 | 122 |
123 | 124 |
125 | ) 126 | 127 | ``` 128 | 129 | Next we might have a defined set of rules that we need to validate for given input values. 130 | 131 | ```javascript 132 | const validationRules = { 133 | name: [ 134 | [isNotEmpty, 'Name should not be empty.'] 135 | ], 136 | random: [ 137 | [isLengthGreaterThan(7), 'Minimum Random length of 8 is required.'], 138 | [hasCapitalLetter, 'Random should contain at least one uppercase letter.'], 139 | ] 140 | } 141 | 142 | ``` 143 | 144 | Further more we know about the inital form state, which could be empty field values. 145 | 146 | ```javascript 147 | const initialState = {password: '', random: ''} 148 | ``` 149 | 150 | Now that we have everything in place, we import Revalidation. 151 | 152 | ```js 153 | import Revalidation from 'revalidation' 154 | ``` 155 | Revalidation only needs the Component and returns a Higher Order Component accepting the following props: 156 | 157 | - __`initialState`__ *(Object)* 158 | 159 | - __`rules`__ *(Object)* 160 | 161 | - __`validateSingle`__ *(Boolean)* 162 | 163 | - __`validateOnChange`__: *(Boolean|Function)* 164 | 165 | - __`asyncErrors`__ *(Object)* 166 | 167 | - __`updateForm`__ *(Object)* 168 | 169 | 170 | ```js 171 | 172 | const enhancedForm = revalidation(Form) 173 | 174 | // inside render f.e. 175 | 176 | submitted} 185 | */} 186 | /> 187 | 188 | ``` 189 | 190 | This enables us to rewrite our Form component, which accepts a ___revalidation___ prop now. 191 | 192 | ```js 193 | 194 | const createErrorMessage = (errorMsgs) => 195 | isValid(errorMsgs) ? null :
{head(errorMsgs)}
196 | 197 | const getValue = e => e.target.value 198 | 199 | const Form = ({ revalidation : {form, onChange, updateState, valid, errors = {}, onSubmit}, onSubmit: onSubmitCb }) => 200 | ( 201 |
202 |
203 | 204 | 210 |
{ createErrorMessage(errors.name) }
211 |
212 |
213 | 214 | 220 |
{ createErrorMessage(errors.random) }
221 |
222 | 223 |
224 | ) 225 | 226 | export default revalidation(Form) 227 | ``` 228 | 229 | revalidtion returns an object containing: 230 | - __form__: form values 231 | - __onChange__: a function expecting form name and value, additionally one can specify if the value and/or the validation should be updated and also accepts a callback function that will be run after an update has occurred. i.e. 232 | 233 | ```js 234 | onChange('name', 'foo') 235 | // or 236 | onChange('name', 'foo', [UPDATE_FIELD]) 237 | // or 238 | onChange('name', 'foo', null, ({valid, form}) => valid ? submitCb(form) : null ) 239 | ``` 240 | 241 | - __updateState__: a function expecting all the form values, f.e. Useful when wanting to reset the form. Depending on the setting either a validation will occur or not. 242 | 243 | 244 | ```js 245 | 246 | ``` 247 | 248 | - __valid__: calculated validation state, f.e. initially disabling the submit button when a form is rendered. 249 | - __submitted__: set to true once the form has been submitted. 250 | - __errors__: the errors object containing an array for every form field. 251 | - __onSubmit__: validates all fields at once, also accepts a callback function that will be called after the a validation state has been calculated. The callback function receives the current state including the valid state. 252 | 253 | ```js 254 | 259 | ``` 260 | 261 | - __updateErrors__: Enables to update any errors. 262 | 263 | 264 | - __updateAsyncErrors__: Enables to update any asynchronous errors. Useful when working with asynchronous validations. 265 | Pass the `updateAsyncErrors` to a callback, once the validation is finished set the result manually. 266 | 267 | ```js 268 | 271 | 272 | // use in another Component... 273 | class HigherUpComponent extends React.Component { 274 | onSubmit = (formValues, updateAsyncErrors) => { 275 | setTimeout(() => { 276 | // something went wrong... 277 | updateAsyncErrors({ name: ['Username is not available'] }) 278 | }, 1000) 279 | } 280 | 281 | render() { 282 | {/* ... */} 283 | } 284 | } 285 | ``` 286 | 287 | - __settings__: access the current settings: `{ validateOnChange: true, validateSingle: true }` 288 | 289 | Additionally revalidation offers a number of helper functions to quickly update any values or validations. 290 | 291 | - __debounce__: a helper function for triggering asynchronous validations. The passed in asynchronous validation can be debounced by a specified time. i.e. 1000 ms. 292 | 293 | ```js 294 | 299 | ``` 300 | 301 | - __updateValue__: update a specific field value. Important to note that no validation will run. Use _updateValueAndValidate_ if you need to update and validate of field. A name attribute must be defined on the element for _updateValue_ to update the value. 302 | 303 | ```js 304 | 311 | ``` 312 | 313 | - __validateValue__: validates a specific field. Useful when validation should happen after an *onBlur* i.e. 314 | A name attribute must be defined on the element for _validateValue_ to validate the value. 315 | 316 | ```js 317 | 325 | ``` 326 | 327 | - __updateValueAndValidate__: Updates and validates the value for the specified element. 328 | A name attribute must be defined on the element for _updateValueAndValidate_ to update the value. 329 | 330 | ```js 331 | 339 | ``` 340 | 341 | 342 | Where and how to display the errors and when and how to validate is responsibility of the form not Revalidation. 343 | Another aspect is that the form props can be updated when needed. 344 | 345 | __NOTE:__ `updateForm` should be used carefully and only when needed. Make sure to reset or remove `updateForm` after 346 | applying the new form values. 347 | 348 | 349 | ```javascript 350 |
354 | ``` 355 | 356 | Either define an initial state or use form props to define an actual form state. Revalidation will check for props first 357 | and then fallback to the initial state when none is found. 358 | 359 | __Revalidation__ also enables to pass in asynchronous error messages via the `asyncErrors` prop. As side effects are run outside of Revalidation itself, any error messages (from a dynamic validation or after submitting to a server and receiving errors) can be passed back into Revalidation. 360 | 361 | ```js 362 | 363 | // i.e. userNameExists is a function returning a promise and sends a request to validate if the username is available. 364 | 365 | 374 | 375 | ``` 376 | 377 | __NOTE:__ A sensible approach with asynchronous validation functions is useful, Revalidation will not run any effects against 378 | an input field. Needed consideration include: when to run the side effects 379 | (dynamically or on submit) and how often to trigger an async validation (immediately on every change or debounced) 380 | 381 | 382 | More: Revalidation also works with deep nested data structure (see the deep nested data example) 383 | 384 | check the [example](https://github.com/25th-floor/revalidation/tree/master/example) for more detailed insight into how to build more advanced forms, f.e. validating dependent fields. 385 | 386 | Clone the repository go to the examples folder and run the following commands: 387 | 388 | ```js 389 | yarn install 390 | npm start. 391 | ``` 392 | 393 | ### Demo 394 | Check the live [demo](http://revalidation.oss.25th-floor.com/example/demo/) 395 | 396 | ### Further Information 397 | 398 | For a deeper understanding of the underlying ideas and concepts: 399 | 400 | [Form Validation As A Higher Order Component Pt.1](https://medium.com/javascript-inside/form-validation-as-a-higher-order-component-pt-1-83ac8fd6c1f0) 401 | 402 | [Form Validation As A Higher Order Component Pt.2](https://medium.com/javascript-inside/form-validation-as-a-higher-order-component-pt-2-1edb7881870d) 403 | 404 | 405 | ### Credits 406 | Written by [A.Sharif](https://twitter.com/sharifsbeat) 407 | 408 | Original idea and support by [Stefan Oestreicher](https://twitter.com/thinkfunctional) 409 | 410 | Very special thanks to [Alex Booker](https://twitter.com/bookercodes) for providing input on the API and creating use cases. 411 | 412 | #### More 413 | __Revalidation__ is under development. 414 | 415 | The underlying synchronous validation is handled via [__Spected__](https://github.com/25th-floor/spected) 416 | 417 | #### Documentation 418 | [API](docs/API.md) 419 | 420 | [FAQ](docs/FAQ.md) 421 | 422 | ### License 423 | 424 | MIT 425 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # API 2 | ## `Revalidation(Component)` 3 | 4 | Creates an enhanced React Component containing validation and state keeping capabilities. 5 | 6 | #### Arguments 7 | 1. `Component` *(React Component)*: The React Component that will be enhanced with validation and state keeping functionality. 8 | 9 | 10 | #### Returns 11 | 12 | #### <HigherOrderComponentForm /> 13 | 14 | `Higher Order React Component`: The provided component will be enhanced with a [`revalidation`](#revalidation) prop. 15 | 16 | 17 | 18 | #### Props 19 | - __`initialState`__ *(Object)*: 20 | 21 | The initial state, make sure to provide defaults for all form fields. 22 | 23 | - __`rules`__ *(Object)*: 24 | 25 | An object of rules, consisting of arrays containing predicate function / error message tuple, f.e. `{name: [[a => a.length > 2, 'Minimum length 3.']]}` 26 | 27 | - __`validateSingle`__ *(Boolean)*: 28 | 29 | if you need validation per field you can set the option to true (default is false). 30 | 31 | - __`validateOnChange`__: *(Boolean|Function)*: 32 | 33 | if you need instant validation as soon as props have changed set to true (default is false). 34 | Pass in a function to enable dynamic validation after the form has been submitted: 35 | 36 | ```js 37 | submitted} /> 38 | ``` 39 | 40 | The provided function will receive the current state, enabling to set `validateOnChange` depending on that state. 41 | 42 | - __`asyncErrors`__ *(Object)*: 43 | 44 | An object containing asynchronous errors, f.e. `{userName: ['UserName is not available.']}` 45 | Enables to pass any asynchronous errors back into Revalidation. 46 | 47 | - __`updateForm`__ *(Object)*: 48 | 49 | Overrides the current form values. Only use in situations where `setState` isn't enough, add and remove `the updateForm` before and after 50 | updating the form values to avoid destroying the local component state when the component will receive new props. To initialize the 51 | state use `initialState`. `updateForm` is ignored when the Component is initially mounted. 52 | 53 | #### Example 54 | 55 | ```js 56 | import React from 'react' 57 | import Revalidation, { isValid } from 'revalidation' 58 | import { head } from 'ramda' 59 | 60 | import helpers from './helpers' 61 | 62 | const { 63 | isNotEmpty, 64 | isLengthGreaterThan, 65 | hasCapitalLetter, 66 | } = helpers 67 | 68 | const displayErrors = (errorMsgs) => 69 | isValid(errorMsgs) ? null :
{head(errorMsgs)}
70 | 71 | const getValue = e => e.target.value 72 | 73 | const Form = ({ revalidation : {form, validate, valid, errors = {}, onSubmit}, onSubmitCb }) => 74 | ( 75 |
76 |
77 | 78 | validate('name', getValue(e))} 82 | /> 83 | { errors.name } 84 |
85 |
86 | 87 | (validate('random', getValue(e))} 91 | /> 92 | { displayErrors(errors.random) } 93 |
94 | 95 |
96 | ) 97 | 98 | const EnhancedForm = Revalidation(Form) 99 | 100 | // ...usage 101 | 102 | const initialState = {name: '', random: ''} 103 | 104 | const validationRules = { 105 | name: [ 106 | [isNotEmpty, 'Name should not be empty.'] 107 | ], 108 | random: [ 109 | [isLengthGreaterThan(7), 'Minimum Random length of 8 is required.'], 110 | [hasCapitalLetter, 'Random should contain at least one uppercase letter.'], 111 | ] 112 | } 113 | 114 | 115 | 121 | 122 | 123 | ``` 124 | 125 | #### revalidation 126 | An additional prop `revalidation` is provided to the enhanced component. 127 | 128 | The following properties are provided by revalidation. 129 | 130 | - __`form`__ *(Object)*: 131 | 132 | Containing the current form values. f.e. input field name can be accessed via `form.name` 133 | 134 | ```js 135 | const Form = ({ revalidation : {form, onSubmit }, onSubmitCb) => 136 | ( 137 |
138 |
139 | 140 | 144 |
145 | 146 |
147 | ) 148 | ``` 149 | 150 | - __`valid`__ *(Boolean)*: 151 | 152 | A computed validation state. Useful when initializing the form and needing to disable a submit f.e. 153 | 154 | ```js 155 | 156 | ``` 157 | - __`submitted`__ *(Boolean)*: 158 | 159 | Set to true after the form has been submitted. 160 | 161 | - __`onChange(key, value, [type], [callback])`__ *(Function)*: 162 | 163 | Apply any changes to a field value and validate. 164 | 165 | ```js 166 | onChange('name', e.targetValue)} 170 | /> 171 | ``` 172 | Additionally one can pass in a callback function. 173 | 174 | ```js 175 | onChange('name', e.targetValue, null, ({valid, form}) => valid ? submitCb(form) : null )} 179 | /> 180 | ``` 181 | 182 | - __`updateState(form)`__ *(Function)*: 183 | 184 | Udpate the complete form state, f.e. implementing a clear function. 185 | 186 | ```js 187 | 188 | ``` 189 | 190 | - __`onSubmit([cb])`__ *(Function)*: 191 | 192 | Function for validating all fields. For example when submitting a form. 193 | To enable an action after successful validation, provide a `callback`. 194 | Thee current state (including errors) as well as the calculated valid state will be passed to the provided callback function. 195 | 196 | ```js 197 | 198 | ``` 199 | 200 | - __`errors`__ *(Object)*: 201 | 202 | The object containing the errors. How to display the errors isn't up to __Revalidation__. 203 | 204 | Initially `errors` might be an empty object, this is the case when no validation has run. 205 | 206 | Every field is mapped to list of error messages. 207 | 208 | ``` 209 | { 210 | name: [], 211 | random: ['Minimum Random length of 8 is required.'] 212 | } 213 | ``` 214 | 215 | Display the error messages as needed. This approach enables to define field specific error messages if needed. 216 | 217 | ```js 218 | 219 | const displayErrors = (errorMsgs) => 220 | isValid(errorMsgs) ? null :
{head(errorMsgs)}
221 | 222 |
223 | 224 | (onChange('random', getValue(e))} 228 | /> 229 | { displayErrors(errors.random) } 230 |
231 | ``` 232 | 233 | - __`updateErrors`__*(Function)*: Enables to update any errors. 234 | 235 | 236 | - __`updateAsyncErrors`__*(Function)*: Enables to update any asynchronous errors. Useful when working with asynchronous validations. 237 | Pass the `updateAsyncErrors` to a callback, once the validation is finished set the result manually. 238 | 239 | ```js 240 | 243 | 244 | // use in another Component... 245 | class HigherUpComponent extends React.Component { 246 | onSubmit = (formValues, updateAsyncErrors) => { 247 | setTimeout(() => { 248 | // something went wrong... 249 | updateAsyncErrors({ name: ['Username is not available'] }) 250 | }, 1000) 251 | } 252 | 253 | render() { 254 | {/* ... */} 255 | } 256 | } 257 | ``` 258 | 259 | - __`settings`__ *(Object)*: 260 | Access the current settings: `{ validateOnChange: true, validateSingle: true }` 261 | 262 | 263 | Additionally revalidation offers a number of helper functions to quickly update any values or validations. 264 | 265 | - __`debounce`__ *(Function)*: 266 | A helper function for triggering asynchronous validations. The passed in asynchronous validation can be debounced by a specified time. i.e. 1000 ms. 267 | 268 | ```js 269 | 274 | ``` 275 | 276 | - __`updateValue`__ *(Function)*: 277 | Update a specific field value. Important to note that no validation will run. Use _updateValueAndValidate_ if you need to update and validate of field. A name attribute must be defined on the element for _updateValue_ to update the value. 278 | 279 | ```js 280 | 287 | ``` 288 | 289 | - __`validateValue`__ *(Function)*: 290 | Validates a specific field. Useful when validation should happen after an *onBlur* i.e. 291 | A name attribute must be defined on the element for _validateValue_ to validate the value. 292 | 293 | ```js 294 | 302 | ``` 303 | 304 | - __`updateValueAndValidate`__ *(Function)*: 305 | Updates and validates the value for the specified element. 306 | A name attribute must be defined on the element for _updateValueAndValidate_ to update the value. 307 | 308 | ```js 309 | 317 | ``` 318 | 319 | 320 | === 321 | 322 | #### Helpers 323 | 324 | ##### isValid 325 | 326 | Use `isValid` to check if errors exist for an input. 327 | 328 | ```js 329 | 330 | 336 | 337 | ``` 338 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | #### How can I disable the submit button when initially rendering the form with empty values? 4 | Revalidation provides a computed `valid` property when passing in the __revalidation__ prop. 5 | Even when the form itself contains no errors yet, `valid` has the actual validation result. 6 | ```js 7 | const Form = ({ revalidation : {form, valid, errors = {}, validateAll}, onSubmit }) => 8 | ( 9 |
10 | {/* ...form fields */} 11 | 17 |
18 | ) 19 | ``` 20 | 21 | #### How can I write predicate functions where the result depends on other form fields? 22 | The predicate function receives two arguments, the actual input as well as an object containing all form field values. 23 | 24 | ```js 25 | 26 | const fieldsEqual = (a, all) => a === all.otherField 27 | 28 | const rules = { 29 | foo: 30 | [ 31 | [fieldsEqual, 'Not Equal'], 32 | ] 33 | } 34 | 35 | const state = {foo: 'foobar', otherField: 'foobar'} 36 | 37 | ``` 38 | 39 | #### How can I prevent empty optional fields from being validated? 40 | Don't provide any validation rules for the optional field. 41 | Revalidation only runs the validation against defined validation rules for given input. 42 | If no validation rules are provided the field will always be valid by default. 43 | 44 | #### How can I prevent Revalidation from validating all the fields as soon as I edit the first field? 45 | Via the `validateSingle` option, set to to true (which is also the default setting). 46 | ```js 47 | const EnhancedForm = revalidation(Form) 48 | 49 | 52 | 53 | ``` 54 | 55 | #### How can I only validate updated form elements without validating any untouched elements? 56 | Via the `validateSingle` option, set to to true (which is also the default setting). 57 | ```js 58 | const EnhancedForm = revalidation(Form) 59 | 60 | 63 | 64 | ``` 65 | 66 | #### How can I validate all fields at once? 67 | Revalidation provides a `validateAll` function via the __revalidation__ prop. 68 | 69 | ```js 70 | const Form = ({ revalidation : {form, valid, errors = {}, validateAll}, onSubmit }) => 71 | ( 72 |
73 | {/* ...form fields */} 74 | 77 |
78 | ) 79 | ``` 80 | 81 | #### How can I define a callback as soon as a validation is successful? 82 | Revalidation provides a `validateAll` function via the __revalidation__ prop. 83 | `validateAll` accepts a callback function as well as any data as arguments. 84 | If the validation is successful Revalidation will call the callback with either the provided data or the current form values. 85 | ```js 86 | const Form = ({ revalidation : {form, valid, errors = {}, validateAll}, onSubmit }) => 87 | ( 88 |
89 | {/* ...form fields */} 90 | 96 |
97 | ) 98 | ``` 99 | 100 | #### How can I instantly validate the form as soon as props have been updated? 101 | Via the `validateOnChange` option, set to to true (which is also the default setting). 102 | const EnhancedForm = revalidation(Form) 103 | 104 | 107 | 108 | ``` 109 | 110 | #### How can I trigger a success callback as soon as a field has been updated and is valid? 111 | Not implemented. If there is a need for covering this issue, we would like to know why. 112 | 113 | 114 | #### Can I define how the error is displayed for every field individually? 115 | Define how the errors should look like inside the Form itself. 116 | Once a validation has run at some point inside the form, the `error` object will contain an array with all error messages. 117 | 118 | ```js 119 | {name: [], random: ['something is missing', 'invalid data']} 120 | ``` 121 | 122 | Now you can do a manual error check per field basis and render the error message accordingly. 123 | ```js 124 | const Form = ({ revalidation : {form, validate, errors = {}, validateAll}, onSubmit }) => 125 | ( 126 |
127 |
128 | {/* some field */} 129 | { isValid(errors.name) ? null :
{errors.name.map((msg, i) => {msg})} } 130 |
131 |
132 | ) 133 | ``` 134 | 135 | 136 | #### How can I pass the current form values to a callback function even when the state is not valid? 137 | Define a callback function and pass in the current form state provided via the __revalidation__ `form` property. 138 | 139 | ```js 140 | const Form = ({ revalidation : {form, validate, valid, errors = {}, validateAll}, ownCallback }) => 141 | ( 142 |
143 |
144 | 145 | ownCallback(form)} 149 | /> 150 | { errors.name } 151 |
152 |
153 | ) 154 | ``` 155 | 156 | 157 | #### Why do I have to manually tell Revalidation when it should update any values? 158 | Revalidation doesn't know anything about the form structure or how it is designed. 159 | It only reacts to changes via passed in props or via `validate` and `validateAll` function calls. 160 | Revalidation only manages and validates the actual form state and provides the data to design your form accordingly. 161 | 162 | ```js 163 | const getValue = e => e.target.value 164 | 165 | const Form = ({ revalidation : {form, validate, errors = {}, validateAll}, onSubmit }) => 166 | ( 167 |
168 |
169 | 170 | updateState('name', e.target.value)} 174 | /> 175 | { errors.name } 176 |
177 |
178 | ) 179 | ``` 180 | 181 | #### Can I use React Classes instead Functions as Form Components? 182 | Yes, Revalidation also works with classes, there is no difference, although the use case is handling local state management and validation for stateless functional components. 183 | -------------------------------------------------------------------------------- /docs/Recipes.md: -------------------------------------------------------------------------------- 1 | # Recipes 2 | 3 | ### Handling Asynchronous Validations 4 | 5 | Coming Soon. 6 | 7 | #### Asynchronous Validation onChange 8 | 9 | Coming Soon. 10 | 11 | #### Asynchronous Validation onSubmit 12 | 13 | Coming Soon. -------------------------------------------------------------------------------- /example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": ["transform-flow-strip-types", "transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /example/Components/AdvancedDeepNestedData.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import revalidation from '../../src/' 4 | import helpers from '../helpers' 5 | import createErrorMessage from './createErrorMessage' 6 | 7 | const { 8 | isNotEmpty, 9 | isLengthGreaterThan, 10 | hasCapitalLetter, 11 | getValue, 12 | } = helpers 13 | 14 | const Form = ({ revalidation: { form, onChange, errors = {}, onSubmit }, onSubmit: submitCb }) => 15 | ( 16 |
17 |
18 | 19 | onChange('name', getValue(e))} 23 | /> 24 |
{ createErrorMessage(errors.name) }
25 |
26 |
27 | 28 | onChange(['settings', 'password'], getValue(e))} 32 | /> 33 |
{ createErrorMessage(errors.settings.password) }
34 |
35 |
36 | 37 | onChange(['settings', 'repeatPassword'], getValue(e))} 41 | /> 42 |
{ createErrorMessage(errors.settings.repeatPassword) }
43 |
44 |
45 | 46 | onChange(['levelOne', 'levelTwo', 'random'], getValue(e))} 50 | /> 51 |
{ createErrorMessage(errors.levelOne.levelTwo.random) }
52 |
53 | 54 |
55 | ) 56 | 57 | const NestedForm = revalidation(Form) 58 | 59 | // validation function 60 | 61 | const isEqual = compareKey => (a, all) => a === all[compareKey] 62 | 63 | // Messages 64 | 65 | const minimumMsg = (field, len) => `Minimum ${field} length of ${len} is required.` 66 | const capitalLetterMag = field => `${field} should contain at least one uppercase letter.` 67 | const equalMsg = (field1, field2) => `${field2} should be equal with ${field1}` 68 | 69 | 70 | const passwordValidationRule = [ 71 | [isLengthGreaterThan(5), minimumMsg('Password', 6)], 72 | [hasCapitalLetter, capitalLetterMag('Password')], 73 | ] 74 | 75 | const repeatPasswordValidationRule = [ 76 | [isLengthGreaterThan(5), minimumMsg('RepeatedPassword', 6)], 77 | [hasCapitalLetter, capitalLetterMag('RepeatedPassword')], 78 | [isEqual('password'), equalMsg('Password', 'RepeatPassword')], 79 | ] 80 | 81 | const nestedValidationRules = { 82 | name: [ 83 | [isNotEmpty, 'Name should not be empty.'], 84 | ], 85 | settings: { 86 | password: passwordValidationRule, 87 | repeatPassword: repeatPasswordValidationRule, 88 | }, 89 | levelOne: { 90 | levelTwo: { 91 | random: [ 92 | [isLengthGreaterThan(7), 'Minimum Random length of 8 is required.'], 93 | [hasCapitalLetter, 'Random should contain at least one uppercase letter.'], 94 | ], 95 | }, 96 | }, 97 | } 98 | 99 | export default class AdvancedDeepNestedData extends React.Component { 100 | 101 | constructor(props) { 102 | super(props) 103 | this.state = { 104 | model: { 105 | name: '', 106 | settings: { 107 | password: '', 108 | repeatPassword: '', 109 | }, 110 | levelOne: { 111 | levelTwo: { 112 | random: '', 113 | } 114 | }, 115 | someOtherValue: 'Not Used', 116 | someOtherNestedStructure: { 117 | otherNested: 'Not Used Either' 118 | } 119 | } 120 | } 121 | } 122 | 123 | render() { 124 | return ( 125 | 133 | ) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /example/Components/AdvancedInstantValidation.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import Revalidation from '../../src/' 4 | import Form from './Form' 5 | import validationRules from '../validationRules' 6 | 7 | export default class AdvancedInstantValidation extends React.Component { 8 | 9 | constructor(props) { 10 | super(props) 11 | this.state = {form: { name: '', password: '', repeatPassword: '', random: '' }} 12 | } 13 | 14 | render() { 15 | return ( 16 | 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/Components/AdvancedNoInstantValidation.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import Revalidation from '../../src/' 4 | import Form from './Form' 5 | import validationRules from '../validationRules' 6 | 7 | export default class AdvancedNoInstantValidation extends React.Component { 8 | 9 | constructor(props) { 10 | super(props) 11 | this.state = {form: { name: '', password: '', repeatPassword: '', random: '' }} 12 | } 13 | 14 | render() { 15 | return ( 16 | 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/Components/AsyncForm.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react' 3 | import Revalidation from '../../src/' 4 | 5 | const get = username => new Promise(res => { 6 | const existingNames = ['foo', 'bar', 'baz', 'foobarbaz'] 7 | setTimeout(() => 8 | res({ 9 | data: { 10 | exists: existingNames.indexOf(username.toLowerCase().trim()) !== -1, 11 | }, 12 | }), 13 | 1000) 14 | }) 15 | 16 | const SubmitForm = ({ 17 | revalidation: { form, onChange, updateState, valid, asyncErrors, errors, onSubmit, debounce }, 18 | onSubmit: submitCb, 19 | pendingNameCheck, 20 | usernameExists, 21 | }) => { 22 | return ( 23 | { e.preventDefault(); onSubmit(({valid, form}) => valid ? submitCb(form) : null ) }}> 24 |
25 | 26 | 31 | {errors.name && errors.name.map((errMsg, index) => (
32 | {errMsg}
))} 33 | {asyncErrors.name &&
{asyncErrors.name[0]}
} 34 |
35 |
36 | To see the async validation fail type: "foobarbaz" 37 |
38 |
39 |

valid? {valid.toString()}

40 |

loading? {pendingNameCheck.toString()}

41 |

valid? {valid.toString()}

42 |

errors? {JSON.stringify(errors, null ,4)}

43 |

asyncErrors? {JSON.stringify(asyncErrors, null ,4)}

44 |
45 | 46 | 47 | 48 | 49 | ) 50 | } 51 | 52 | const EnhancedSubmitForm = Revalidation(SubmitForm) 53 | 54 | class SubmitPage extends React.Component { 55 | constructor(props) { 56 | super(props) 57 | this.state = {form: {name: ''}, pendingNameCheck: false, pendingSubmit: false, asyncErrors:{}} 58 | } 59 | 60 | onSubmit = (data) => { 61 | this.setState(state => ({pendingSubmit: true})) 62 | // something went wrong... 63 | setTimeout(() => { 64 | this.setState(state => ({ pendingSubmit: false })) 65 | this.props.onSubmit(data) 66 | }, 1000) 67 | } 68 | 69 | isUnusedUserName = (username, {errors}) => { 70 | if (errors.name.length > 0) return 71 | this.setState(state => ({pendingNameCheck: true})) 72 | get(username) 73 | .then(({ data }) => data).then(data => { 74 | const asyncErrors = data.exists 75 | ? { name: [`Username ${name} is not available`] } 76 | : { name: [] } 77 | this.setState({ asyncErrors, pendingNameCheck: false }) 78 | }) 79 | } 80 | 81 | render() { 82 | const validationRules = { 83 | name: [ 84 | [x => x && x.length >= 6, 'Minimum length of 6'], 85 | ], 86 | } 87 | 88 | return ( 89 | 100 | ) 101 | } 102 | } 103 | 104 | export default SubmitPage 105 | -------------------------------------------------------------------------------- /example/Components/Basic.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import Revalidation from '../../src/' 4 | import SimpleForm from './SimpleForm' 5 | import { basicValidationRules } from '../validationRules' 6 | 7 | export default class Basic extends React.Component { 8 | 9 | constructor(props) { 10 | super(props) 11 | this.state = {form: {name: '', random: ''}} 12 | } 13 | 14 | render() { 15 | return ( 16 |
17 | submitted} 23 | /> 24 |
25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/Components/BasicWithIsValid.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import Revalidation from '../../src/' 4 | import SimpleForm from './SimpleForm' 5 | import { basicValidationRules } from '../validationRules' 6 | 7 | export default class BasicWithIsValid extends React.Component { 8 | 9 | constructor(props) { 10 | super(props) 11 | this.state = {form: {name: '', random: ''}} 12 | } 13 | 14 | render() { 15 | return ( 16 |
17 | 25 |
26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/Components/BasicWithoutSubmitButton.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import Revalidation from '../../src/' 4 | import Form from './FormWithoutSubmitButton' 5 | import { basicValidationRules } from '../validationRules' 6 | 7 | export default class BasicWithoutSubmitButton extends React.Component { 8 | 9 | constructor(props) { 10 | super(props) 11 | this.state = {form: {name: '', random: ''}} 12 | } 13 | 14 | render() { 15 | return ( 16 |
17 |
24 |
25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/Components/Form.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react' 3 | import { compose } from 'ramda' 4 | 5 | import revalidation from '../../src/' 6 | import helpers from '../helpers' 7 | import createErrorMessage from './createErrorMessage' 8 | 9 | const { getValue } = helpers 10 | 11 | const Form = ({ revalidation: { form, onChange, errors = {}, onSubmit }, onSubmit: submitCb }) => 12 | ( 13 |
14 |
15 | 16 | 21 |
{ createErrorMessage(errors.name) }
22 |
23 |
24 | 25 | 30 |
{ createErrorMessage(errors.password) }
31 |
32 |
33 | 34 | 39 |
{ createErrorMessage(errors.repeatPassword) }
40 |
41 |
42 | 43 | 48 |
{ createErrorMessage(errors.random) }
49 |
50 | 51 |
52 | ) 53 | 54 | export default revalidation(Form) 55 | -------------------------------------------------------------------------------- /example/Components/FormWithoutSubmitButton.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react' 3 | import { compose } from 'ramda' 4 | import revalidation, { isValid } from '../../src/' 5 | 6 | import helpers from '../helpers' 7 | import createErrorMessage from './createErrorMessage' 8 | 9 | const { getValue } = helpers 10 | 11 | const Form = ({ 12 | revalidation: { 13 | form, 14 | onChange, 15 | validate, 16 | valid, 17 | run, 18 | errors = {}, 19 | onSubmit, 20 | submitted, 21 | updateValue, 22 | validateValue, 23 | updateValueAndValidate, 24 | }, 25 | onSubmit: submitCb 26 | }) => 27 | ( 28 |
29 |
30 | 31 | onChange('name', getValue(e), null, ({valid, form}) => valid ? submitCb(form) : null )} 37 | /> 38 |
{ createErrorMessage(errors.name) }
39 |
40 |
41 | 42 | onChange('random', getValue(e), null, ({valid, form}) => valid ? submitCb(form) : null )} 49 | /> 50 |
{ createErrorMessage(errors.random) }
51 |
52 |
53 | ) 54 | 55 | export default revalidation(Form) 56 | -------------------------------------------------------------------------------- /example/Components/ServerSideErrors.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react' 3 | import { compose } from 'ramda' 4 | import revalidation, { isValid } from '../../src/' 5 | 6 | import helpers from '../helpers' 7 | import createErrorMessage from './createErrorMessage' 8 | import { basicValidationRules } from '../validationRules' 9 | 10 | const { getValue } = helpers 11 | 12 | const Form = ({ 13 | revalidation: { 14 | asyncErrors, 15 | form, 16 | onChange, 17 | validate, 18 | valid, 19 | run, 20 | errors = {}, 21 | onSubmit, 22 | submitted, 23 | updateValue, 24 | validateValue, 25 | updateValueAndValidate, 26 | }, 27 | onSubmit: submitCb, 28 | pendingSubmit, 29 | }) => 30 | ( 31 |
32 |
33 | 34 | 41 |
{ createErrorMessage(errors.name) }
42 |
{ createErrorMessage(asyncErrors.name) }
43 |
44 |
45 | 46 | 54 |
{ createErrorMessage(errors.random) }
55 |
56 | 59 |
60 |
 61 |       submitted? {JSON.stringify(submitted, null, 4)}
62 | valid? {JSON.stringify(valid, null, 4)}
63 | pending? {JSON.stringify(pendingSubmit, null, 4)} 64 |
65 |
66 | ) 67 | 68 | const EnhancedForm = revalidation(Form) 69 | 70 | class ServerSideErrors extends React.Component { 71 | constructor(props) { 72 | super(props) 73 | this.state = { form: { name: '', random: '' }, pendingSubmit: false, asyncErrors:{} } 74 | } 75 | 76 | onSubmit = (formValues) => { 77 | const message = `Just updated: ${JSON.stringify(formValues, null, 4)}` 78 | // something went wrong... 79 | this.setState(state => ({ pendingSubmit: true })) 80 | setTimeout(() => { 81 | this.setState(state => 82 | ({ asyncErrors: { 83 | name: ['Something went wrong, username is not available'] 84 | }, 85 | pendingSubmit: false, 86 | message 87 | })) 88 | }, 1000) 89 | } 90 | 91 | render() { 92 | const { asyncErrors, form, message, pendingSubmit } = this.state 93 | return ( 94 |
95 |
96 |
{message}
97 |
98 | submitted} 106 | /> 107 |
108 | ) 109 | } 110 | } 111 | 112 | export default ServerSideErrors 113 | -------------------------------------------------------------------------------- /example/Components/SetAsyncErrorsExample.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react' 3 | import { compose } from 'ramda' 4 | import revalidation, { isValid } from '../../src/' 5 | 6 | import helpers from '../helpers' 7 | import createErrorMessage from './createErrorMessage' 8 | import { basicValidationRules } from '../validationRules' 9 | 10 | const { getValue } = helpers 11 | 12 | const Form = ({ 13 | revalidation: { 14 | asyncErrors, 15 | form, 16 | onChange, 17 | validate, 18 | valid, 19 | run, 20 | errors = {}, 21 | onSubmit, 22 | updateAsyncErrors, 23 | submitted, 24 | updateValue, 25 | validateValue, 26 | updateValueAndValidate, 27 | }, 28 | onSubmit: submitCb, 29 | pendingSubmit, 30 | }) => 31 | ( 32 |
33 |
34 | 35 | 42 |
{ createErrorMessage(errors.name) }
43 |
{ createErrorMessage(asyncErrors.name) }
44 |
45 |
46 | 47 | 55 |
{ createErrorMessage(errors.random) }
56 |
57 | 60 |
61 |
 62 |       submitted? {JSON.stringify(submitted, null, 4)}
63 | valid? {JSON.stringify(valid, null, 4)}
64 | pending? {JSON.stringify(pendingSubmit, null, 4)} 65 |
66 |
67 | ) 68 | 69 | const EnhancedForm = revalidation(Form) 70 | 71 | class SetAsyncErrorsExample extends React.Component { 72 | constructor(props) { 73 | super(props) 74 | this.state = { form: { name: '', random: '' }, pendingSubmit: false } 75 | } 76 | 77 | onSubmit = (formValues, updateAsyncErrors) => { 78 | const message = `Just updated: ${JSON.stringify(formValues, null, 4)}` 79 | // something went wrong... 80 | this.setState(state => ({ pendingSubmit: true })) 81 | setTimeout(() => { 82 | updateAsyncErrors({ name: ['Username is not available'] }) 83 | this.setState(state => ({ pendingSubmit: false, message })) 84 | }, 1000) 85 | } 86 | 87 | render() { 88 | const { form, message, pendingSubmit } = this.state 89 | return ( 90 |
91 |
92 |
{message}
93 |
94 | submitted} 101 | /> 102 |
103 | ) 104 | } 105 | } 106 | 107 | export default SetAsyncErrorsExample 108 | -------------------------------------------------------------------------------- /example/Components/SimpleForm.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react' 3 | import { compose } from 'ramda' 4 | import revalidation, { isValid } from '../../src/' 5 | 6 | import helpers from '../helpers' 7 | import createErrorMessage from './createErrorMessage' 8 | 9 | const { getValue } = helpers 10 | 11 | const Form = ({ 12 | revalidation: { 13 | form, 14 | onChange, 15 | validate, 16 | valid, 17 | run, 18 | errors = {}, 19 | onSubmit, 20 | submitted, 21 | updateValue, 22 | validateValue, 23 | updateValueAndValidate, 24 | }, 25 | onSubmit: submitCb, 26 | disableButtonOption = false 27 | }) => 28 | ( 29 |
30 |
31 | 32 | 39 |
{ createErrorMessage(errors.name) }
40 |
41 |
42 | 43 | 51 |
{ createErrorMessage(errors.random) }
52 |
53 | 60 |
61 | ) 62 | 63 | export default revalidation(Form) 64 | -------------------------------------------------------------------------------- /example/Components/createErrorMessage.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react' 3 | import { head } from 'ramda' 4 | import { isValid } from '../../src/' 5 | 6 | export default (errorMsgs) => isValid(errorMsgs) ? null :
{head(errorMsgs)}
7 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-nested-ternary, no-unused-vars, no-undef */ 2 | 3 | import React from 'react' 4 | import { render } from 'react-dom' 5 | 6 | import AdvancedInstantValidation from './Components/AdvancedInstantValidation' 7 | import AdvancedNoInstantValidation from './Components/AdvancedNoInstantValidation' 8 | import AdvancedDeepNestedData from './Components/AdvancedDeepNestedData' 9 | import Basic from './Components/Basic' 10 | import BasicWithIsValid from './Components/BasicWithIsValid' 11 | import AsyncForm from './Components/AsyncForm' 12 | import ServerSideErrors from './Components/ServerSideErrors' 13 | import SetAsyncErrorsExample from './Components/SetAsyncErrorsExample' 14 | import BasicWithoutSubmitButton from './Components/BasicWithoutSubmitButton' 15 | 16 | class Root extends React.Component { 17 | constructor(props) { 18 | super(props) 19 | this.state = { example: 1, message: '', formValues: { name: '', random: '' } } 20 | } 21 | 22 | onSubmit = (formValues) => { 23 | const message = `Just updated: ${JSON.stringify(formValues, null, 4)}` 24 | this.setState(() => ({ formValues, message })) 25 | } 26 | 27 | changeExample = (example) => { 28 | if (example !== this.state.example) { 29 | this.setState(() => ({example, message: '', formValues: {}})) 30 | } 31 | } 32 | 33 | updateProps = () => { 34 | this.setState(() => ({ 35 | formValues: { name: 'foobarBaz', random: 'n' }, 36 | })) 37 | } 38 | 39 | getForm = (example, formValues = {}) => { 40 | const initState = { name: '', password: '', repeatPassword: '', random: '' } 41 | const updatedValues = {...initState, ...formValues} 42 | 43 | switch (example) { 44 | 45 | case 1: return 46 | 47 | case 2: return 48 | 49 | case 3: return 50 | 51 | case 4: return 52 | 53 | case 5: return 54 | 55 | case 6: return 56 | 57 | case 7: return 58 | 59 | case 8: return 60 | 61 | case 9: return 62 | 63 | default: return 64 | } 65 | } 66 | 67 | render() { 68 | const { example, message, formValues } = this.state 69 | 70 | const examples = [ 71 | {id: 1, name: 'Basic'}, 72 | {id: 2, name: 'Basic (Disable Submit Button if Invalid)'}, 73 | {id: 3, name: 'Advanced (No dynamic prop updates validation)'}, 74 | {id: 4, name: 'Advanced (Dynamic prop updates validation)'}, 75 | {id: 5, name: 'Advanced (Deep Nested Input Data)'}, 76 | {id: 6, name: 'Async Example'}, 77 | {id: 7, name: 'Sever Side Errors'}, 78 | {id: 8, name: 'setAsyncErrors Example'}, 79 | {id: 9, name: 'Basic without Submit Button'}, 80 | ] 81 | 82 | const selectedForm = this.getForm(example, formValues) 83 | const getClassName = id => (example === id) ? 'selected' : '' 84 | const getConfig = id => (id === 1) 85 | ? { validateSingle: true, validateOnChange: true } 86 | : (id === 3) 87 | ? { validateSingle: false, validateOnChange: false } 88 | : { validateSingle: false, validateOnChange: true } 89 | 90 | return ( 91 |
92 |

Revalidation

93 |

High Order Validation React Component

94 |
95 |
96 | {examples.map(({id, name}) =>
this.changeExample(id)} 98 | className={getClassName(id)} 99 | key={id} 100 | > 101 | {name} 102 |
)} 103 |
104 |
105 |
configuration:
106 |               {JSON.stringify(getConfig(example), null, 4)}
107 |             
108 |
Passed in props:
109 |               {JSON.stringify(formValues, null, 4)}
110 |             
111 |
{message}
112 |
113 | {selectedForm} 114 | { ([3, 4].indexOf(example) !== -1) && } 115 |
116 |
117 | ) 118 | } 119 | } 120 | 121 | render( 122 |
123 | 124 |
, 125 | document.getElementById('app'), 126 | ) 127 | -------------------------------------------------------------------------------- /example/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Higher Order Validation React Component in React 7 | 96 | 97 | 98 | 99 | 100 |
101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /example/helpers.js: -------------------------------------------------------------------------------- 1 | import { 2 | compose, 3 | curry, 4 | path, 5 | prop, 6 | } from 'ramda' 7 | 8 | // helper 9 | const getValue = path(['target', 'value']) 10 | 11 | // validations 12 | const isNotEmpty = a => a.trim().length > 0 13 | const hasCapitalLetter = a => /[A-Z]/.test(a) 14 | const isGreaterThan = curry((len, a) => (a > len)) 15 | const isLengthGreaterThan = len => compose(isGreaterThan(len), prop('length')) 16 | 17 | export default { 18 | getValue, 19 | isNotEmpty, 20 | isLengthGreaterThan, 21 | hasCapitalLetter, 22 | } 23 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Higher Order Validation React Component in React 7 | 96 | 97 | 98 | 99 |
100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /example/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Higher Order Validation React Component in React 7 | 96 | 97 | 98 | 99 | 100 |
101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "revalidation-example", 3 | "description": "Higher Order Component for Form Validation in React example", 4 | "private": true, 5 | "scripts": { 6 | "start": "webpack-dev-server" 7 | }, 8 | "devDependencies": { 9 | "babel": "^6.23.0", 10 | "babel-cli": "^6.24.1", 11 | "babel-core": "^6.25.0", 12 | "babel-loader": "^7.0.0", 13 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 14 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 15 | "babel-plugin-transform-react-jsx": "^6.24.1", 16 | "babel-preset-es2015": "^6.24.1", 17 | "babel-preset-react": "^6.24.1", 18 | "babel-preset-stage-0": "^6.24.1", 19 | "html-webpack-plugin": "^3.2.0", 20 | "prop-types": "^15.5.10", 21 | "react": "^15.5.4", 22 | "react-dom": "^15.5.4", 23 | "webpack": "^4.30.0", 24 | "webpack-cli": "^3.3.2", 25 | "webpack-dev-server": "^3.3.1" 26 | }, 27 | "dependencies": { 28 | "ramda": "^0.24.1", 29 | "revalidation": "^0.12.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/validationRules.js: -------------------------------------------------------------------------------- 1 | import helpers from './helpers' 2 | 3 | const { 4 | isNotEmpty, 5 | isLengthGreaterThan, 6 | hasCapitalLetter, 7 | } = helpers 8 | 9 | // validation function 10 | 11 | const isEqual = compareKey => (a, all) => a === all[compareKey] 12 | 13 | // Messages 14 | 15 | const minimumMsg = (field, len) => `Minimum ${field} length of ${len} is required.` 16 | const capitalLetterMag = field => `${field} should contain at least one uppercase letter.` 17 | const equalMsg = (field1, field2) => `${field2} should be equal with ${field1}` 18 | 19 | 20 | const passwordValidationRule = [ 21 | [isLengthGreaterThan(5), minimumMsg('Password', 6)], 22 | [hasCapitalLetter, capitalLetterMag('Password')], 23 | ] 24 | 25 | const repeatPasswordValidationRule = [ 26 | [isLengthGreaterThan(5), minimumMsg('RepeatedPassword', 6)], 27 | [hasCapitalLetter, capitalLetterMag('RepeatedPassword')], 28 | [isEqual('password'), equalMsg('Password', 'RepeatPassword')], 29 | ] 30 | 31 | export const basicValidationRules = { 32 | name: [ 33 | [isNotEmpty, 'Name should not be empty.'], 34 | ], 35 | random: [ 36 | [isLengthGreaterThan(7), 'Minimum Random length of 8 is required.'], 37 | [hasCapitalLetter, 'Random should contain at least one uppercase letter.'], 38 | ], 39 | } 40 | 41 | const validationRules = { 42 | ...basicValidationRules, 43 | password: passwordValidationRule, 44 | repeatPassword: repeatPasswordValidationRule, 45 | } 46 | 47 | export default validationRules 48 | -------------------------------------------------------------------------------- /example/webpack.config.demo.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | 5 | module.exports = { 6 | entry: { 7 | bundle: './example/app', 8 | }, 9 | output: { 10 | path: `${path.join(__dirname)}/demo`, 11 | filename: '[name].js', 12 | }, 13 | plugins: [ 14 | new HtmlWebpackPlugin({ 15 | filename: 'index.html', 16 | inject: true, 17 | template: './example/index.template.html', 18 | }), 19 | ], 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.js$/, 24 | use: ['babel-loader'], 25 | exclude: /(node_modules|dist)/, 26 | }, 27 | ], 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | module.exports = { 5 | cache: true, 6 | devtool: 'source-map', 7 | entry: { 8 | app: `${path.join(__dirname)}/app.js`, 9 | }, 10 | output: { 11 | path: `${path.join(__dirname)}/public`, 12 | publicPath: '/', 13 | filename: 'bundle.js', 14 | chunkFilename: '[chunkhash].js', 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | use: ['babel-loader'], 21 | exclude: /(node_modules|dist)/, 22 | }, 23 | ], 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "revalidation", 3 | "version": "0.12.1", 4 | "description": "Higher Order Component for Form Validation in React", 5 | "main": "lib/index.js", 6 | "author": "25th-floor", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/25th-floor/revalidation.git" 11 | }, 12 | "scripts": { 13 | "clean": "rimraf lib dist", 14 | "build": "npm run build:lib && npm run build:dist", 15 | "build:lib": "babel src -d lib", 16 | "build:dist": "NODE_ENV=production rollup -c", 17 | "build:demo": "webpack --config example/webpack.config.demo.js --mode production", 18 | "test": "npm run lint && mocha --compilers js:babel-core/register --recursive --colors", 19 | "flow": "flow check", 20 | "lint": "eslint src", 21 | "prepublish": "npm run clean && npm run build" 22 | }, 23 | "keywords": [ 24 | "Validation", 25 | "React", 26 | "Forms" 27 | ], 28 | "devDependencies": { 29 | "babel": "^6.23.0", 30 | "babel-cli": "^6.24.1", 31 | "babel-core": "^6.25.0", 32 | "babel-eslint": "^10.0.1", 33 | "babel-loader": "^7.0.0", 34 | "babel-plugin-add-module-exports": "^0.2.1", 35 | "babel-plugin-external-helpers": "^6.22.0", 36 | "babel-plugin-ramda": "^1.2.0", 37 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 38 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 39 | "babel-plugin-transform-react-jsx": "^6.24.1", 40 | "babel-preset-es2015": "^6.24.1", 41 | "babel-preset-react": "^6.24.1", 42 | "babel-preset-stage-0": "^6.24.1", 43 | "eslint": "^5.16.0", 44 | "eslint-config-airbnb-base": "^11.2.0", 45 | "eslint-plugin-import": "^2.17.2", 46 | "flow-bin": "^0.47.0", 47 | "mocha": "^3.4.2", 48 | "prop-types": "^15.5.10", 49 | "react": "^15.5.4", 50 | "react-dom": "^15.5.4", 51 | "react-test-renderer": "^15.6.1", 52 | "rimraf": "^2.6.1", 53 | "rollup": "^0.42.0", 54 | "rollup-plugin-babel": "^2.7.1", 55 | "rollup-plugin-commonjs": "^8.0.2", 56 | "rollup-plugin-flow": "^1.1.1", 57 | "rollup-plugin-node-resolve": "^3.0.0", 58 | "rollup-plugin-replace": "^1.1.1", 59 | "rollup-plugin-uglify": "^2.0.1", 60 | "webpack": "^4.30.0", 61 | "webpack-cli": "^3.3.2" 62 | }, 63 | "dependencies": { 64 | "data.task": "^3.1.1", 65 | "hoist-non-react-statics": "^1.2.0", 66 | "ramda": "^0.24.1", 67 | "spected": "^0.7.1" 68 | }, 69 | "peerDependencies": { 70 | "react": "*", 71 | "react-dom": "*" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve' 2 | import babel from 'rollup-plugin-babel' 3 | import replace from 'rollup-plugin-replace' 4 | import uglify from 'rollup-plugin-uglify' 5 | import commonjs from 'rollup-plugin-commonjs' 6 | import flow from 'rollup-plugin-flow' 7 | 8 | var env = process.env.NODE_ENV 9 | 10 | var config = { 11 | entry: 'src/index.js', 12 | moduleName: 'Revalidation', 13 | exports: 'named', 14 | format: 'umd', 15 | sourceMap: env !== 'production', 16 | targets: (env == 'production') ? 17 | [ 18 | { dest: 'dist/revalidation.min.js', format: 'umd' }, 19 | ] : 20 | [ 21 | { dest: 'dist/revalidation.js', format: 'umd' }, 22 | { dest: 'dist/revalidation.es.js', format: 'es' }, 23 | ], 24 | globals: { 25 | 'react': 'React', 26 | 'react-dom': 'ReactDOM', 27 | }, 28 | external: ['react', 'react-dom'], 29 | plugins: [ 30 | babel({ 31 | babelrc: false, 32 | presets: [["es2015", { "modules": false }], "stage-0"], 33 | plugins: [ 34 | "external-helpers", 35 | 'transform-object-rest-spread', 36 | 'transform-flow-strip-types', 37 | 'ramda', 38 | 'transform-react-jsx', 39 | ], 40 | exclude: 'node_modules/**', 41 | }), 42 | flow(), 43 | commonjs(), 44 | nodeResolve({ 45 | jsnext: true, 46 | }), 47 | replace({ 48 | 'process.env.NODE_ENV': JSON.stringify(env) 49 | }) 50 | ] 51 | } 52 | 53 | if (env === 'production') { 54 | config.plugins.push( 55 | uglify({ 56 | compress: { 57 | pure_getters: true, 58 | unsafe: true, 59 | unsafe_comps: true, 60 | }, 61 | warnings: false, 62 | }) 63 | ) 64 | } 65 | 66 | export default config 67 | -------------------------------------------------------------------------------- /src/Revalidation.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint-disable no-nested-ternary, no-unneeded-ternary */ 3 | import React, { createElement } from 'react' 4 | import { 5 | always, 6 | assoc, 7 | assocPath, 8 | curry, 9 | ifElse, 10 | is, 11 | keys, 12 | map, 13 | merge, 14 | mergeDeepRight, 15 | partial, 16 | prop, 17 | propOr, 18 | reduce, 19 | zipObj, 20 | } from 'ramda' 21 | import hoistNonReactStatics from 'hoist-non-react-statics' 22 | import validate from './validate' 23 | import isValid from './utils/isValid' 24 | import debounce from './helpers/debounce' 25 | import updateFormValues from './updaters/updateFormValues' 26 | import updateSyncErrors from './updaters/updateSyncErrors' 27 | 28 | import { UPDATE_FIELD, UPDATE_ALL, VALIDATE_FIELD, VALIDATE_ALL } from './constants' 29 | 30 | /** 31 | * Calculates the new state to be set depending on the actions triggered via the form. 32 | * 33 | * Can be an entry point for externally changing the state in future versions, by defining own update functions that 34 | * change the next state depending on certain user actions. For now this is only an internal implementation. 35 | * 36 | * @param {Array} updateFns all update functions that need to change to the current state 37 | * @param {Object} state the actual state 38 | * @param {String} type defines which action should be handled via the update functions 39 | * @param {Object} enhancedProps containing the props as well as additional information like field name or value if available 40 | * @returns {Object} return the new state that needs to be set. 41 | */ 42 | const runUpdates = (updateFns, state, type, enhancedProps) => reduce((updatedState, updateFn) => 43 | updateFn(updatedState, type, enhancedProps), state, updateFns) 44 | 45 | /** 46 | * Maps an empty array to every item of the list and returns a map representing the items as keys 47 | * Also works with deep nested data. 48 | * 49 | * @param {Array} obj the to object to convert 50 | * @returns {Array} 51 | * @example 52 | * 53 | * initializeErrors({'a': null 'b': null}) //=> {a: [], b: []} 54 | * 55 | * initializeErrors: [{'a': null, b: {c: {d: null}}}] //=> {'a': [], b: {c: {d: []}}} 56 | * 57 | */ 58 | const initializeErrors = obj => map(ifElse(is(Object), partial(initializeErrors, []), always([])), obj) 59 | 60 | /** 61 | * revalidation expects a React Component and returns a React Component containing additional functions and props 62 | * for managing local component state as well validating that state, wrapping the originally provided Component. 63 | */ 64 | function revalidation( 65 | Component:any // eslint-disable-line comma-dangle 66 | ):any { 67 | class HigherOrderFormComponent extends React.Component { 68 | state:{ 69 | form: Object, 70 | errors: Object, 71 | asyncErrors: Object, 72 | } 73 | 74 | props:{ 75 | initialState: Object, 76 | rules: Object, 77 | asyncErrors?: Object, 78 | validateSingle?: boolean|Function, 79 | validateOnChange?: boolean|Function, 80 | updateForm?: Object 81 | } 82 | 83 | static defaultProps = { 84 | initialState: {}, 85 | validateSingle: false, 86 | validateOnChange: false, 87 | } 88 | 89 | updateFns: Array = [updateFormValues, updateSyncErrors] 90 | 91 | constructor(props) { 92 | super(props) 93 | 94 | const form = propOr([], 'initialState', props) 95 | const initErrors = initializeErrors(form) 96 | 97 | this.state = { 98 | form, 99 | errors: initErrors, 100 | asyncErrors: initErrors, 101 | debounceFns: this.createDebounceFunctions(prop('initialState', props)), 102 | submitted: false, 103 | } 104 | } 105 | 106 | componentWillReceiveProps(nextProps) { 107 | const { updateForm, asyncErrors = {} } = nextProps 108 | const type = this.getValidateOnChange(this.props.validateOnChange) 109 | ? [UPDATE_ALL, VALIDATE_ALL] 110 | : [UPDATE_ALL] 111 | 112 | this.setState((state, props) => { 113 | const nextState = updateForm 114 | ? runUpdates(this.updateFns, state, type, { ...props, value: updateForm }) 115 | : state 116 | 117 | const updatedState = assoc('asyncErrors', mergeDeepRight(prop('asyncErrors', state), asyncErrors), nextState) 118 | return updatedState 119 | }) 120 | } 121 | 122 | createDebounceFunctions(form: Array) { 123 | return zipObj(keys(form), 124 | map(name => debounce(name, this.updateField, this.runAsync), keys(form)) // eslint-disable-line comma-dangle 125 | ) 126 | } 127 | 128 | update = (type, data = {}, cb = () => {}) => { 129 | this.setState( 130 | (state, props) => 131 | runUpdates(this.updateFns, state, type, { ...props, ...data }), 132 | cb // eslint-disable-line comma-dangle 133 | ) 134 | } 135 | 136 | updateErrors = (errorsState) => { 137 | this.setState(state => { 138 | const nextErrorsState = merge(prop('errors', state), errorsState) 139 | return assoc('errors', nextErrorsState, state) 140 | }) 141 | } 142 | 143 | updateAsyncErrors = (asyncErrorsState) => { 144 | this.setState(state => { 145 | const nextAsyncErrorsState = merge(prop('asyncErrors', state), asyncErrorsState) 146 | return assoc('asyncErrors', nextAsyncErrorsState, state) 147 | }) 148 | } 149 | 150 | validateAll = (cb: Function):void => { 151 | this.update( 152 | [VALIDATE_ALL], 153 | {}, 154 | () => { 155 | if (!cb) return 156 | const valid = isValid(this.state.errors) && 157 | ( 158 | !this.getValidateOnChange(this.props.validateOnChange) || 159 | isValid(this.state.asyncErrors) 160 | ) 161 | cb({ ...this.state, valid }) 162 | } // eslint-disable-line comma-dangle 163 | ) 164 | } 165 | 166 | updateState = (nextState: Object) => { 167 | const type = this.getValidateOnChange(this.props.validateOnChange) 168 | ? [UPDATE_ALL, VALIDATE_ALL] 169 | : [UPDATE_ALL] 170 | 171 | this.update(type, { value: nextState }) 172 | } 173 | 174 | updateField = curry((name:string|Array, value:any, type: Array = null, cb: Function):void => { 175 | const updateType = 176 | this.getValidateOnChange(this.props.validateOnChange) 177 | ? type 178 | ? type 179 | : this.props.validateSingle 180 | ? [UPDATE_FIELD, VALIDATE_FIELD] 181 | : [UPDATE_FIELD, VALIDATE_ALL] 182 | : [UPDATE_FIELD] 183 | 184 | const fieldName = typeof name === 'string' ? [name] : name 185 | this.update( 186 | updateType, 187 | { name: fieldName, value }, 188 | () => { 189 | if (!cb) return 190 | const valid = isValid(this.state.errors) && 191 | ( 192 | !this.getValidateOnChange(this.props.validateOnChange) || 193 | isValid(this.state.asyncErrors) 194 | ) 195 | cb({ ...this.state, valid }) 196 | } // eslint-disable-line comma-dangle 197 | ) 198 | }) 199 | 200 | onChange = curry((name:string|Array, value:any): void => { 201 | this.updateField(name, value, [UPDATE_FIELD]) 202 | }) 203 | 204 | runAsync = (asyncFn: Function, name: Array|string, value: any): void => { 205 | // clear the current async errors for the field 206 | const fieldName = typeof name === 'string' ? [name] : name 207 | this.setState(state => assocPath(['asyncErrors', ...fieldName], [], state)) 208 | asyncFn(value, this.state) 209 | } 210 | 211 | updateValue = event => { 212 | const { name, value } = this.extractNameAndValue(event) 213 | this.updateField(name, value, [UPDATE_FIELD]) 214 | } 215 | 216 | validateValue = event => { 217 | const { name, value } = this.extractNameAndValue(event) 218 | this.updateField(name, value, [VALIDATE_FIELD]) 219 | } 220 | 221 | updateValueAndValidate = event => { 222 | const { name, value } = this.extractNameAndValue(event) 223 | this.updateField(name, value, [UPDATE_FIELD, VALIDATE_FIELD]) 224 | } 225 | 226 | extractNameAndValue = event => { 227 | const target = event.target 228 | const value = target.type === 'checkbox' ? target.checked : target.value 229 | const name = target.name 230 | return { name, value } 231 | } 232 | 233 | getValidateOnChange = (validateOnChange) => is(Function, validateOnChange) 234 | ? validateOnChange({ submitted: this.state.submitted }) 235 | : validateOnChange 236 | 237 | render() { 238 | const { form, errors, asyncErrors, debounceFns, submitted } = this.state 239 | /* eslint-disable no-unused-vars */ 240 | const { rules, asyncRules, initialState, updateForm, validateSingle, validateOnChange, ...rest } = this.props 241 | const validateOnChangeResult = this.getValidateOnChange(validateOnChange) 242 | const valid = isValid(validate(rules, form)) && 243 | isValid(errors) && 244 | (validateOnChangeResult && isValid(asyncErrors)) 245 | 246 | const revalidationProp = { 247 | form, 248 | errors, 249 | asyncErrors, 250 | valid, 251 | submitted, 252 | debounce: debounceFns, 253 | updateState: this.updateState, 254 | onChange: this.updateField, 255 | onSubmit: this.validateAll, 256 | settings: { validateOnChange: validateOnChangeResult, validateSingle }, 257 | updateErrors: this.updateErrors, 258 | updateAsyncErrors: this.updateAsyncErrors, 259 | UPDATE_FIELD, 260 | VALIDATE: validateSingle ? VALIDATE_FIELD : VALIDATE_ALL, 261 | // short cut functions 262 | updateValue: this.updateValue, 263 | validateValue: this.validateValue, 264 | updateValueAndValidate: this.updateValueAndValidate, 265 | } 266 | 267 | return createElement(Component, { 268 | ...rest, 269 | revalidation: revalidationProp, 270 | }) 271 | } 272 | } 273 | 274 | HigherOrderFormComponent.displayName = `Revalidation_(${Component.displayName || Component.name || 'Component'})` 275 | return hoistNonReactStatics(HigherOrderFormComponent, Component) 276 | } 277 | 278 | export default revalidation 279 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | export const UPDATE_FIELD = 'UPDATE_FIELD' 3 | export const UPDATE_ALL = 'UPDATE_ALL' 4 | export const VALIDATE_FIELD = 'VALIDATE_FIELD' 5 | export const VALIDATE_ALL = 'VALIDATE_ALL' 6 | export const VALIDATE_FIELD_SYNC = 'VALIDATE_FIELD_SYNC' 7 | export const VALIDATE_FIELD_ASYNC = 'VALIDATE_FIELD_ASYNC' 8 | export const VALIDATE_ALL_SYNC = 'VALIDATE_ALL_SYNC' 9 | export const VALIDATE_ALL_ASYNC = 'VALIDATE_ALL_ASYNC' 10 | -------------------------------------------------------------------------------- /src/helpers/debounce.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | /** 4 | * Delay the execution of any asynchronous validations 5 | * First update the state 6 | * then delay the validation according to the given delay value. 7 | * 8 | * @param {String} name field name 9 | * @param {Function} onChange the internal update function 10 | * @param {Function} runAsync an internal function that take async functions and calls them with the value and state. 11 | * @returns {Function} 12 | */ 13 | export default function debounce(name: string, onChange: Function, runAsync: Function) { 14 | let timeout 15 | /** 16 | * @param {Function} fn the asynchronous function to be called 17 | * @param {Number} delay delaying running the asynchronous function in ms. 18 | * @param {Array} types possible types to override the onChnage defaults [UPDATE_FIELD, VALIDATE_FIELD] 19 | * @returns {Function} 20 | */ 21 | return function f1(fn: Function, delay: number, types: Array = null): Function { 22 | return function f2(e) { 23 | e.preventDefault() 24 | const value = e.target.value 25 | onChange(name, value, types) 26 | clearTimeout(timeout) 27 | timeout = setTimeout(() => runAsync(fn, name, value), delay) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/helpers/getValue.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { pathOr } from 'ramda' 4 | 5 | /** 6 | * Calls preventDefault on an event and returns the value. 7 | * 8 | * @param e 9 | * @returns {*} 10 | */ 11 | export default function getValue(e) { 12 | e.preventDefault() 13 | return pathOr(null, ['target', 'value'], e) 14 | } 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import revalidation from './Revalidation' 3 | import debounce from './helpers/debounce' 4 | import getValue from './helpers/getValue' 5 | import isValid from './utils/isValid' 6 | 7 | export default revalidation 8 | 9 | export { 10 | debounce, 11 | getValue, 12 | isValid, 13 | } 14 | -------------------------------------------------------------------------------- /src/updaters/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import updateFormValues from './updateFormValues' 3 | import updateSyncErrors from './updateSyncErrors' 4 | 5 | export default { 6 | updateFormValues, 7 | updateSyncErrors, 8 | } 9 | -------------------------------------------------------------------------------- /src/updaters/types.js: -------------------------------------------------------------------------------- 1 | export type Rule = Array 2 | export type EnhancedProps = { 3 | name?: string|Array, 4 | value?: any, 5 | validateSingle?: boolean, 6 | validateOnChange?: boolean, 7 | rules?: Array, 8 | } 9 | -------------------------------------------------------------------------------- /src/updaters/updateFormValues.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint-disable no-nested-ternary */ 3 | import { 4 | always, 5 | assoc, 6 | assocPath, 7 | cond, 8 | contains, 9 | T, 10 | } from 'ramda' 11 | 12 | import type { EnhancedProps } from './types' 13 | import { UPDATE_FIELD, UPDATE_ALL } from '../constants' 14 | 15 | /** 16 | * @param {Object} state 17 | * @param {Array} type 18 | * @param {Object} enhancedProps 19 | * @returns {[Object, Array]} 20 | */ 21 | export default function updateFormValues(state: Object, type: Array, enhancedProps: EnhancedProps) { 22 | const { name = [], value } = enhancedProps 23 | 24 | const updateState = cond([ 25 | [contains(UPDATE_FIELD), always(assocPath(['form', ...name], value, state))], 26 | [contains(UPDATE_ALL), always(assoc('form', value, state))], 27 | [T, always(state)], 28 | ]) 29 | 30 | return updateState(type) 31 | } 32 | -------------------------------------------------------------------------------- /src/updaters/updateSyncErrors.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint-disable no-nested-ternary */ 3 | import { 4 | always, 5 | assocPath, 6 | cond, 7 | contains, 8 | merge, 9 | path, 10 | prop, 11 | T, 12 | } from 'ramda' 13 | 14 | import validate from '../validate' 15 | import type { EnhancedProps } from './types' 16 | import { VALIDATE_FIELD, VALIDATE_ALL } from '../constants' 17 | 18 | /** 19 | * 20 | * @param {[Object, Array]} state a tuple containing 21 | * @param {Array} effects 22 | * @param {Array} type 23 | * @param {Object} enhancedProps 24 | * @returns {[Object, Array]} 25 | */ 26 | export default function updateSyncErrors (state: Object, type: Array, enhancedProps: EnhancedProps) { 27 | const { name = [], rules } = enhancedProps 28 | const errors = validate(rules, prop('form', state)) 29 | 30 | /* eslint-disable no-shadow */ 31 | const updateState = cond([ 32 | [ 33 | type => contains(VALIDATE_FIELD, type) && name, 34 | () => { 35 | // reset any asyncErrors as the field value has been updated 36 | const cleanedState = path(['asyncErrors', name], state) ? assocPath(['asyncErrors', name], [], state) : state 37 | return assocPath(['errors', ...name], path([...name], errors), cleanedState) 38 | }, 39 | ], 40 | [ 41 | contains(VALIDATE_ALL), 42 | always(merge(state, { errors, submitted: true })), 43 | ], 44 | [T, always(state)], 45 | ]) 46 | 47 | return updateState(type) 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import isValid from './isValid' 4 | 5 | export default { 6 | isValid, 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/isValid.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { 3 | filter, 4 | is, 5 | isEmpty, 6 | } from 'ramda' 7 | 8 | /* eslint-disable no-mixed-operators, indent */ 9 | 10 | const isObject = a => (is(Object, a) && !is(Function, a)) 11 | 12 | /** 13 | * check if object keys contain any string, function, non empty array or object values. 14 | * 15 | * @param obj the object to validate 16 | * @returns {boolean} 17 | */ 18 | export default function isValid(obj:Object|Array = {}):boolean { 19 | const a = filter(i => i && 20 | isObject(i) 21 | ? !isValid(i) 22 | : typeof i === 'string' || typeof i === 'function' 23 | , obj // eslint-disable-line comma-dangle 24 | ) 25 | 26 | return isEmpty(a) // eslint-disable-line comma-dangle 27 | } 28 | -------------------------------------------------------------------------------- /src/validate.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { validate } from 'spected' 3 | import { identity } from 'ramda' 4 | 5 | /** 6 | * Defines the shape of the validation results. Always return an empty [] when value is valid. 7 | * In case of errors simply return the array containing the errors. 8 | * 9 | * @example 10 | * 11 | * validate({id: [[id => id > 0, 'Please provide a valid id']], {id: 1}}) 12 | * //=> {id: []} 13 | * 14 | * validate({id: [[id => id > 0, 'Please provide a valid id']], {id: 0}}) 15 | * //=> {id: ['Please provide a valid id']} 16 | * 17 | */ 18 | export default validate(() => [], identity) 19 | -------------------------------------------------------------------------------- /test/revalidation.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import { equal } from 'assert' 4 | import React from 'react' // eslint-disable-line no-unused-vars 5 | import ShallowRenderer from 'react-test-renderer/shallow' 6 | import Revalidation, { isValid } from '../src/' 7 | 8 | const isNotEmpty = a => a.trim().length > 0 9 | 10 | const validationRules = { 11 | name: [[isNotEmpty, 'Name should not be empty']], 12 | } 13 | 14 | const displayErrors = (errorMsgs) => 15 | isValid(errorMsgs) ? null :
{errorMsgs[0]}
16 | 17 | const Form = ({ 18 | revalidation: { form, updateValueAndValidate, errors = {}, onSubmit }, 19 | onSubmit: submitCb, 20 | }) => 21 |
22 |
23 | 24 | 30 | {displayErrors(errors.name)} 31 |
32 | 33 |
34 | 35 | const initialState = { name: '' } 36 | 37 | const EnhancedForm = Revalidation(Form) // eslint-disable-line no-unused-vars 38 | 39 | describe('revalidation', () => { 40 | it('callback passed to `onSubmit` is not called when the form has errors', () => { 41 | const renderer = new ShallowRenderer() 42 | let wasCallbackCalled = false 43 | renderer.render( 44 | { 49 | wasCallbackCalled = true 50 | }} 51 | />, 52 | ) 53 | const output = renderer.getRenderOutput() 54 | output.props.revalidation.onSubmit(({valid, form}) => valid ? output.props.onSubmit(form) : null) 55 | equal(wasCallbackCalled, false) 56 | }) 57 | 58 | it('callback passed to `onSubmit` is called when the form is valid', () => { 59 | const renderer = new ShallowRenderer() 60 | const initialState = { name: 'foo' } 61 | let wasCallbackCalled = false 62 | renderer.render( 63 | { 69 | wasCallbackCalled = true 70 | }} 71 | />, 72 | ) 73 | const output = renderer.getRenderOutput() 74 | output.props.revalidation.onSubmit(({valid, form, errors}) => console.log('valid: ', valid, form, errors) || valid ? output.props.onSubmit(form) : null) 75 | equal(wasCallbackCalled, true) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /test/updaters/updateFormValues.test.js: -------------------------------------------------------------------------------- 1 | import { deepEqual } from 'assert' 2 | 3 | import updateFormValues from '../../src/updaters/updateFormValues' 4 | import { UPDATE_FIELD, UPDATE_ALL, VALIDATE_FIELD, VALIDATE_ALL } from '../../src/constants' 5 | 6 | describe('updaters/updateFormValues', () => { 7 | 8 | it('should change the field value according to the provided value when an input has been updated', () => { 9 | const expected = {form: {name: 'bar'}} 10 | const result = updateFormValues({form: {name: 'foo'}}, UPDATE_FIELD, {name: ['name'], value: 'bar'}) 11 | deepEqual(expected, result) 12 | }) 13 | 14 | it('should change the field value according to the provided value when the complete form state has been updated', () => { 15 | const expected = {form: {name: '', random: ''}} 16 | const result = updateFormValues({form: {name: 'foo', random: 'bar'}}, UPDATE_ALL, { 17 | value: { 18 | name: '', 19 | random: '' 20 | } 21 | }) 22 | deepEqual(expected, result) 23 | }) 24 | 25 | it('should not change the form state when action=VALIDATE_FIELD', () => { 26 | const expected = {form: {name: 'foo', random: 'bar'}} 27 | const result = updateFormValues({form: {name: 'foo', random: 'bar'}}, VALIDATE_FIELD, { 28 | value: { 29 | name: '', 30 | random: '' 31 | } 32 | }) 33 | deepEqual(expected, result) 34 | }) 35 | 36 | it('should not change the form state when action=VALIDATE_ALL', () => { 37 | const expected = {form: {name: 'foo', random: 'bar'}} 38 | const result = updateFormValues({form: {name: 'foo', random: 'bar'}}, VALIDATE_ALL, { 39 | value: { 40 | name: '', 41 | random: '' 42 | } 43 | }) 44 | deepEqual(expected, result) 45 | }) 46 | 47 | it('should should update a deeply nested form field when action=UPDATE_FIELD', () => { 48 | const expected = {form: {name: 'foo', levelOne: {levelTwo: {random: 'foobar'}}}} 49 | const result = updateFormValues({ 50 | form: { 51 | name: 'foo', 52 | levelOne: { 53 | levelTwo: { 54 | random: 'bar' 55 | } 56 | } 57 | } 58 | }, UPDATE_FIELD, {name: ['levelOne', 'levelTwo', 'random'], value: 'foobar'}) 59 | deepEqual(expected, result) 60 | }) 61 | 62 | it('should should update a deeply nested form when action=UPDATE_ALL', () => { 63 | const expected = {form: {name: 'foo', levelOne: {levelTwo: {random: 'bar'}}}} 64 | const result = updateFormValues({ 65 | form: { 66 | name: 'oldName', 67 | levelOne: {levelTwo: {random: 'oldRandom'}} 68 | } 69 | }, UPDATE_ALL, { 70 | value: { 71 | name: 'foo', 72 | levelOne: { 73 | levelTwo: { 74 | random: 'bar' 75 | } 76 | } 77 | } 78 | }) 79 | deepEqual(expected, result) 80 | }) 81 | 82 | }) 83 | -------------------------------------------------------------------------------- /test/updaters/updateSyncErrors.test.js: -------------------------------------------------------------------------------- 1 | import { deepEqual } from 'assert' 2 | 3 | import updateSyncErrors from '../../src/updaters/updateSyncErrors' 4 | import { UPDATE_FIELD, UPDATE_ALL, VALIDATE_FIELD, VALIDATE_ALL } from '../../src/constants' 5 | 6 | const rules = { 7 | name: [[x => x.length > 3, 'Minimum length is four.']], 8 | random: [[x => x.length > 6, 'Minimum length is seven.']] 9 | } 10 | 11 | const nestedRules = { 12 | name: [[x => x.length > 3, 'Minimum length is four.']], 13 | levelOne: { 14 | levelTwo: { 15 | random: [[x => x.length > 6, 'Minimum length is seven.']] 16 | } 17 | } 18 | } 19 | 20 | describe('updaters/updateSyncErrors', () => { 21 | 22 | it('should return an empty array when field value is valid', () => { 23 | const expected = {form: {name: 'foobar'}, errors: {name: []}} 24 | const result = updateSyncErrors({form: {name: 'foobar'}, errors: {}}, [VALIDATE_FIELD], { 25 | rules, 26 | name: ['name'] 27 | }) 28 | deepEqual(expected, result) 29 | }) 30 | 31 | it('should return an array containing the error messages when field value is invalid', () => { 32 | const expected = {form: {name: 'bar'}, errors: {name: ['Minimum length is four.']}} 33 | const result = updateSyncErrors({form: {name: 'bar'}, errors: {}}, [VALIDATE_FIELD], { 34 | rules, 35 | name: ['name'] 36 | }) 37 | deepEqual(expected, result) 38 | }) 39 | 40 | it('should only validate the field that has been updated when action=VALIDATE_FIELD', () => { 41 | const expected = {form: {name: 'foo', random: '1234567'}, errors: {random: []}} 42 | const result = updateSyncErrors({form: {name: 'foo', random: '1234567'}, errors: {}}, [VALIDATE_FIELD], { 43 | rules, 44 | name: ['random'] 45 | }) 46 | deepEqual(expected, result) 47 | }) 48 | 49 | it('should return an empty array when field value is valid and action=VALIDATE_FIELD', () => { 50 | const expected = {form: {name: 'foobar'}, errors: {name: []}} 51 | const result = updateSyncErrors({form: {name: 'foobar'}, errors: {}}, [VALIDATE_FIELD], { 52 | rules, 53 | name: ['name'] 54 | }) 55 | deepEqual(expected, result) 56 | }) 57 | 58 | it('should validate all fields when the complete form state has been updated', () => { 59 | const expected = { 60 | form: {name: 'foo', random: 'random'}, 61 | errors: {name: ['Minimum length is four.'], random: ['Minimum length is seven.']}, 62 | submitted: true, 63 | } 64 | const result = updateSyncErrors({form: {name: 'foo', random: 'random'}, errors: {}}, [VALIDATE_ALL], { 65 | rules, 66 | }) 67 | deepEqual(expected, result) 68 | }) 69 | 70 | it('should validate all fields when action=VALIDATE_ALL', () => { 71 | const expected = { 72 | form: {name: 'foo', random: 'random'}, 73 | errors: {name: ['Minimum length is four.'], random: ['Minimum length is seven.']}, 74 | submitted: true, 75 | } 76 | const result = updateSyncErrors({form: {name: 'foo', random: 'random'}, errors: {}}, [VALIDATE_ALL], { 77 | rules, 78 | }) 79 | deepEqual(expected, result) 80 | }) 81 | 82 | it('should skip validation when action=UPDATE_ALL', () => { 83 | const expected = { 84 | form: {name: 'foo', random: 'random'}, 85 | errors: {} 86 | } 87 | const result = updateSyncErrors({form: {name: 'foo', random: 'random'}, errors: {}}, [UPDATE_ALL], { 88 | rules, 89 | }) 90 | deepEqual(expected, result) 91 | }) 92 | 93 | it('should should validate a deeply nested form field when action=VALIDATE_FIELD', () => { 94 | const expected = { 95 | form: {name: 'foo', levelOne: {levelTwo: {random: 'bar'}}}, 96 | errors: {name: [], levelOne: {levelTwo: {random: ['Minimum length is seven.']}}}, 97 | } 98 | const result = updateSyncErrors({ 99 | form: { 100 | name: 'foo', 101 | levelOne: { 102 | levelTwo: { 103 | random: 'bar' 104 | } 105 | } 106 | }, 107 | errors: {name: [], levelOne: {levelTwo: {random: []}}}, 108 | }, VALIDATE_FIELD, {name: ['levelOne', 'levelTwo', 'random'], rules: nestedRules}) 109 | deepEqual(expected, result) 110 | }) 111 | 112 | it('should should validate all deeply nested fields when action=VALIDATE_ALL', () => { 113 | const expected = { 114 | form: {name: 'foo', levelOne: {levelTwo: {random: 'bar'}}}, 115 | errors: {name: ['Minimum length is four.'], levelOne: {levelTwo: {random: ['Minimum length is seven.']}}}, 116 | submitted: true, 117 | } 118 | const result = updateSyncErrors({ 119 | form: { 120 | name: 'foo', 121 | levelOne: {levelTwo: {random: 'bar'}} 122 | }, 123 | errors: {}, 124 | }, VALIDATE_ALL, { 125 | rules: nestedRules, 126 | }) 127 | deepEqual(expected, result) 128 | }) 129 | 130 | }) 131 | -------------------------------------------------------------------------------- /test/utils/isValid.test.js: -------------------------------------------------------------------------------- 1 | import { equal, ok } from 'assert' 2 | 3 | import isValid from '../../src/utils/isValid' 4 | 5 | describe('utils/isValid', () => { 6 | 7 | it('should return true when validating []', () => { 8 | ok(isValid([])) 9 | }) 10 | 11 | it('should return true when validating {}', () => { 12 | ok(isValid({})) 13 | }) 14 | 15 | it('should return false when validating {id: "foo"}', () => { 16 | ok(!isValid({id: 'foo'})) 17 | }) 18 | 19 | it('should return false when validating {id: "foo", name: true, random: true}', () => { 20 | ok(!isValid({id: 'foo', name: true, random: true})) 21 | }) 22 | 23 | it('should return false when validating ["foo"]', () => { 24 | ok(!isValid(['foo'])) 25 | }) 26 | 27 | it('should return false when validating [true, true, "foo"]', () => { 28 | ok(!isValid([true, true, 'foo'])) 29 | }) 30 | 31 | it('should return false when validating {random: x => x}', () => { 32 | ok(!isValid({random: x => x})) 33 | }) 34 | 35 | it('should return false when validating [x => x]', () => { 36 | ok(!isValid([x => x])) 37 | }) 38 | 39 | it('should return true when validating {random:{}}', () => { 40 | ok(isValid({random: {}})) 41 | }) 42 | 43 | it('should return true when validating {random:[]}', () => { 44 | ok(isValid({random: []})) 45 | }) 46 | 47 | it('should return true when validating [{}]', () => { 48 | ok(isValid([{}])) 49 | }) 50 | 51 | it('should return true when passing in an undefined value', () => { 52 | ok(isValid(undefined)) 53 | }) 54 | 55 | it('should return true when validating deeply nested data: {name: [], levelOne: {random: [], nestedArray: [[], [], []]}}', () => { 56 | ok(isValid({name: [], levelOne: {random: [], nestedArray: [[], [], []]}})) 57 | }) 58 | 59 | it('should return true when validating deeply nested data: {name: [], levelOne: {levelTwo: {random: [], nestedArray: [[], [], []]}}}', () => { 60 | ok(isValid({name: [], levelOne: {levelTwo: {random: [], nestedArray: [[], [], []]}}})) 61 | }) 62 | 63 | it('should return false when validating deeply nested data: {name: [], levelOne: {random: ["foo"], nestedArray: [["bar"], [], ["baz"]]}}', () => { 64 | ok(!isValid({name: [], levelOne: {random: ['foo'], nestedArray: [['bar'], [], ['baz']]}})) 65 | }) 66 | 67 | it('should return false when validating deeply nested data: {name: [], levelOne: {levelTwo: {random: ["foo"], nestedArray: [["bar"], [], ["baz"]]}}}', () => { 68 | ok(!isValid({name: [], levelOne: {levelTwo: {random: ['foo'], nestedArray: [['bar'], [], ['baz']]}}})) 69 | }) 70 | 71 | }) 72 | --------------------------------------------------------------------------------