├── .gitmodules ├── src ├── actions │ ├── types │ │ └── validate.types.js │ └── validate.action.js ├── components │ ├── labelErrorComponent.js │ └── props.js ├── middleware │ ├── validate-utils.js │ ├── validate-default-messages.js │ ├── validate-default-patterns.js │ ├── validate-validators.js │ └── validate.js └── reducer │ └── validate.reducer.js ├── index.js ├── .eslintrc ├── LICENSE ├── .gitignore ├── .npmignore ├── package.json └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "example/redux-form-validator-example"] 2 | path = example/redux-form-validator-example 3 | url = git://github.com/posabsolute/redux-form-validator-example 4 | -------------------------------------------------------------------------------- /src/actions/types/validate.types.js: -------------------------------------------------------------------------------- 1 | /* 2 | * action types 3 | */ 4 | 5 | export const MODEL_COMPONENT_DEFAULT_STATE = 'MODEL_COMPONENT_DEFAULT_STATE'; 6 | export const MODEL_INPUT_VALIDATION = 'MODEL_INPUT_VALIDATION'; 7 | export const MODEL_FORM_VALIDATION = 'MODEL_FORM_VALIDATION'; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | import validateReducer from "./src/reducer/validate.reducer"; 3 | import LabelError from "./src/components/labelErrorComponent"; 4 | import validateProps from "./src/components/props"; 5 | import * as validateActions from "./src/actions/validate.action"; 6 | import * as validateTypes from "./src/actions/types/validate.types"; 7 | import validateMiddleware from "./src/middleware/validate"; 8 | 9 | 10 | export {validateReducer, LabelError, validateProps, validateActions, validateTypes, validateMiddleware} -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "globals": { 9 | "If": true, 10 | "For": true, 11 | "connect": true 12 | }, 13 | "rules": { 14 | "react/jsx-uses-react": 2, 15 | "react/jsx-uses-vars": 2, 16 | "react/react-in-jsx-scope": 2, 17 | "block-scoped-var": 0, 18 | "padded-blocks": 0, 19 | "no-console": 0, 20 | }, 21 | "plugins": [ 22 | "react" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/actions/validate.action.js: -------------------------------------------------------------------------------- 1 | import { VALIDATE } from '../middleware/validate'; 2 | import {MODEL_COMPONENT_DEFAULT_STATE} from './types/validate.types'; 3 | 4 | export function setDefaultValidatorState(component, model) { 5 | return { 6 | type: MODEL_COMPONENT_DEFAULT_STATE, 7 | component, 8 | model, 9 | }; 10 | } 11 | 12 | export function validateInput(inputValue, inputName, component, model, inputObject, form) { 13 | return { 14 | [VALIDATE]: { 15 | inputName, 16 | inputValue, 17 | component, 18 | model, 19 | inputObject, 20 | form 21 | }, 22 | }; 23 | } 24 | 25 | export function validateForm(form, component, model) { 26 | return { 27 | [VALIDATE]: { 28 | form, 29 | component, 30 | model, 31 | }, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/labelErrorComponent.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Redux-form-validation 3 | * Error Label Component 4 | * You can use this component to display error messages in your app. Also support hidden classes 5 | */ 6 | import React from 'react'; 7 | import classNames from 'classnames/bind'; 8 | /** 9 | * Error Label Component 10 | * return a stateless react component 11 | * @param {Object} classes - Default css classes always added to the component 12 | * @param {Object} field - Input store current values, inbluding if the input is valid 13 | * @return {React Component} Return the component. 14 | */ 15 | export default ({classes, field}) => { 16 | const labelClass = classNames({ 17 | [classes]: true, 18 | 'label-error': true, 19 | 'label-error--hidden': field.valid === false ? false : true, 20 | }); 21 | 22 | return ( 23 |
24 | { field.message } 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Cedric Dugas 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 | 23 | -------------------------------------------------------------------------------- /src/middleware/validate-utils.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Redux-form-validation 3 | * Utils 4 | * Small functions that can be reused throughout the validation middleware 5 | */ 6 | 7 | /** 8 | * isNumber() - returns a new element 9 | * based on the passed in tag name 10 | * 11 | * @param {Unknown} value 12 | * @return {Bool} if the value is a number 13 | */ 14 | export function isNumber(value) { 15 | return (Object.prototype.toString.call(value) === '[object Number]' || Object.prototype.toString.call(value) === '[object String]') && !isNaN(parseFloat(value)) && isFinite(value.toString().replace(/^-/, '')); 16 | } 17 | /** 18 | * objectValueToArray() - returns a new array from form list object 19 | * used to change radioList object to an input arra 20 | * 21 | * @param {obj} html form object 22 | * @return {Array} 23 | */ 24 | export function objectValueToArray(obj){ 25 | return [...obj]; 26 | } 27 | /** 28 | * Deferred 29 | * Create a function with an exposed promise 30 | * 31 | */ 32 | export class Deferred { 33 | constructor() { 34 | this.promise = new Promise((resolve, reject)=> { 35 | this.reject = reject; 36 | this.resolve = resolve; 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 2 | 3 | *.iml 4 | 5 | ## Directory-based project format: 6 | .idea/ 7 | # if you remove the above rule, at least ignore the following: 8 | 9 | # User-specific stuff: 10 | # .idea/workspace.xml 11 | # .idea/tasks.xml 12 | # .idea/dictionaries 13 | 14 | # Sensitive or high-churn files: 15 | # .idea/dataSources.ids 16 | # .idea/dataSources.xml 17 | # .idea/sqlDataSources.xml 18 | # .idea/dynamic.xml 19 | # .idea/uiDesigner.xml 20 | 21 | # Gradle: 22 | # .idea/gradle.xml 23 | # .idea/libraries 24 | 25 | # Mongo Explorer plugin: 26 | # .idea/mongoSettings.xml 27 | 28 | ## File-based project format: 29 | *.ipr 30 | *.iws 31 | 32 | ## Plugin-specific files: 33 | 34 | # IntelliJ 35 | /out/ 36 | 37 | # mpeltonen/sbt-idea plugin 38 | .idea_modules/ 39 | # JIRA plugin 40 | atlassian-ide-plugin.xml 41 | 42 | # Crashlytics plugin (for Android Studio and IntelliJ) 43 | com_crashlytics_export_strings.xml 44 | crashlytics.properties 45 | crashlytics-build.properties 46 | node_modules/ 47 | .sass-cache/ 48 | bower_components/ 49 | dist/ 50 | app/bower_components 51 | .DS_Store 52 | .tmp 53 | ios 54 | .yo-rc.json 55 | .jshintrc 56 | .gitattributes 57 | .editorconfig 58 | .babelrc 59 | npm-debug.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 2 | 3 | *.iml 4 | 5 | ## Directory-based project format: 6 | .idea/ 7 | # if you remove the above rule, at least ignore the following: 8 | 9 | # User-specific stuff: 10 | # .idea/workspace.xml 11 | # .idea/tasks.xml 12 | # .idea/dictionaries 13 | 14 | # Sensitive or high-churn files: 15 | # .idea/dataSources.ids 16 | # .idea/dataSources.xml 17 | # .idea/sqlDataSources.xml 18 | # .idea/dynamic.xml 19 | # .idea/uiDesigner.xml 20 | 21 | # Gradle: 22 | # .idea/gradle.xml 23 | # .idea/libraries 24 | 25 | # Mongo Explorer plugin: 26 | # .idea/mongoSettings.xml 27 | 28 | ## File-based project format: 29 | *.ipr 30 | *.iws 31 | 32 | ## Plugin-specific files: 33 | 34 | # IntelliJ 35 | /out/ 36 | 37 | # mpeltonen/sbt-idea plugin 38 | .idea_modules/ 39 | # JIRA plugin 40 | atlassian-ide-plugin.xml 41 | 42 | # Crashlytics plugin (for Android Studio and IntelliJ) 43 | com_crashlytics_export_strings.xml 44 | crashlytics.properties 45 | crashlytics-build.properties 46 | node_modules/ 47 | .sass-cache/ 48 | bower_components/ 49 | dist/ 50 | app/bower_components 51 | .DS_Store 52 | .tmp 53 | ios 54 | .yo-rc.json 55 | .jshintrc 56 | .gitattributes 57 | .editorconfig 58 | .babelrc 59 | npm-debug.log -------------------------------------------------------------------------------- /src/reducer/validate.reducer.js: -------------------------------------------------------------------------------- 1 | import {MODEL_COMPONENT_DEFAULT_STATE, MODEL_INPUT_VALIDATION, MODEL_FORM_VALIDATION} from '../actions/types/validate.types'; 2 | 3 | const initialState = {}; 4 | 5 | export default function validation(state = initialState, action) { 6 | let newState = Object.assign({}, state); 7 | 8 | switch (action.type) { 9 | case MODEL_INPUT_VALIDATION: 10 | let inputState = newState[action.component][action.model][action.inputName] || {}; 11 | newState[action.component][action.model][action.inputName] = { 12 | ...inputState, 13 | ...action.state, 14 | }; 15 | return newState; 16 | 17 | 18 | case MODEL_COMPONENT_DEFAULT_STATE: 19 | return { 20 | ...state, 21 | [action.component]: { 22 | [action.model]: {}, 23 | }, 24 | }; 25 | 26 | case MODEL_FORM_VALIDATION: 27 | let modelState = newState[action.component][action.model]; 28 | newState[action.component][action.model] = { 29 | ...modelState, 30 | ...action.state, 31 | } 32 | return newState; 33 | 34 | default: 35 | return state; 36 | } 37 | } 38 | 39 | /* 40 | validation = { 41 | login : { 42 | user: { 43 | url:{ 44 | validate: true, 45 | message: "This field is required" 46 | } 47 | } 48 | } 49 | } 50 | */ 51 | -------------------------------------------------------------------------------- /src/middleware/validate-default-messages.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Redux-form-validation 3 | * Messages 4 | * Default validation messages, input error messages can be overriden in the model 5 | */ 6 | export default { 7 | required: () => `This field is required`, 8 | acceptance: () => `You must be accept the terms`, 9 | min: (messages) => `Must be greater than or equal to ${messages.rule}`, 10 | max: (messages) => `Must be less than or equal to ${messages.rule}`, 11 | range: (messages) => `Must be between ${messages.rule[0]} and ${messages.rule[1]}`, 12 | length: (messages) => `Must be ${messages.rule} characters`, 13 | minLength: (messages) => `Must be at least ${messages.rule} characters`, 14 | maxLength: (messages) => `Must be at most ${messages.rule} characters`, 15 | minChecked: (messages) => `Choose at least ${messages.rule} options`, 16 | maxChecked: (messages) => `Choose a maximum of ${messages.rule} options`, 17 | rangeLength: (messages) => `Must be between ${messages.rule[0]} and ${messages.rule[1]} characters`, 18 | oneOf: (messages) => `Must be one of: ${messages.rule}`, 19 | equalTo: (messages) => `Must be the same as ${messages.rule}`, 20 | digits: () => `Must only contain digits`, 21 | number: () => `Must be a number`, 22 | email: () => `Must be a valid email`, 23 | url: () => `Must be a valid url`, 24 | inlinePattern: () => `Is invalid`, 25 | }; 26 | 27 | -------------------------------------------------------------------------------- /src/middleware/validate-default-patterns.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Redux-form-validation 3 | * Patterns 4 | * Expose often use regex validator patterns 5 | * Most of them are taken shamelessly from backbone.validation (https://github.com/thedersen/backbone.validation) 6 | */ 7 | export default { 8 | // Matches any digit(s) (i.e. 0-9) 9 | digits: /^\d+$/, 10 | 11 | // Matches any number (e.g. 100.000) 12 | number: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/, 13 | 14 | // Matches a valid email address (e.g. mail@example.com) 15 | email: /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i, 16 | 17 | // Mathes any valid url (e.g. http://www.xample.com) 18 | url: /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i, 19 | }; 20 | -------------------------------------------------------------------------------- /src/middleware/validate-validators.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Redux-form-validation 3 | * Validators 4 | * Most of them are taken shamelessly from backbone.validation (https://github.com/thedersen/backbone.validation) 5 | */ 6 | import {isNumber, Deferred, objectValueToArray} from './validate-utils'; 7 | import defaultPatterns from './validate-default-patterns'; 8 | 9 | export default { 10 | required(value, rule, list, type) { 11 | if(type === "[object RadioNodeList]"){ 12 | const radioElements = objectValueToArray(list); 13 | const checkedInputs = radioElements.filter( item => item.checked === true); 14 | console.log(checkedInputs.length ? true : false) 15 | return checkedInputs.length ? true : false; 16 | } 17 | return value ? true : false; 18 | }, 19 | // function validator 20 | // Call a function that must return true 21 | func(value, rule) { 22 | if (typeof rule !== 'function') { 23 | console.warn('func validation rule is not a function', rule); 24 | return false; 25 | } 26 | return rule(value); 27 | }, 28 | // Async validator 29 | // Send a promess to the model async validation option, 30 | // the promess must be resolved or rejected by the async function 31 | async(value, rule) { 32 | const asyncPromise = new Deferred(); 33 | if (typeof rule.then === 'function') { 34 | console.warn('async is not a promise', rule); 35 | asyncPromise.reject(); 36 | } 37 | rule.call(asyncPromise, value); 38 | return asyncPromise; 39 | }, 40 | // Acceptance validator 41 | // Validates that something has to be accepted, e.g. terms of use 42 | // true or 'true' are valid 43 | acceptance(value) { 44 | if (value != true) { 45 | return false; 46 | } 47 | return true; 48 | }, 49 | // minChecked validator 50 | // Validate that the checkbox group has a minimum of box checked 51 | // the min value specified 52 | minChecked(value, rule, list) { 53 | const checkboxElements = objectValueToArray(list); 54 | const checkedInputs = checkboxElements.filter( item => item.checked === true); 55 | if (checkedInputs.length < rule) { 56 | return false; 57 | } 58 | return true; 59 | }, 60 | // maxChecked validator 61 | // Validate that the checkbox group has no more than the maximum allowed of box checked 62 | // the max value specified 63 | maxChecked(value, rule, list) { 64 | const checkboxElements = objectValueToArray(list); 65 | const checkedInputs = checkboxElements.filter( item => item.checked === true); 66 | if (checkedInputs.length > rule) { 67 | return false; 68 | } 69 | return true; 70 | }, 71 | // Max validator 72 | // Validates that the value has to be a number and equal to or less than 73 | // the max value specified 74 | max(value, rule) { 75 | if (!isNumber(value) || value > rule) { 76 | return false; 77 | } 78 | return true; 79 | }, 80 | // Range validator 81 | // Validates that the value has to be a number and equal to or between 82 | // the two numbers specified 83 | range(value, rule) { 84 | if (!isNumber(value) || value < rule[0] || value > rule[1]) { 85 | return false; 86 | } 87 | return true; 88 | }, 89 | // Length validator 90 | // Validates that the value has to be a string with length equal to 91 | // the length value specified 92 | length(value, rule) { 93 | if (typeof value !== 'string' || value.length !== rule) { 94 | return false; 95 | } 96 | return true; 97 | }, 98 | // Min length validator 99 | // Validates that the value has to be a string with length equal to or greater than 100 | // the min length value specified 101 | minLength(value, rule) { 102 | if (typeof value !== 'string' || value.length < rule) { 103 | return false; 104 | } 105 | return true; 106 | }, 107 | // Max length validator 108 | // Validates that the value has to be a string with length equal to or less than 109 | // the max length value specified 110 | maxLength(value, rule) { 111 | if (typeof value !== 'string' || value.length > rule) { 112 | return false; 113 | } 114 | return true; 115 | }, 116 | // Range length validator 117 | // Validates that the value has to be a string and equal to or between 118 | // the two numbers specified 119 | rangeLength(value, rule) { 120 | if (typeof value !== 'string' || value.length < rule[0] || value.length > rule[1]) { 121 | return false; 122 | } 123 | return true; 124 | }, 125 | // One of validator 126 | // Validates that the value has to be equal to one of the elements in 127 | // the specified array. Case sensitive matching 128 | oneOf(value, rule) { 129 | const elem = rule.find( (arrayVal) => arrayVal == value); 130 | if (!elem) { 131 | return false; 132 | } 133 | return true; 134 | }, 135 | 136 | // Equal to input in form 137 | // Validates that the value has to be equal to the value of the attribute 138 | // with the name specified 139 | equalTo(value, rule, list, inputType, form) { 140 | if (value !== form[rule].value) { 141 | return false; 142 | } 143 | return true; 144 | }, 145 | // Pattern validator 146 | // Validates that the value has to match the pattern specified. 147 | // Can be a regular expression or the name of one of the built in patterns 148 | pattern(value, rule) { 149 | // Don' validate empty inputs. 150 | if (!value || value.length === 0) { 151 | return true; 152 | } 153 | 154 | if (!value.toString().match(defaultPatterns[rule] || rule)) { 155 | return false; 156 | } 157 | 158 | return true; 159 | }, 160 | }; 161 | 162 | -------------------------------------------------------------------------------- /src/components/props.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Redux-form-validation 3 | * Component props 4 | * props are added to an input 5 | * must be init in componentDidMount. this.validate = validate(this, myModelToValidate); 6 | */ 7 | 8 | import classNames from 'classnames/bind'; 9 | 10 | const validate = (component, model, validatorName) => { 11 | 12 | function getComponentName() { 13 | return validatorName || component.constructor.name; 14 | } 15 | /** 16 | * init() 17 | * Check you have everything you need to validate form & inputs 18 | * Set default component/model state in the store 19 | */ 20 | function init() { 21 | if (!model.name) { 22 | console.warn('Model has no name, validation will not work'); 23 | } 24 | if (!model.data) { 25 | console.warn('Your model has no validation rules under data'); 26 | } 27 | if (!getComponentName()) { 28 | console.warn('Your form name is undefined, it should be passed as 3rd argument'); 29 | } 30 | // Call actions set default component/model state in the store 31 | component.props.setDefaultValidatorState(getComponentName(), model.name); 32 | } 33 | init(); 34 | /** 35 | * return helpers that can be used by the component 36 | */ 37 | return { 38 | /** 39 | * onBlur() 40 | * Input onBlur Event is overriden to validate input 41 | * you can easily override this function & do the validation yourself with component.validate.input(evt.target.value, evt.target.name); 42 | * @param {Object} evt - input blur react event 43 | */ 44 | onBlur: (evt) => { 45 | component.validate.input(evt.target.value, evt.target.name, evt.target.form.elements[evt.target.name], evt.target.form.elements); 46 | }, 47 | /** 48 | * fieldStore() 49 | * get the input store state back 50 | * @param {String} field - Name of the input 51 | * @return {Object} fieldStore - The store values for this input (including if it is valid) 52 | */ 53 | fieldStore: (field) => { 54 | let fieldStore = {}; 55 | const store = component.props.validationStore; 56 | if(!store){ console.warn('validation store is undefined'); } 57 | 58 | if (store && store[getComponentName()] && store[getComponentName()][model.name] && store[getComponentName()][model.name][field]) { 59 | fieldStore = store[getComponentName()][model.name][field]; 60 | } 61 | return fieldStore; 62 | }, 63 | 64 | /** 65 | * formStore() 66 | * get the form store state back 67 | * @return {Object} formStore - The store values for this form (including if it is valid) 68 | */ 69 | formStore: () => { 70 | let formStore = {}; 71 | const store = component.props.validationStore; 72 | if(!store){ console.warn('validation store is undefined'); } 73 | if (store && store[getComponentName()] && store[getComponentName()][model.name] && store[getComponentName()][model.name]) { 74 | formStore = store[getComponentName()][model.name]; 75 | } 76 | return formStore; 77 | }, 78 | /** 79 | * classes() 80 | * return the css classes for an input, validating agains the input storex 81 | * @param {Object} classes - Passed at the component level, classes always added to the input component 82 | * @param {String} el - Name of the input 83 | * @return {String} inputClasses - Return all the classes in 1 string for this input 84 | */ 85 | classes: (classes, el) => { 86 | if (!el) { 87 | console.warn('You must pass the inputname when using validate.classes'); 88 | } 89 | let inputClasses; 90 | if (typeof classes === 'object') { 91 | inputClasses = { 92 | ...classes, 93 | 'input-validation-error': !component.validate.isInputValid(el), 94 | }; 95 | } else { 96 | inputClasses = { 97 | [classes]: true, 98 | 'input-validation-error': !component.validate.isInputValid(el), 99 | }; 100 | } 101 | return classNames(inputClasses); 102 | }, 103 | /** 104 | * isInputValid() 105 | * return if the input is valid from the value in the store 106 | * @param {String} el - Name of the input 107 | * @return {Bool} isValid - Return is the input is valid 108 | */ 109 | isInputValid: (el) => { 110 | let isValid = true; 111 | if (component.validate.fieldStore(el).valid === false) { 112 | isValid = false; 113 | } 114 | return isValid; 115 | }, 116 | /** 117 | * input() 118 | * Call the input validation action 119 | * @param {String} value - Value of the input 120 | * @param {String} name - Name of the input 121 | * Default: 122 | * @return {Bool} isValid - input validation state 123 | * if validation incluse an async validation rule, we return a promise 124 | * @return {Promise} inputState.promise.promise - Return a promise that will trigger a resolved or reject state depending on input validity. 125 | */ 126 | input: (value, name, inputObject, form) => { 127 | return component.props.validateInput(value, name, getComponentName(), model, inputObject, form); 128 | }, 129 | /** 130 | * form() 131 | * Call the input validation action 132 | * @param {String} value - Value of the input 133 | * @param {String} name - Name of the input 134 | * Default: 135 | * @return {Bool} isValid - input validation state 136 | * if validation incluse an async validation rule, we return a promise 137 | * @return {Promise} inputState.promise.promise - Return a promise that will trigger a resolved or reject state depending on input validity. 138 | */ 139 | formValidate: (inputs) => { 140 | return component.props.validateForm(inputs, getComponentName(), model); 141 | }, 142 | }; 143 | }; 144 | 145 | export default validate; 146 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-form-validator", 3 | "version": "2.0.5", 4 | "description": "Form validation for redux", 5 | "main": "index.js", 6 | "format" : "es6", 7 | "jspm": { 8 | "format" : "es6" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [ 14 | "redux", 15 | "react-component", 16 | "form", 17 | "react-component", 18 | "validation", 19 | "react" 20 | ], 21 | "author": { 22 | "name": "Cedric Dugas" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/posabsolute/redux-form-validator/issues" 26 | }, 27 | "homepage": "https://github.com/posabsolute/redux-form-validator#readme", 28 | "license": "MIT", 29 | "dependencies": { 30 | "classnames": "^2.2.1", 31 | "react": "^0.14.3", 32 | "react-redux": "^4.0.5", 33 | "redux": "^3.0.5" 34 | }, 35 | "readme": "# Redux Form Validation\nAn es6 redux form validator middleware that help you validate inputs.\n\nDemo: [Live Example](http://posabsolute.github.io/redux-flash-notification-example/) | [Source](https://github.com/posabsolute/redux-flash-notification-example)\n\nBetter Documentation: [http://posabsolute.github.io/redux-flash-notification](http://posabsolute.github.io/redux-flash-notification)\n\n\n## Integration\n\n\n1 npm install 'redux-form-validation' --save\n\n\n2 Add the reducer to your root reducer\n\n```javascript\n\nimport {validateReducer} from 'redux-form-validation';\n\nconst rootReducer = combineReducers({\n validate: validateReducer,\n});\n\nexport default rootReducer;\n```\n\n3 Add the validate middleware\n```javascript\nimport {validateMiddleware} from 'redux-form-validation';\n\nconst createStoreWithMiddleware = compose(\n validateMiddleware,\n ),\n window.devToolsExtension ? window.devToolsExtension() : f => f\n)(createStore);\n\n```\n\n4 Connect Validate store, actions & specicify the component & model to validate. It is important this be available throughout every components that use validation, you can trickle them down throught props.\n\nIn your componentWillMount init the validation component:\n\n```javascript\nimport {validate, validateActions} from 'redux-form-validation';\n\nconst mapStateToProps = (state) => {\n return {\n validationStore: state.validation,\n };\n};\n\nconst mapDispatchToProps = (\n dispatch,\n) => {\n return {\n ...bindActionCreators(validateActions, dispatch),\n };\n};\n\n\n@connect(mapStateToProps, mapDispatchToProps)\nexport default class LoginComponent extends React.Component {\n componentWillMount() {\n this.validate = validate(this, userModel);\n }\n render() {\n return ;\n }\n}\n\n5. Add validation to your inputs, there is also a error label component for your convenience. Unfortunately you cannot use stateless component because the middleware makes use of refs.\n\n a. add {...validate} to your input\n b. add a name & ref to your input (must be the same)\n c. To get the error class, use className={validate.classes('input-line', 'url')}\n\nIt should look like:\n```javascript\n\n\n```\n\n```javascript\nimport React from 'react';\nimport LabelError from 'components/validation/labelErrorComponent';\n\nclass LoginFormComponent extends React.Component {\n render() {\n const {validate, onSubmit} = this.props;\n return (\n
{ evt.preventDefault(); onSubmit.call(this, validate);} }>\n
\n \n \n
\n
\n \n \n
\n
\n \n
\n
\n
\n );\n }\n}\n```\n\n6. Create a model\n\nAnatomy of a model\n\n```javascript\nconst userModel = {\n name:'userModel',\n data: {\n 'url': {\n validate: {\n required: true,\n func: (value) => {\n return true;\n },\n message: 'This is a test',\n },\n },\n },\n}\n```\n\n7 Using webpack? include jsx/es6\n```javascript\n module: {\n loaders: [{\n test:[/\\.jsx$/, /\\.js$/],\n loaders: ['react-hot', 'babel?stage=0&loose[]=es6.modules'],\n include: [\n path.resolve(__dirname, \"src\"),\n path.resolve(__dirname, \"node_modules/flash-notification-react-redux\")\n ],\n }, {\n test: [/\\.scss$/, /\\.css$/],\n loader: 'css?localIdentName=[path]!postcss-loader!sass',\n }],\n },\n};\n```\n\n8 You're done.\n\n\n## Using actions\n\nYou can use validation actions to execute code depending if the form or input is valid. It's a good way to control side effects like calling an api action once the field if valid.\n\n### Validate Sync Form\n```javascript\nonSubmit: function(validateProps) {\n const inputs = this.refs;\n if (validateProps.form(form)) {\n // form is valid, redirect to nre page\n }else{\n // form is not valid\n }\n}\n```\n### Validate Async Form\nIf you validate asyncly 1 input or form, you must use a promise instead of just using a bool.\n```javascript\nonSubmit: function submit(validateProps) {\n const inputs = this.refs;\n validateProps.form(inputs).then(() =>{\n console.log('form is valid');\n }).catch(() => { \n console.log(\"form is not valid\"); \n });\n}\n```\n\n### Validate Sync input\n\n```javascript\nif(this.validate.input(value, field)){\n // input is valid\n}else{\n // input is not valid\n}\n```\n\n\n### Validate Async input\n\n```javascript\nthis.validate.input(value, field).then(() => {\n // input is valid\n})\n.catch(function(errorMessage) {\n // input is not valid\n});\n```\n\n\n## Validation model\n\n### data\n\nA Model must have a data object that describe fields to validate. Under the validate object list all the validators you want to use.\n\n### Global Validate functions\n\nThe model can also have global validation functions that are executed once all inputs are valid.\n\n#### validate(form, dispatch)\n\nUsed to do sync validations after all your inputs are valid. Must return true or false\n\n```javascript\nconst userModel = {\n name:'userModel',\n data: {\n 'url': {\n validate: {\n required: true,\n func: (value) => {\n return true;\n },\n message: 'This is a test',\n },\n },\n },\n validate: (form, dispatch) => {\n // form\n let validate = false;\n if (!form.url.value) {\n dispatch({\n type: 'GROWLER__SHOW',\n growler: {\n text: 'Please enter your url',\n type: 'growler--error',\n },\n });\n validate = false;\n }\n\n return true;\n },\n};\n```\n\n## Built-in validators\n\n### func validator\n\nLets you implement a custom function used for validation.\n\n```js\nconst userModel = {\n name:'userModel',\n data: {\n 'username': {\n validate: {\n required: true,\n pattern: 'email',\n async: function() {\n setTimeout( () => {\n this.resolve(\"yes\");\n }, 5000);\n },\n },\n },\n```\n\n\n\n\n### async validator\n\nLets you implement a custom async function used for validation using a Promise. Must return this.resolve or this.reject. You can reject with a custom message passed as a string.\n\n```js\nconst userModel = {\n name:'userModel',\n data: {\n 'username': {\n validate: {\n required: true,\n pattern: 'email',\n async: function() {\n setTimeout( () => {\n this.reject(\"Sorry, this username is already used.\");\n }, 5000);\n },\n },\n },\n```\n\n### required\n\nValidates if the attribute is required or not.\nThis can be specified as either a boolean value or a function that returns a boolean value.\n\n```js\nconst userModel = {\n name:'userModel',\n data: {\n 'username': {\n validate: {\n required: true,\n pattern: 'email',\n },\n },\n },\n};\n```\n\n### acceptance\n\nValidates that something has to be accepted, e.g. terms of use. `true` or 'true' are valid.\n\n```js\nconst userModel = {\n name:'userModel',\n data: {\n 'username': {\n validate: {\n required: true,\n acceptance: true\n }\n }\n};\n```\n\n### min\n\nValidates that the value has to be a number and equal to or more than the min value specified.\n\n```js\nconst userModel = {\n name:'userModel',\n data: {\n 'age': {\n validate: {\n min: 1,\n }\n }\n }\n});\n```\n\n### max\n\nValidates that the value has to be a number and equal to or less than the max value specified.\n\n```js\nconst userModel = {\n name:'userModel',\n data: {\n 'age': {\n validate: {\n max: 100,\n }\n }\n }\n};\n```\n\n### range\n\nValidates that the value has to be a number and equal to or between the two numbers specified.\n\n```js\nconst userModel = {\n name:'userModel',\n data: {\n 'age': {\n validate: {\n range: [1, 10],\n }\n }\n }\n};\n```\n\n### length\n\nValidates that the value has to be a string with length equal to the length value specified.\n\n```js\nconst userModel = {\n name:'userModel',\n data: {\n 'postalCode': {\n validate: {\n length: 4,\n }\n }\n }\n};\n```\n\n### minLength\n\nValidates that the value has to be a string with length equal to or greater than the min length value specified.\n\n```js\nconst userModel = {\n name:'userModel',\n data: {\n 'password': {\n validate: {\n minLength: 8\n }\n }\n }\n};\n```\n\n\n### maxLength\n\nValidates that the value has to be a string with length equal to or less than the max length value specified.\n\n```js\nconst userModel = {\n name:'userModel',\n data: {\n 'password': {\n validate: {\n maxLength: 100\n }\n }\n }\n};\n```\n\n### rangeLength\n\nValidates that the value has to be a string and equal to or between the two numbers specified.\n\n```js\nconst userModel = {\n name:'userModel',\n data: {\n 'password': {\n validate: {\n rangeLength: [6, 100]\n }\n }\n }\n};\n```\n\n### oneOf\n\nValidates that the value has to be equal to one of the elements in the specified array. Case sensitive matching.\n\n```js\nconst userModel = {\n name:'userModel',\n data: {\n 'country': {\n validate: {\n oneOf: ['Norway', 'Sweeden']\n }\n }\n }\n};\n```\n\n### equalTo\n\nValidates that the value has to be equal to the value of the attribute with the name specified.\n\n```js\nconst userModel = {\n name:'userModel',\n data: {\n 'password': {\n validate: {\n equalTo: 'password'\n }\n }\n }\n};\n```\n\n\n### pattern\n\nValidates that the value has to match the pattern specified. Can be a regular expression or the name of one of the built in patterns.\n\n```js\nconst userModel = {\n name:'userModel',\n data: {\n 'email': {\n validate: {\n pattern: 'email'\n }\n }\n }\n};\n```\n\nThe built-in patterns are:\n\n* number - Matches any number (e.g. -100.000,00)\n* email - Matches a valid email address (e.g. mail@example.com)\n* url - Matches any valid url (e.g. http://www.example.com)\n* digits - Matches any digit(s) (i.e. 0-9)\n\nSpecify any regular expression you like:\n\n```js\nconst userModel = {\n name:'userModel',\n data: {\n 'email': {\n validate: {\n pattern: /^sample/\n }\n }\n }\n};\n```\n\n\n## Limitations\n\nThis component is based on the use of redux, react, es6 & es7 (decorators) and webpack for loading the css as an import module.\n", 36 | "readmeFilename": "README.md", 37 | "_id": "redux-form-validator@1.0.0", 38 | "_shasum": "c7429bb73445d9376932aea92be3d3399ebd2ff7", 39 | "_from": "redux-form-validator@>=1.0.0 <2.0.0" 40 | } 41 | -------------------------------------------------------------------------------- /src/middleware/validate.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Redux-form-validation 3 | * Validate Middleware 4 | * Do the validation heavy-lifting 5 | */ 6 | import validateFuncs from './validate-validators'; 7 | import defaultMessages from './validate-default-messages'; 8 | import {Deferred} from './validate-utils'; 9 | 10 | export const VALIDATE = Symbol('Validate'); 11 | 12 | 13 | export default store => next => action => { 14 | const validator = action[VALIDATE]; 15 | // Check if the middleware is directly called in a action 16 | if (typeof validator === 'undefined') { 17 | return next(action); 18 | } 19 | // get data from the action call 20 | const { form, inputName, inputValue, model, component, inputObject } = validator; 21 | // init is done at the end 22 | /** 23 | * validateRules() 24 | * Take each validation rule from an input and validates. 25 | * 26 | * @param {Object} rules - all the validation rules defined for an input 27 | * @param {String} inputValue - input value to validate 28 | * @return {Object} input - input status added to the validation state. 29 | */ 30 | function validateRules(rules, value, inputData, inputType) { 31 | // default store value for an input 32 | let input = { 33 | valid: true, 34 | rules: [], 35 | }; 36 | // Take each validation rules & validate 37 | Object.keys(rules).forEach( rule => { 38 | // if validator exist 39 | if (validateFuncs[rule]) { 40 | // return validation status 41 | const validation = validateFuncs[rule](value, rules[rule], inputData, inputType, form); 42 | 43 | // Do we have an async validation with promise 44 | if (rule === 'async') { 45 | input.isPromise = true; 46 | input.promise = validation; 47 | } 48 | 49 | // if we have an error 50 | // validation can either be false 51 | // or an error message string 52 | if (validation !== true) { 53 | input.valid = false; 54 | // add the errored rule to store 55 | input.rules.push(rule); 56 | // Define Message used 57 | const messages = { 58 | rule: rules[rule], 59 | }; 60 | input.message = rules.message || 61 | (defaultMessages[rule] && defaultMessages[rule](messages)) || 62 | (defaultMessages[rules[rule]] && defaultMessages[rules[rule]](messages)) || 63 | validation; 64 | } 65 | } 66 | }); 67 | return input; 68 | } 69 | 70 | /** 71 | * dispatchAction() 72 | * Dispatch action to store 73 | * 74 | * @param {Object} state - input state 75 | */ 76 | function dispatchInputAction(state, name) { 77 | next({ 78 | type: 'MODEL_INPUT_VALIDATION', 79 | model: model.name, 80 | component, 81 | inputName: name, 82 | state, 83 | }); 84 | } 85 | 86 | function dispatchFormAction(formState) { 87 | next({ 88 | type: 'MODEL_FORM_VALIDATION', 89 | model: model.name, 90 | component, 91 | state: formState, 92 | }); 93 | } 94 | 95 | function dispatchErrorFormAction(formState, message) { 96 | formState.valid = false; 97 | if (typeof message === 'string') { 98 | formState.message = message; 99 | } 100 | dispatchFormAction(formState); 101 | } 102 | /** 103 | * validateInput() 104 | * Validate one input (multiple rules) 105 | * 106 | * Default: 107 | * @return {Bool} isValid - input validation state 108 | * if validation incluse an async validation rule, we return a promise 109 | * @return {Promise} inputState.promise.promise - Return a promise that will trigger a resolved or reject state depending on input validity. 110 | */ 111 | function validateInput(name = inputName, value = inputValue, inputData = inputObject) { 112 | // get validation rules 113 | const modelData = model.data[name]; 114 | const inputType = inputData.toString(); 115 | // get input state 116 | if(!modelData || !modelData.validate){ 117 | console.warn('undefined validation rules for ' + name); 118 | return { 119 | isValid : true, 120 | value: value, 121 | }; 122 | } 123 | 124 | let inputState = validateRules(modelData.validate, value, inputData, inputType); 125 | // we save the value for the store 126 | inputState.currentValue = value; 127 | 128 | // is the input has an async validation rule; 129 | if (inputState.isPromise) { 130 | dispatchInputAction({state:'isValidating'}, name); 131 | // in case any other rules state the input is not valid, reject promise 132 | if (!inputState.valid) { inputState.promise.reject(inputState); } 133 | 134 | // if validation function resolve promise, dispatch a valid action on the input 135 | inputState.promise.promise.then(() =>{ 136 | inputState.state = 'done'; 137 | inputState.valid = true; 138 | inputState.message = ''; 139 | dispatchInputAction(inputState, name); 140 | }) 141 | // if validation fails because of another validation rule or 142 | // any other rules state the input is not valid, reject promise 143 | .catch((state) => { 144 | inputState.state = 'done'; 145 | inputState.valid = false; 146 | // other rules pass back an object 147 | if (typeof state === 'object') { 148 | inputState = state; 149 | // in case the async validation rule pass down an error message when it reject it. 150 | }else if (typeof state === 'string') { 151 | inputState.message = state; 152 | } 153 | // dispatch input error status to the store 154 | dispatchInputAction(inputState, name); 155 | }); 156 | // return promise so it can be used in component code. this.validateInput('username').then() 157 | return inputState.promise.promise; 158 | } 159 | // in case we are not in a promise, dispatch input state 160 | // & return input validity, so it can be used in component code. if(this.validateInput('username')){} 161 | if(inputState.valid){ 162 | inputState.message = ''; 163 | } 164 | dispatchInputAction(inputState, name); 165 | return { 166 | isValid : inputState.valid, 167 | value: value, 168 | }; 169 | } 170 | 171 | /** 172 | * getFormValues(form.elements) 173 | * return form values 174 | * 175 | * @param {form} form.elements -html5 form elements 176 | * @return {object} formValues - all form values 177 | */ 178 | function getFormValues(formElements = form) { 179 | let formValues = {}; 180 | 181 | for (let i=0; i { 241 | // form.elements contain both a numeric and named object for the same input 242 | // use the named only 243 | if(!isNaN(input)){return;} 244 | const inputState = validateInput( input, form[input].value, form[input]); 245 | // if return a bool false 246 | if (!inputState.isValid && !inputState.then) { 247 | formState.valid = false; 248 | } 249 | // if return a promise we add the promise to array for later use of promise.all() 250 | if (inputState.then) { 251 | formState.isPromise = true; 252 | promises.push(inputState); 253 | } 254 | }); 255 | } 256 | /** 257 | * rejectPromise() 258 | * Small utils used by handleInputsPromises() & handleFormAsyncValidation 259 | * Used to reject early when in async mode 260 | * 261 | * @return {Promise} formState.async.promise 262 | */ 263 | function rejectPromise() { 264 | formState.async.reject(); 265 | formState.valid = false; 266 | dispatchFormAction(formState); 267 | return formState.async.promise; 268 | } 269 | /** 270 | * handleInputsPromises() 271 | * Used to validate all input promises 272 | * Also contain workflow to finish validation 273 | * 274 | * @return {Promise} formState.async.promise || formState.asyncAll 275 | */ 276 | function handleInputsPromises() { 277 | if (formState.isPromise) { 278 | // is we already know the form is not valid form sync input validation, reject 279 | // we also validate the global model sync validation 280 | // we must reject an empty promise, you cannot reject Promise.all manually 281 | if (!formState.valid || (model.validate && !model.validate(form, store.dispatch).valid)) { 282 | return rejectPromise(); 283 | } 284 | // we need to return the final promise 285 | // in the case of a inputAsync & a formAsync we must add the form async 286 | // to the inputs promise array 287 | if(model.validateAsync){ 288 | promises.push(handleFormAsyncValidation()); 289 | } 290 | // add all promises together 291 | formState.asyncAll = Promise.all(promises); 292 | // all the promise resolve, move ahead! 293 | formState.asyncAll.then(() =>{ 294 | dispatchFormAction(formState); 295 | }) 296 | // if validation fails because one of the promise does not resolve 297 | .catch((message) => { 298 | dispatchErrorFormAction(formState, message); 299 | }); 300 | // when in promise mode we return a promise to the component 301 | return formState.asyncAll; 302 | } 303 | } 304 | /** 305 | * handleFormAsyncValidation() 306 | * Used to validate global model async validation 307 | * 308 | * @return {Promise} model.validateAsync.promise 309 | */ 310 | function handleFormAsyncValidation() { 311 | // model has an async function 312 | if (model.validateAsync) { 313 | dispatchFormAction({state:'isValidating'}); 314 | // reject early of form is already not valid 315 | if (!formState.valid || (model.validate && !model.validate(form, store.dispatch))) { 316 | return rejectPromise(); 317 | } 318 | // call validate async function & return the promise 319 | model.validateAsync.call(formState.async, form, store.dispatch); 320 | formState.async.promise.then(() => { 321 | formState.state = 'done'; 322 | dispatchFormAction(formState); 323 | }).catch((message) => { 324 | formState.state = 'done'; 325 | dispatchErrorFormAction(formState, message); 326 | }); 327 | return formState.async.promise; 328 | } 329 | } 330 | 331 | } 332 | /** 333 | * init the validation middleware 334 | * Are we validating a form or an input 335 | */ 336 | if (inputName) { 337 | return validateInput(); 338 | } 339 | if (form) { 340 | return validateForm(); 341 | } 342 | }; 343 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux Form Validation 2 | An es6 redux form validator middleware that helps you manage inputs. This middleware is not about keeping your global form state; it's about keeping your form validation state. 3 | 4 | If you are looking for a fully controlled input approach, you should head to redux-form. 5 | 6 | About controlled inputs: [My controlled input dilemma with react & redux](http://www.position-absolute.com/?p=5264) 7 | 8 | Demo: [Live Example](http://posabsolute.github.io/redux-form-validator-example/) | [Source](https://github.com/posabsolute/redux-form-validator-example) 9 | 10 | Documentation: [http://posabsolute.github.io/redux-form-validator](http://posabsolute.github.io/redux-form-validator) 11 | 12 | 13 | ## Integration 14 | 15 | 16 | 1 npm install 'redux-form-validation' --save 17 | 18 | 19 | 2 Add the reducer to your root reducer 20 | 21 | ```javascript 22 | 23 | import {validateReducer} from 'redux-form-validation'; 24 | 25 | const rootReducer = combineReducers({ 26 | validate: validateReducer, 27 | }); 28 | 29 | export default rootReducer; 30 | ``` 31 | 32 | 3 Add the validate middleware 33 | ```javascript 34 | import {validateMiddleware} from 'redux-form-validation'; 35 | 36 | const createStoreWithMiddleware = compose( 37 | validateMiddleware, 38 | ), 39 | window.devToolsExtension ? window.devToolsExtension() : f => f 40 | )(createStore); 41 | 42 | ``` 43 | 44 | 4 Connect Validate store, actions & specify the component & model to validate. It is important this be available throughout every component that uses validation, you can trickle them down through props. 45 | 46 | In your componentWillMount init the validation component: 47 | 48 | ```javascript 49 | import {validate, validateActions} from 'redux-form-validation'; 50 | 51 | const mapStateToProps = (state) => { 52 | return { 53 | validationStore: state.validation, 54 | }; 55 | }; 56 | 57 | const mapDispatchToProps = ( 58 | dispatch, 59 | ) => { 60 | return { 61 | ...bindActionCreators(validateActions, dispatch), 62 | }; 63 | }; 64 | 65 | 66 | @connect(mapStateToProps, mapDispatchToProps) 67 | export default class LoginComponent extends React.Component { 68 | componentWillMount() { 69 | this.validate = validate(this, userModel); 70 | } 71 | render() { 72 | return ; 73 | } 74 | } 75 | ``` 76 | 77 | 5. Add validation to your inputs, there is also an error label component for your convenience. 78 | 79 | a. add {...validate} to your input 80 | b. add a name to your input (the middleware use the html5 form.elements) 81 | c. To get the error class on your input, use className={validate.classes('input-line', 'url')} 82 | 83 | It should look like: 84 | ```javascript 85 | 86 | 87 | ``` 88 | 89 | ```javascript 90 | import React from 'react'; 91 | import LabelError from 'components/validation/labelErrorComponent'; 92 | 93 | class LoginFormComponent extends React.Component { 94 | render() { 95 | const {validate, onSubmit} = this.props; 96 | return ( 97 |
{ evt.preventDefault(); onSubmit.call(this, validate);} }> 98 |
99 | 100 | 101 |
102 |
103 | 104 | 105 |
106 |
107 | 108 |
109 |
110 |
111 | ); 112 | } 113 | } 114 | ``` 115 | 116 | 6. Create a model 117 | 118 | Anatomy of a model 119 | 120 | ```javascript 121 | const userModel = { 122 | name:'userModel', 123 | data: { 124 | 'url': { 125 | validate: { 126 | required: true, 127 | func: (value) => { 128 | return true; 129 | }, 130 | message: 'This is a test', 131 | }, 132 | }, 133 | }, 134 | } 135 | ``` 136 | 137 | 7 Using webpack? include jsx/es6 138 | ```javascript 139 | module: { 140 | loaders: [{ 141 | test:[/\.jsx$/, /\.js$/], 142 | loaders: ['react-hot', 'babel?stage=0&loose[]=es6.modules'], 143 | include: [ 144 | path.resolve(__dirname, "src"), 145 | path.resolve(__dirname, "node_modules/redux-form-validator") 146 | ], 147 | }, { 148 | test: [/\.scss$/, /\.css$/], 149 | loader: 'css?localIdentName=[path]!postcss-loader!sass', 150 | }], 151 | }, 152 | }; 153 | ``` 154 | 155 | 8 You're done. 156 | 157 | 158 | ## Using actions 159 | 160 | You can use validation actions to execute code depending if the form or input is valid. It's a good way to control side effects like calling an api action once the field if valid. 161 | 162 | ### Validate Sync Form 163 | ```javascript 164 | onSubmit: function(validateProps) { 165 | const inputs = this.refs; 166 | if (validateProps.form(form)) { 167 | // form is valid, redirect to nre page 168 | }else{ 169 | // form is not valid 170 | } 171 | } 172 | ``` 173 | ### Validate Async Form 174 | If you validate asyncly 1 input or form, you must use a promise instead of just using a bool. 175 | ```javascript 176 | onSubmit: function submit(validateProps) { 177 | const inputs = this.refs; 178 | validateProps.form(inputs).then(() =>{ 179 | console.log('form is valid'); 180 | }).catch(() => { 181 | console.log("form is not valid"); 182 | }); 183 | } 184 | ``` 185 | 186 | ### Validate Sync input 187 | 188 | ```javascript 189 | if(this.validate.input(value, field)){ 190 | // input is valid 191 | }else{ 192 | // input is not valid 193 | } 194 | ``` 195 | 196 | 197 | ### Validate Async input 198 | 199 | ```javascript 200 | this.validate.input(value, field).then(() => { 201 | // input is valid 202 | }) 203 | .catch(function(errorMessage) { 204 | // input is not valid 205 | }); 206 | ``` 207 | 208 | 209 | ## Validation model 210 | 211 | ### data 212 | 213 | A Model must have a data object that describe fields to validate. Under the validate object list all the validators you want to use. 214 | 215 | ### Global Validate functions 216 | 217 | The model can also have global validation functions that are executed once all inputs are valid. 218 | 219 | #### validate(form, dispatch) 220 | 221 | Used to do sync validations after all your inputs are valid. Must return true or false 222 | 223 | ```javascript 224 | const userModel = { 225 | name:'userModel', 226 | data: { 227 | 'url': { 228 | validate: { 229 | required: true, 230 | func: (value) => { 231 | return true; 232 | }, 233 | message: 'This is a test', 234 | }, 235 | }, 236 | }, 237 | validate: (form, dispatch) => { 238 | // form 239 | let validate = false; 240 | if (!form.url.value) { 241 | dispatch({ 242 | type: 'GROWLER__SHOW', 243 | growler: { 244 | text: 'Please enter your url', 245 | type: 'growler--error', 246 | }, 247 | }); 248 | validate = false; 249 | } 250 | 251 | return true; 252 | }, 253 | }; 254 | ``` 255 | 256 | ## Built-in validators 257 | 258 | ### func validator 259 | 260 | Lets you implement a custom function used for validation. 261 | 262 | ```js 263 | const userModel = { 264 | name:'userModel', 265 | data: { 266 | 'username': { 267 | validate: { 268 | required: true, 269 | pattern: 'email', 270 | async: function() { 271 | setTimeout( () => { 272 | this.resolve("yes"); 273 | }, 5000); 274 | }, 275 | }, 276 | }, 277 | ``` 278 | 279 | 280 | 281 | 282 | ### async validator 283 | 284 | Lets you implement a custom async function used for validation using a Promise. Must return this.resolve or this.reject. You can reject with a custom message passed as a string. 285 | 286 | ```js 287 | const userModel = { 288 | name:'userModel', 289 | data: { 290 | 'username': { 291 | validate: { 292 | required: true, 293 | pattern: 'email', 294 | async: function() { 295 | setTimeout( () => { 296 | this.reject("Sorry, this username is already used."); 297 | }, 5000); 298 | }, 299 | }, 300 | }, 301 | ``` 302 | 303 | ### required 304 | 305 | Validates if the attribute is required or not. 306 | This can be specified as either a boolean value or a function that returns a boolean value. 307 | 308 | ```js 309 | const userModel = { 310 | name:'userModel', 311 | data: { 312 | 'username': { 313 | validate: { 314 | required: true, 315 | pattern: 'email', 316 | }, 317 | }, 318 | }, 319 | }; 320 | ``` 321 | 322 | ### acceptance 323 | 324 | Validates that something has to be accepted, e.g. terms of use. `true` or 'true' are valid. 325 | 326 | ```js 327 | const userModel = { 328 | name:'userModel', 329 | data: { 330 | 'username': { 331 | validate: { 332 | required: true, 333 | acceptance: true 334 | } 335 | } 336 | }; 337 | ``` 338 | 339 | ### min 340 | 341 | Validates that the value has to be a number and equal to or more than the min value specified. 342 | 343 | ```js 344 | const userModel = { 345 | name:'userModel', 346 | data: { 347 | 'age': { 348 | validate: { 349 | min: 1, 350 | } 351 | } 352 | } 353 | }); 354 | ``` 355 | 356 | ### max 357 | 358 | Validates that the value has to be a number and equal to or less than the max value specified. 359 | 360 | ```js 361 | const userModel = { 362 | name:'userModel', 363 | data: { 364 | 'age': { 365 | validate: { 366 | max: 100, 367 | } 368 | } 369 | } 370 | }; 371 | ``` 372 | 373 | ### range 374 | 375 | Validates that the value has to be a number and equal to or between the two numbers specified. 376 | 377 | ```js 378 | const userModel = { 379 | name:'userModel', 380 | data: { 381 | 'age': { 382 | validate: { 383 | range: [1, 10], 384 | } 385 | } 386 | } 387 | }; 388 | ``` 389 | 390 | ### length 391 | 392 | Validates that the value has to be a string with length equal to the length value specified. 393 | 394 | ```js 395 | const userModel = { 396 | name:'userModel', 397 | data: { 398 | 'postalCode': { 399 | validate: { 400 | length: 4, 401 | } 402 | } 403 | } 404 | }; 405 | ``` 406 | 407 | ### minLength 408 | 409 | Validates that the value has to be a string with length equal to or greater than the min length value specified. 410 | 411 | ```js 412 | const userModel = { 413 | name:'userModel', 414 | data: { 415 | 'password': { 416 | validate: { 417 | minLength: 8 418 | } 419 | } 420 | } 421 | }; 422 | ``` 423 | 424 | 425 | ### maxLength 426 | 427 | Validates that the value has to be a string with length equal to or less than the max length value specified. 428 | 429 | ```js 430 | const userModel = { 431 | name:'userModel', 432 | data: { 433 | 'password': { 434 | validate: { 435 | maxLength: 100 436 | } 437 | } 438 | } 439 | }; 440 | ``` 441 | 442 | ### rangeLength 443 | 444 | Validates that the value has to be a string and equal to or between the two numbers specified. 445 | 446 | ```js 447 | const userModel = { 448 | name:'userModel', 449 | data: { 450 | 'password': { 451 | validate: { 452 | rangeLength: [6, 100] 453 | } 454 | } 455 | } 456 | }; 457 | ``` 458 | 459 | ### oneOf 460 | 461 | Validates that the value has to be equal to one of the elements in the specified array. Case sensitive matching. 462 | 463 | ```js 464 | const userModel = { 465 | name:'userModel', 466 | data: { 467 | 'country': { 468 | validate: { 469 | oneOf: ['Norway', 'Sweeden'] 470 | } 471 | } 472 | } 473 | }; 474 | ``` 475 | 476 | ### equalTo 477 | 478 | Validates that the value has to be equal to the value of the attribute with the name specified. 479 | 480 | ```js 481 | const userModel = { 482 | name:'userModel', 483 | data: { 484 | 'password': { 485 | validate: { 486 | equalTo: 'password' 487 | } 488 | } 489 | } 490 | }; 491 | ``` 492 | 493 | 494 | ### pattern 495 | 496 | Validates that the value has to match the pattern specified. Can be a regular expression or the name of one of the built in patterns. 497 | 498 | ```js 499 | const userModel = { 500 | name:'userModel', 501 | data: { 502 | 'email': { 503 | validate: { 504 | pattern: 'email' 505 | } 506 | } 507 | } 508 | }; 509 | ``` 510 | 511 | The built-in patterns are: 512 | 513 | * number - Matches any number (e.g. -100.000,00) 514 | * email - Matches a valid email address (e.g. mail@example.com) 515 | * url - Matches any valid url (e.g. http://www.example.com) 516 | * digits - Matches any digit(s) (i.e. 0-9) 517 | 518 | Specify any regular expression you like: 519 | 520 | ```js 521 | const userModel = { 522 | name:'userModel', 523 | data: { 524 | 'email': { 525 | validate: { 526 | pattern: /^sample/ 527 | } 528 | } 529 | } 530 | }; 531 | ``` 532 | 533 | ## Contributions 534 | 535 | There is plenty to do in the issue tracker, look at the 1.1 milestone 536 | 537 | 538 | ## Limitations 539 | 540 | This component is based on the use of redux, react, es6 & es7 (decorators) and webpack for loading the css as an import module. 541 | --------------------------------------------------------------------------------