Repository navigation
Configure Form Validation
Now that we have a fully operational CRUD, let's explore one of the features that redux-form makes it fairly
straightforward to implement: form validation.
It's really easy to perform field-level and form-level validation with redux-form: you just supply a function
that returns either an error message or undefined and redux-form evaluates it for you. Let's put it to practice
now.
As mentioned in earlier lessons, the Field component can take into its component prop either a string (that
has to be input, select or textarea) or a React component, be it either a class-based component or a stateless
function-based component. When we pass a React component to Field, it injects various props to that component
that's pertinent to it, such as input type, whether it has been touched (focused) before, if it has any error
or warning messages, and many more metadata on the field.
The TextInput component we're going to create is a stateless component, therefore we can declare it as a function
and create it inside the components directory.
nano src/components/TextInput.jsimport React from 'react';
const TextInput = ({ input, label, type, placeholder, meta: { touched, error, warning } }) => (
<fieldset
className={`form-group ${
touched && (
error ? 'has-error' : warning ? 'has-warning' : 'has-success'
)
}`}
>
<label htmlFor={input.name} className="control-label">{label}</label>
<input {...input} placeholder={placeholder} type={type} className="form-control" />
{
(() => (
touched && (
(error &&
<span className="help-block">
<i className="glyphicon glyphicon-remove-sign"></i> {error}
</span>
) || (warning &&
<span className="help-block">
<i className="glyphicon glyphicon-exclamation-sign"></i> {warning}
</span>
)
)
))()
}
</fieldset>
);
export default TextInput;This component will replace the entirety of the fieldset blocks in ContactForm and will change colors according
to the validation error level: if it's a validation error, it will turn red and an error message will appear below
the field; if it's a warning, it will turn yellow and a similar message will appear as well. If the form has any
validation errors, it won't allow you to submit it.
The new ContactForm will look more or less like this:
nano src/containers/ContactForm.js...
import TextInput from '../components/TextInput';
...
render() {
const { handleSubmit, pristine, submitting, reset } = this.props;
return (
<form className="form-horizontal" onSubmit={handleSubmit}>
<Field name="id" type="hidden" component="input" />
<Field
name="name"
component={TextInput}
type="text"
label="Name"
placeholder="e.g. John Doe"
/>
<Field
name="phone"
component={TextInput}
type="text"
label="Phone"
placeholder="(111) 111-1111"
/>
<Field
name="email"
component={TextInput}
type="email"
label="Email"
placeholder="john.doe@example.com"
/>
<fieldset className="form-group">
<button type="submit" disabled={pristine || submitting} className="btn btn-primary">Submit</button>
<button type="button" disabled={pristine || submitting} className="btn btn-default" onClick={reset}>Reset</button>
<Link to="/"><i className="glyphicon glyphicon-chevron-left"></i> Back to Home Page</Link>
</fieldset>
</form>
)
}Save, refresh your app and test it. It should be working the same as it did previously.
Except the component we created is capable of displaying the errors to the user and actively prevent them from submitting wrongly formatted data to the server.
The validation rules will be as following:
- Name field
- Must not be empty
- Must not have more than 25 characters (more than enough for a full name)
- Phone field
- Must not be empty
- Must match standard US phone number format (e.g. +1 (area code) prefix-suffix)
- Must be unique
- Email field
- Must not be empty
Most of the validation we're going to do on this form is synchronous, meaning we won't need the back-end to do
it for us. Validation rules are functions that return a string containing a message if it has errors, or
undefined otherwise. Let's apply the aforementioned rules to our form.
nano src/containers/ContactForm.js...
class ContactForm extends Component {
required(value) {
return value && value.length > 0 ? undefined : 'This field is required.';
}
maxLength(max) {
return value => value && value.length <= max ? undefined : `Must have ${max} characters or less.`;
}
phoneFormat(value) {
return value && /^\+1 \([0-9]{3}\) [0-9]{3}-[0-9]{4}$/.test(value) ? undefined : 'Must be in US format, e.g. +1 (111) 111-1111';
}
...
<Field
name="name"
component={TextInput}
type="text"
label="Name"
placeholder="e.g. John Doe"
validate={[
this.required,
(() => this.maxLength(25))() // maxLength returns the actual validation function,
// so we need to actually call it here instead of just passing it
]}
/>
<Field
name="phone"
component={TextInput}
type="text"
label="Phone"
placeholder="+1 (111) 111-1111"
validate={[
this.required,
this.phoneFormat
]}
/>
<Field
name="email"
component={TextInput}
type="email"
label="Email"
placeholder="john.doe@example.com"
validate={this.required}
/>
...
}Save and refresh the app. You'll notice that, if you fill a field with invalid values, the field will be highlighted red and you won't be able to submit the form.
There's one rule that we have specified that the phone number must be unique across the database. But how should we know that the phone number is unique if there's no other number to compare to? This is one of the cases when we need to check the server for validation while the form is being filled out.
Firstly, we will need to Base64-encode the phone before sending it to the server. We can do it with the btoa
function that's already available in the browser. btoa converts the string to Base64 whilst atob de-converts
it back. We also need to take care of, when the user is editing a contact, they must be able to submit the form
while not changing the phone number, so the query must exclude the ID of the contact being edited. We will also
need to adjust the back-end server to deal with such filtering.
First off, we will refactor the GET /contacts endpoint on server.js to parse a query string and pass it as
a filter clause to MongoDB. Also, since Node.js doesn't have atob and btoa functions natively, we'll need to
install them manually. Fortunately, we only need atob here.
yarn add atob
nano server.js...
const atob = require('atob');
...
app.get('/contacts', (req, res) => {
const { query } = req;
let filters = {};
if (Object.keys(query).length > 0) {
filters = {
phone: atob(query.phone)
};
if (query.id !== 'undefined' && query.id.length > 0) {
Object.assign(filters, {
_id: {
'$ne': new ObjectID(query.id)
}
});
}
}
db.collection(COLLECTION).find(filters).toArray((error, docs) => {
...
});
});
...We're retrieving the query string from the req object as an object itself and creating a filters object
that will contain the query we'll pass to MongoDB. If the query string isn't empty, we'll pass in the decoded
phone string and, if there's an id and it isn't undefined or empty (meaning we're editing a contact), we
must eliminate it from the results, else we would just find the same contact we're editing. Then we pass the
filters object to MongoDB and retrieve the results.
To implement async validation, instead of assigning it directly to the phone field like we did with the other
validation rules, we need to declare it outside of the ContactForm class as a const (just like we usually
do with Redux prop mapping functions) and pass it to the reduxForm call.
nano src/containers/ContactForm.js...
const phoneUnique = values => {
const { phone, id } = values;
return fetch(`/contacts?phone=${btoa(phone)}&id=${id}`)
.then(response => response.json())
.then(json => {
if (json.length > 0) {
// eslint-disable-next-line
throw { phone: 'This phone number already exists.' };
}
});
}
export default reduxForm({
form: 'contact', // This is the form's name and must be unique across the app
asyncValidate: phoneUnique,
asyncBlurFields: ['phone'],
enableReinitialize: true
})(ContactForm);Note that redux-form will only apply async validators to fields once they pass the sync validation rules.
Save, reload and test the app.
git add .
git commit -m "Add sync and async validation rules to ContactForm"Next step: Deploy to Heroku