Elegant and composable validations.
Revalidate was originally created as a helper library for composing and reusing
common validations to generate validate functions for
Redux Form. It became evident that the
validators that revalidate can generate are pretty agnostic about how they are
used. They are just functions that take a value and return an error message if
the value is invalid.
Table of Contents
Install
$ npm install revalidate
:tada: Integrations :tada:
-
react-revalidate
Validate React component props with revalidate validation functions.
-
redux-revalidate
Validate your Redux store state with revalidate validation functions.
-
Redux Form
Create validation functions for your form components out of the box. See the
example below.
Usage
Revalidate provides functions for creating validation functions as well as
composing and combining them. Think redux
for validation functions.
createValidator
The simplest function is createValidator
which creates a value validation
function. createValidator
takes two arguments. The first argument is a curried
function that takes an error message and the value. The curried function must
return the message if the value is invalid. If the field value is valid, it's
recommended that you return nothing, so a return value of undefined
implies
the field value was valid.
The second argument is a function that takes a field name and must return the
error message. Optionally, you can just pass in a string as the second argument
if you don't want to depend on the field name.
The returned validation function is also a curried function. The first argument
is a field name string or a configuration object where you can specify the field
or a custom error message. The second argument is the value. You can pass in
both arguments at the same time too. We'll see why currying the function can be
useful when we want to compose validators.
Here is an implementation of an isRequired
validator with createValidator
:
import { createValidator } from 'revalidate';
const isRequired = createValidator(
message => value => {
if (value == null || value === '') {
return message;
}
},
field => `${field} is required`
);
var createValidator = require('revalidate').createValidator;
var isRequired = createValidator(
function(message) {
return function(value) {
if (value == null || value === '') {
return message;
}
};
},
function(field) {
field + ' is required'
}
);
isRequired('My Field')();
isRequired('My Field')('');
isRequired('My Field')('42');
isRequired({ message: 'Error' })();
Validation functions can optionally accept a second parameter including all of
the current values. This allows comparing one value to another as part of
validation. For example:
import { createValidator } from 'revalidate';
var createValidator = require('revalidate').createValidator;
export default function matchesField(otherField, otherFieldLabel) {
return createValidator(
message => (value, allValues) => {
if (!allValues || value !== allValues[otherField]) {
return message;
}
},
field => `${field} must match ${otherFieldLabel}`
);
}
matchesField('password')('My Field')();
matchesField('password')('My Field')('yes', { password: 'no' });
matchesField('password')('My Field')('yes', { password: 'yes' });
matchesValue('password')({
message: 'Passwords must match',
})('yes', { password: 'no' });
composeValidators
Revalidate becomes really useful when you use the composeValidators
function.
As the name suggests, it allows you to compose validators into one. By default
the composed validator will check each validator and return the first error
message it encounters. Validators are checked in a left-to-right fashion to
make them more readable. (Note: this is opposite most functional
implementations of the compose function.)
The composed validator is also curried and takes the same arguments as an
individual validator made with createValidator
.
import {
createValidator,
composeValidators,
isRequired
} from 'revalidate';
var r = require('revalidate');
var createValidator = r.createValidator;
var composeValidators = r.composeValidators;
var isRequired = r.isRequired;
const isAlphabetic = createValidator(
message => value => {
if (value && !/^[A-Za-z]+$/.test(value)) {
return message;
}
},
field => `${field} must be alphabetic`
);
const validator = composeValidators(
isRequired,
isAlphabetic({
message: 'Can only contain letters'
})
)('My Field');
validator();
validator('123');
validator('abc');
Multiple Errors as an Array
You can supply an additional multiple: true
option to return all errors as an
array from your composed validators. This will run all composed validations
instead of stopping at the first one that fails.
import { createValidator, composeValidators } from 'revalidate';
var r = require('revalidate');
var createValidator = r.createValidator;
var composeValidators = r.composeValidators;
const startsWithA = createValidator(
message => value => {
if (value && !/^A/.test(value)) {
return message;
}
},
field => `${field} must start with A`
);
const endsWithC = createValidator(
message => value => {
if (value && !/C$/.test(value)) {
return message;
}
},
field => `${field} must end with C`
);
const validator = composeValidators(
startsWithA,
endsWithC
)({ field: 'My Field', multiple: true });
validator('BBB');
Multiple Errors as an Object
Alternatively, if you want to be able to reference specific errors, you can
return multiple errors as an object, thereby allowing you to name the errors. To
return multiple errors as an object, pass in your validators as an object to
composeValidators
instead of a variadic number of arguments. The keys you use
in your object will be the keys in the returned errors object. Don't forget to
still supply the multiple: true
option!
const validator = composeValidators({
A: startsWithA,
C: endsWithC
})({ field: 'My Field', multiple: true });
validator('BBB');
combineValidators
combineValidators
is analogous to a function like combineReducers
from
redux. It allows you to validate multiple field values at once. It returns a
function that takes an object with field names mapped to their values.
combineValidators
will run named validators you supplied it with their
respective field values and return an object literal containing any error
messages for each field value. An empty object return value implies no field
values were invalid.
import {
createValidator,
composeValidators,
combineValidators,
isRequired,
isAlphabetic,
isNumeric
} from 'revalidate';
var r = require('revalidate');
var createValidator = r.createValidator;
var composeValidators = r.composeValidators;
var combineValidators = r.combineValidators;
var isRequired = r.isRequired;
var isAlphabetic = r.isAlphabetic;
var isNumeric = r.isNumeric;
const dogValidator = combineValidators({
name: composeValidators(
isRequired,
isAlphabetic
)('Name'),
age: isNumeric('Age')
});
dogValidator({});
dogValidator({ name: '123', age: 'abc' });
dogValidator({ name: 'Tucker', age: '10' });
Nested Fields
combineValidators
also works with deeply nested fields in objects and arrays.
To specify nested fields, just supply the path to the field with dots:
'contact.firstName'
.
For arrays of values you can use brace syntax: 'phones[]'
.
For nested fields of objects in arrays you can combine dots and braces:
'cars[].make'
.
You can combine and traverse as deep as you want:
'deeply.nested.list[].of.cats[].name'
!
import {
composeValidators,
combineValidators,
isRequired,
isAlphabetic,
isNumeric,
isOneOf,
matchesField,
} from 'revalidate';
var r = require('revalidate');
var composeValidators = r.composeValidators;
var combineValidators = r.combineValidators;
var isRequired = r.isRequired;
var isAlphabetic = r.isAlphabetic;
var isNumeric = r.isNumeric;
var isOneOf = r.isOneOf;
var matchesField = r.matchesField;
const validate = combineValidators({
'favoriteMeme': isAlphabetic('Favorite Meme'),
'contact.name': composeValidators(
isRequired,
isAlphabetic
)('Contact Name'),
'contact.age': isNumeric('Contact Age'),
'phones[]': isNumeric('Phone'),
'cars[].make': composeValidators(
isRequired,
isOneOf(['Honda', 'Toyota', 'Ford'])
)('Car Make'),
'otherContact.name': matchesField(
'contact.name',
'Contact Name'
)('Other Name'),
});
validate({});
validate({
contact: { name: 'Joe', age: 'thirty' },
phones: ['abc', '123'],
cars: [{ make: 'Toyota' }, {}],
otherContact: { name: 'Jeremy' },
});
Redux Form
As mentioned, even though revalidate is pretty agnostic about how you use it, it
does work out of the box for Redux Form. The validate
function you might write
for a Redux Form example like
here
can also be automatically generated with combineValidators
. The function it
returns will work perfectly for the validate
option for your form components
for React and Redux Form.
Here is that example from Redux Form rewritten to generate a validate
function
with revalidate.
import React, {Component, PropTypes} from 'react';
import {reduxForm} from 'redux-form';
import {
createValidator,
composeValidators,
combineValidators,
isRequired,
hasLengthLessThan,
isNumeric
} from 'revalidate';
export const fields = ['username', 'email', 'age'];
const isValidEmail = createValidator(
message => value => {
if (value && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)) {
return message;
}
},
'Invalid email address'
);
const isGreaterThan = (n) => createValidator(
message => value => {
if (value && Number(value) <= n) {
return message;
}
},
field => `${field} must be greater than ${n}`
);
const customIsRequired = isRequired({ message: 'Required' });
const validate = combineValidators({
username: composeValidators(
customIsRequired,
hasLengthLessThan(16)({
message: 'Must be 15 characters or less'
})
)(),
email: composeValidators(
customIsRequired,
isValidEmail
)(),
age: composeValidators(
customIsRequired,
isNumeric({
message: 'Must be a number'
}),
isGreaterThan(17)({
message: 'Sorry, you must be at least 18 years old'
})
)()
});
class SynchronousValidationForm extends Component {
static propTypes = {
fields: PropTypes.object.isRequired,
handleSubmit: PropTypes.func.isRequired,
resetForm: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired
};
render() {
const {fields: {username, email, age}, resetForm, handleSubmit, submitting} = this.props;
return (<form onSubmit={handleSubmit}>
<div>
<label>Username</label>
<div>
<input type="text" placeholder="Username" {...username}/>
</div>
{username.touched && username.error && <div>{username.error}</div>}
</div>
<div>
<label>Email</label>
<div>
<input type="text" placeholder="Email" {...email}/>
</div>
{email.touched && email.error && <div>{email.error}</div>}
</div>
<div>
<label>Age</label>
<div>
<input type="text" placeholder="Age" {...age}/>
</div>
{age.touched && age.error && <div>{age.error}</div>}
</div>
<div>
<button type="submit" disabled={submitting}>
{submitting ? <i/> : <i/>} Submit
</button>
<button type="button" disabled={submitting} onClick={resetForm}>
Clear Values
</button>
</div>
</form>
);
}
}
export default reduxForm({
form: 'synchronousValidation',
fields,
validate
})(SynchronousValidationForm);
Common Validators
Revalidate exports some common validations for your convenience. If you need
something more complex, then you'll need to create your own validators with
createValidator
.
isRequired
isRequired
is pretty self explanatory. It determines that a value isn't valid
if it's null
, undefined
or the empty string ''
.
isRequired('My Field')();
isRequired('My Field')(null);
isRequired('My Field')('');
isRequired('My Field')('42');
hasLengthBetween
hasLengthBetween
tests that the value falls between a min and max inclusively.
It wraps a call to createValidator
, so you must first call it with the min and
max arguments.
hasLengthBetween(1, 3)('My Field')('hello');
hasLengthGreaterThan
hasLengthGreaterThan
tests that the value is greater than a predefined length.
It wraps a call to createValidator
, so you must first call it with the
min length.
hasLengthGreaterThan(3)('My Field')('foo');
hasLengthLessThan
hasLengthLessThan
tests that the value is less than a predefined length. It
wraps a call to createValidator
, so you must first call it with the max
length.
hasLengthLessThan(4)('My Field')('hello');
isAlphabetic
isAlphabetic
simply tests that the value only contains any of the 26 letters
in the English alphabet.
isAlphabetic('My Field')('1');
isAlphaNumeric
isAlphaNumeric
simply tests that the value only contains any of the 26 letters
in the English alphabet or any numeric digit (i.e. 0-9).
isAlphaNumeric('My Field')('!@#$');
isNumeric
isNumeric
simply tests that the string is comprised of only digits (i.e.
0-9).
isNumeric('My Field')('a');
isOneOf
isOneOf
tests that the value is contained in a predefined array of values. It
wraps a call to createValidator
, so you must first call it with the array of
allowed values.
isOneOf(['foo', 'bar'])('My Field')('baz');
isOneOf(['foo', 'bar'])('My Field')('FOO');
By default it does a sameness equality (i.e. ===
) with case sensitivity
for determining if a value is valid. You can supply an optional second argument
function to define how values should be compared. The comparer function takes
the field value as the first argument and each valid value as the second
argument. You could use this to make values case insensitive. Returning a truthy
value in a comparison means that the field value is valid.
const validator = isOneOf(
['foo', 'bar'],
(value, validValue) => (
value && value.toLowerCase() === validValue.toLowerCase()
)
);
validator('My Field')('FOO');
matchesField
matchesField
checks that a field matches another field's value. This is
perfect for password confirmation fields.
matchesField
takes the name of the other field as the first argument and an
optional second argument for the other field's label. The returned functions are
like the other validation functions.
matchesField(
'password',
'Password'
)('Password Confirmation')('yes', { password: 'no' });
matchesField('password')('Password Confirmation')('yes', { password: 'yes' });
With combineValidators
:
import {
combineValidators,
isRequired,
matchesField,
} from 'revalidate';
var r = require('revalidate');
var combineValidators = r.combineValidators;
var isRequired = r.isRequired;
var matchesField = r.matchesField;
const validate = combineValidators({
password: isRequired('Password'),
confirmPassword: matchesField('password')({
message: 'Passwords do not match',
}),
});
validate({
password: 'helloworld',
confirmPassword: 'helloworld',
});
validate({
password: 'helloworld',
confirmPassword: 'holamundo',
});
isRequiredIf
isRequiredIf
allows you to conditionally require a value based on the result
of a predicate function. As long as your predicate function returns a truthy
value, the field value will be required.
This is perfect if you want to require a field if another field value is
present:
const validator = combineValidators({
username: isRequiredIf(
values => values && !values.useEmailAsUsername
)('Username'),
});
validator();
validator({
useEmailAsUsername: false,
});
validator({
username: 'jfairbank',
useEmailAsUsername: false,
});
validator({
useEmailAsUsername: true,
});
If you compose isRequiredIf
with composeValidators
, your other validations
will still run even if your field isn't required:
const validator = combineValidators({
username: composeValidators(
isRequiredIf(values => values && !values.useEmailAsUsername),
isAlphabetic
)('Username'),
});
validator({
username: '123',
useEmailAsUsername: false,
});
validator({
username: '123',
useEmailAsUsername: true,
});
matchesPattern
matchesPattern
is a general purpose validator for validating values against
arbitrary regex patterns.
const isAlphabetic = matchesPattern(/^[A-Za-z]+$/)('Username');
isAlphabetic('abc');
isAlphabetic('123');
Note: matchesPattern
does not require a value, so falsy values will pass.
isAlphabetic();
isAlphabetic(null);
isAlphabetic('');
Test Helpers
Revalidate includes some test helpers to make testing your validation functions
easier. You can import the helpers from revalidate/assertions
. All helpers
return booleans.
hasError
Use hasError
to assert that a validation result has at least one error. Negate
to assert there are no errors. The only argument is the validation result from
your validate function.
import { hasError } from 'revalidate/assertions';
var hasError = require('revalidate/assertions').hasError;
const validateName = isRequired('Name');
hasError(validateName(''));
hasError(validateName('Tucker'));
const validateAge = composeValidators(
isRequired,
isNumeric
)('Age');
hasError(validateAge(''));
hasError(validateAge('abc'));
hasError(validateAge('10'));
const validateAge = composeValidators(
isRequired,
isNumeric,
hasLengthLessThan(3)
)('Age');
hasError(validateAge(''));
hasError(validateAge('abc'));
hasError(validateAge('100'));
hasError(validateAge('one hundred'));
hasError(validateAge('10'));
const validateDog = combineValidators({
'name:' isRequired('Name'),
'age:' composeValidators(
isRequired,
isNumeric
)('Age'),
'favorite.meme': isRequired('Favorite Meme'),
});
hasError(validateDog({
age: '10',
favorite: { meme: 'Doge' },
}));
hasError(validateDog({
name: 'Tucker',
age: 'abc',
favorite: { meme: 'Doge' },
}));
hasError(validateDog({
favorite: { meme: 'Doge' },
}));
hasError(validateDog({
name: 'Tucker',
age: '10',
}));
hasError(validateDog({
name: 'Tucker',
age: '10',
favorite: { meme: 'Doge' },
}));
hasErrorAt
Use hasErrorAt
with combined validators to assert a specific field has an
error. It takes two arguments, the validation result and the field key to check.
(Note: hasErrorAt
only works with validators created from
combineValidators
.)
import { hasErrorAt } from 'revalidate/assertions';
var hasErrorAt = require('revalidate/assertions').hasErrorAt;
const result = validateDog({
age: '10',
favorite: { meme: 'Doge' },
});
hasErrorAt(result, 'name');
hasErrorAt(result, 'age');
hasErrorAt(result, 'favorite.meme');
const result = validateDog({
name: 'Tucker',
age: 'abc',
favorite: { meme: 'Doge' },
});
hasErrorAt(result, 'name');
hasErrorAt(result, 'age');
hasErrorAt(result, 'favorite.meme');
const result = validateDog({
favorite: { meme: 'Doge' },
});
hasErrorAt(result, 'name');
hasErrorAt(result, 'age');
hasErrorAt(result, 'favorite.meme');
const result = validateDog({
name: 'Tucker',
age: '10',
});
hasErrorAt(result, 'name');
hasErrorAt(result, 'age');
hasErrorAt(result, 'favorite.meme');
const result = validateDog({
name: 'Tucker',
age: '10',
favorite: { meme: 'Doge' },
});
hasErrorAt(result, 'name');
hasErrorAt(result, 'age');
hasErrorAt(result, 'favorite.meme');
hasErrorOnlyAt
Use hasErrorOnlyAt
with combined validators to assert a specific field is the
ONLY error in the validation result. It takes two arguments, the validation
result and the field key to check. (Note: hasErrorOnlyAt
only works with
validators created from combineValidators
.)
import { hasErrorOnlyAt } from 'revalidate/assertions';
var hasErrorOnlyAt = require('revalidate/assertions').hasErrorOnlyAt;
const result = validateDog({
age: '10',
favorite: { meme: 'Doge' },
});
hasErrorOnlyAt(result, 'name');
hasErrorOnlyAt(result, 'age');
hasErrorOnlyAt(result, 'favorite.meme');
const result = validateDog({
name: 'Tucker',
age: 'abc',
favorite: { meme: 'Doge' },
});
hasErrorOnlyAt(result, 'name');
hasErrorOnlyAt(result, 'age');
hasErrorOnlyAt(result, 'favorite.meme');
const result = validateDog({
favorite: { meme: 'Doge' },
});
hasErrorOnlyAt(result, 'name');
hasErrorOnlyAt(result, 'age');
hasErrorOnlyAt(result, 'favorite.meme');
const result = validateDog({
name: 'Tucker',
age: '10',
});
hasErrorOnlyAt(result, 'name');
hasErrorOnlyAt(result, 'age');
hasErrorOnlyAt(result, 'favorite.meme');
const result = validateDog({
name: 'Tucker',
age: '10',
favorite: { meme: 'Doge' },
});
hasErrorOnlyAt(result, 'name');
hasErrorOnlyAt(result, 'age');
hasErrorOnlyAt(result, 'favorite.meme');