Last active
February 17, 2017 08:43
-
-
Save stewartduffy/37bbf2c0a2a0d2f8e4f73a9dc193ee8b to your computer and use it in GitHub Desktop.
Redux Form Validation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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 contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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 contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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 contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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