Skip to content

Instantly share code, notes, and snippets.

@stewartduffy
Last active February 17, 2017 08:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stewartduffy/37bbf2c0a2a0d2f8e4f73a9dc193ee8b to your computer and use it in GitHub Desktop.
Save stewartduffy/37bbf2c0a2a0d2f8e4f73a9dc193ee8b to your computer and use it in GitHub Desktop.
Redux Form Validation
/*
* Simple example of how to use the validators in a Redux form
* This file is pseudo code
*/
// Other imports would go above here ^^
import {validators, validatedForm} from 'common/validation' // import validators & validatedForm
import {details} from './actionMaps'
const Details = ({data, fields, handleSubmit, resetForm, close, dirty}, {trans, permissions}) => (
// Form JSX goes here.....
)
const fields = { // Define fields in form
login: validators().login(), // Login validator is a compose predefined helper validator
firstName: validators().required().max(255), // Chain as many validators as needed
lastName: validators().required().max(255, 'LastNameFieldIsToLong'), // You can pass in a custom validation message like this
email: validators().email(),
newPassword: validators().password('changePassword'),
confirmPassword: validators().confirmPassword('newPassword', 'changePassword'),
changePassword: true, // No validation on this field
activated: true,
roles: true,
groups: true
}
function mapStateToProps(state, {data}) { // Note we validatedForm() instead of reduxForm()
// state to props here...
}
const DetailsForm = validatedForm({
form: 'adminUserDetails',
fields // Pass fields object
}, mapStateToProps)(Details)
export default details.connect()(DetailsForm)
/*
* This file is a real example of how to use the validators in a redux form
* <ValidationGroup> is basically a react component that returns a <label />, <input /> & <span>[VALIDATION MESSAGE HERE]
*/
import React from 'react'
import {Button, ButtonToolbar, Legend, Checkbox} from 'common/components'
import {ValidationGroup, validators, validatedForm} from 'common/validation'
import {details} from './actionMaps'
import {CloseButton} from '../common'
const Details = ({data, fields, handleSubmit, resetForm, close, dirty}, {trans, permissions}) => (
<form noValidate onSubmit={handleSubmit} className="ws-admin-details-form">
<fieldset className="ws-fieldset-form-wrapper">
<Legend title={trans(data.id ? 'EditUser' : 'AddUser')} className="ws-form-main-legend">
<CloseButton onClick={close} />
</Legend>
<div className="ws-form-fields-wrapper">
<fieldset className="ws-fieldset-fields-wrapper">
<div className="ws-form-group-column">
<ValidationGroup
label={trans('Logon')}
componentClass="input"
placeholder={trans('LogonPlaceholder')}
{...fields.login}
autoFocus
/>
<ValidationGroup
label={trans('PreferredName')}
componentClass="input"
placeholder={trans('PreferredNamePlaceholder')}
{...fields.preferredName}
/>
</div>
<div className="ws-form-group-column">
<ValidationGroup
label={trans('Email')}
componentClass="input"
type="email"
placeholder={trans('EmailPlaceholder')}
className="ws-email"
{...fields.email}
/>
<div className="form-group">
<Checkbox {...fields.activated}>{trans('Activated')}</Checkbox>
</div>
</div>
</fieldset>
<fieldset className="ws-fieldset-fields-wrapper">
<legend>{trans('ChangePassword')}</legend>
<div className="ws-form-group-column">
<ValidationGroup
label={trans('NewPassword')}
componentClass="input"
type="password"
placeholder={trans('NewPasswordPlaceholder')}
{...fields.newPassword}
/>
</div>
<div className="ws-form-group-column">
<ValidationGroup
label={trans('ConfirmPassword')}
componentClass="input"
type="password"
placeholder={trans('ConfirmPasswordPlaceholder')}
{...fields.confirmPassword}
/>
</div>
</fieldset>
</div>
<ButtonToolbar>
<div className="pull-left">
<Button className="ws-btn-default" type="button" onClick={close}>{trans('Cancel')}</Button>
</div>
<div className="pull-right">
{dirty && data.id ? <Button className="ws-btn-default" type="button" onClick={resetForm}>{trans('Reset')}</Button> : null}
<Button className="ws-btn-primary" type="submit">{trans(data.id ? 'Update' : 'Create')}</Button>
</div>
</ButtonToolbar>
</fieldset>
</form>
)
Details.contextTypes = {
trans: React.PropTypes.func
}
Details.propTypes = {
data: React.PropTypes.object,
fields: React.PropTypes.object,
handleSubmit: React.PropTypes.func,
resetForm: React.PropTypes.func,
close: React.PropTypes.func,
dirty: React.PropTypes.bool
}
const fields = {
login: validators().login(),
preferredName: validators().required().max(255),
email: validators().email(),
newPassword: validators().password('changePassword'),
confirmPassword: validators().confirmPassword('newPassword', 'changePassword'),
changePassword: true,
activated: true,
roles: true,
groups: true
}
function mapStateToProps(state, {data}) {
return {
initialValues: {
preferredName: data.preferredName,
login: data.login,
email: data.email,
newPassword: '',
confirmPassword: '',
changePassword: false,
activated: data.activated || false,
roles: data.roles || [],
groups: data.groups || []
}
}
}
const DetailsForm = validatedForm({
form: 'adminUserDetails',
fields
}, mapStateToProps)(Details)
export default details.connect()(DetailsForm)
/*
* This file returns a HOC (higher order component) that returns a reduxForm to wrap your form component in.
* But it also includes the validators so we can implement validation in our redux form.
*/
import * as loadash from 'lodash'
import {reduxForm} from 'redux-form'
let enableValidation = true
function validator(values, fields) {
let errors = {}
if (enableValidation) {
loadash.forIn(fields, (fieldValue, fieldKey) => {
fieldValue.validators && fieldValue.validators.forEach(validator => {
if (validator(values[fieldKey], values)) {
errors[fieldKey] = validator(values[fieldKey], values)
}
})
})
}
return errors
}
export function disableValidation() {
enableValidation = false
}
export function validatedForm(options, ...rest) {
const validate = values => (validator(values, options.fields))
const fields = Object.keys(options.fields)
return reduxForm({...options, fields, validate}, ...rest)
}
/*
* This file is the validators builder function. Built in a way that easy to extend & add rules.
* Thes validators can be chained like this:
*
* const fields = {
* login: validators().required().alphaNumeric(),
* firstName: validators().required().min(2).max(100)
* }
*
*/
import {regex} from 'common/utils'
const validators = function() {
return {
/**
* Array of validator functions to run against fields in validatedForm.
* This is populated when validators() is called.
*
* @example
* const fields = {
* login: validators().required().alphaNumeric(),
* firstName: validators().required()
* }
*/
validators: [],
/**
* Adds a required validator to field
*
* @param {string} conditionalFieldKey - name of another field in form, if provided then this field is conditionally required if conditionalFieldKey is truthy.
* @param {string} conditionalFieldValue - to be used with conditionalFieldKey if you want this to be required based on another field's value.
* @param {string} msg - custom error message
*/
required(conditionalFieldKey, conditionalFieldValue, msg = 'Required') {
this.validators.push((value, values) => {
if (conditionalFieldValue) {
return (conditionalFieldKey && values[conditionalFieldKey] === conditionalFieldValue && !value || !conditionalFieldKey && !value ? msg : null)
} else {
return (conditionalFieldKey && values[conditionalFieldKey] && !value || !conditionalFieldKey && !value ? msg : null)
}
})
return this
},
/**
* Adds a minimum character length validator to field
*
* @param {number} minLength - field must not be less than minLength
* @param {string} msg - custom error message
*/
min(minLength, msg) {
const defaultError = {message: ({glTrans}) => (glTrans('LengthMustBeAtLeast', {minLength}))}
const errorMessage = msg || defaultError
this.validators.push((value, values) => (value && value.length < minLength ? errorMessage : null))
return this
},
/**
* Adds a maximum character length validator to field
*
* @param {number} maxLength - field must not be greater than maxLength
* @param {string} msg - custom error message
*/
max(maxLength = 254, msg) {
const defaultError = {message: ({glTrans}) => (glTrans('LengthCannotExceed', {maxLength}))}
const errorMessage = msg || defaultError
this.validators.push((value, values) => (value && value.length > maxLength ? errorMessage : null))
return this
},
/**
* Adds an alphaNumeric validator to field.
* Runs a regex test, checking that value contains only letters & numbers with no spaces or special characters.
*
* @param {string} msg - custom error message
*/
alphaNumeric(msg = 'ThisMustBeAlphanumeric') {
this.validators.push((value, values) => (value && !regex.alphaNumeric.test(value) ? msg : null))
return this
},
/**
* Adds a validator based on custom regular expression to field.
*
* @param {RegExp} pattern - regular expression to test field value
* @param {string} msg - custom error message
*/
pattern(pattern, msg = 'InvalidPattern') {
this.validators.push((value, values) => (value && pattern.test(value) ? msg : null))
return this
},
/**
* Adds a validator that checks if this field is the same as another field
*
* @param {string} anotherField - name of another field in form that this this field must match.
* @param {string} msg - custom error message
*/
matches(anotherField, msg = 'FieldsDoNotMatch') {
this.validators.push((value, values) => (value && value !== values[anotherField] ? msg : null))
return this
},
/**
* Adds an custom validator.
* Runs a function test, if predicate returns true then error is displayed, else no action
*
* @param {string} msg - custom error message
*/
custom(predicate, msg = 'FieldValueIncorrect') {
this.validators.push((value, values) => (value && predicate(value) ? msg : null))
return this
},
/**
* Adds a email validator to field
*
* @param {string} msg - custom error message
*/
email(msg = 'InvalidEmailAddress') {
this.required(null, null, msg)
this.min(5)
this.max(255)
return this
},
/**
* Adds a login validator to field
* This validator is composed of required, alphaNumeric, max
*
*/
login() {
this.required()
this.alphaNumeric()
this.max(100)
return this
},
/**
* Adds a password validator to field
* This validator is composed of required, min, max.
*
* @param {string} conditionalFieldKey - conditionalFieldKey to pass to required validator.
*/
password(conditionalFieldKey) {
const minLength = 6
const maxLength = 254
const minError = {message: ({glTrans}) => (glTrans('PasswordTooShort', {minLength}))}
const maxError = {message: ({glTrans}) => (glTrans('PasswordTooLong', {maxLength}))}
this.required(conditionalFieldKey)
this.min(minLength, minError)
this.max(maxLength, maxError)
return this
},
/**
* Adds a confirm password validator to field
* This validator is composed of required & matches.
*
* @param {string} newPasswordField - name of the 'newPassword' field in form that this this field must match
* @param {string} conditionalFieldKey - conditionalFieldKey to pass to required validator.
*/
confirmPassword(newPasswordField, conditionalFieldKey) {
this.required(conditionalFieldKey)
this.matches(newPasswordField, 'PasswordsDoNotMatch')
return this
}
}
}
export default validators
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment