revalidate
Composable form value validations for JavaScript.
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.
Install
$ npm install revalidate
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`
);
isRequired('My Field')();
isRequired('My Field')('');
isRequired('My Field')('42');
isRequired({ message: 'Error' })();
composeValidators
composeValidators
is the function that makes revalidate start to be really
useful. As the name suggests, it allows you to compose validators into one. 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';
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');
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';
const dogValidator = combineValidators({
name: composeValidators(
isRequired,
isAlphabetic
)('Name'),
age: isNumeric('Age')
});
dogValidator({});
dogValidator({ name: '123', age: 'abc' });
dogValidator({ name: 'Tucker', age: '10' });
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(values.email)) {
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 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');