Validation
Validation always starts with the screen you are designing. Most forms map to a data schema, which can have a list of very complicated convoluted rules.
While it is important to have validation to make sure data is correct, we should try and clean up data before we try and validate it.
Some teams implement validation requiring only A to Z and 4-10 characters, which can unintentionally reject common names.
Falsehoods Programmers Believe About Names has an excellent list of issues that can occur.
Breakdown
We’ll need to do the following steps to have a validation route
- The Screen
- Define your validation
- POST route for processing the data
- Catch and throw an error
- Error summary
- Error on the field
The Screen
For this, we will pick something simple the first radio example - this has been adapted slightly to make a prototype page.
{% extends "layouts/main.html" %}
{# Title of the page #}{% block pageTitle %}Where do you live - GOV.UK{% endblock %}
{% block content %} <div class="govuk-grid-row"> <div class="govuk-grid-column-two-thirds"> {# form to POST on the GET route #} <form method="post" action="#"> {# example of govuk radio button #} {{ govukRadios({ name: "whereDoYouLive", fieldset: { legend: { text: "Where do you live?", isPageHeading: true, classes: "govuk-fieldset__legend--l" } }, items: [ { value: "england", text: "England" }, { value: "scotland", text: "Scotland" }, { value: "wales", text: "Wales" }, { value: "northern-ireland", text: "Northern Ireland" } ] }) }} {# button to submit the form #} {{ govukButton({ text: "Save and continue" }) }} </form>{% endblock %}
You can access this using the url /where-do-you-live
.
Define your validation
For a radio button, we require at least one answer with the content “Select the country where you live”.
POST route
The prototype already has a GET route based on the file name because we have a POST form in the template, we can now add a POST route to our routes.js file.
// place this in your routes.js filerouter.post('/where-do-you-live', (req, res) => { // log to see what has been posted to the server console.log(req.body); // redirect to the next page res.redirect("/next-page");});
If we run this file and select nothing the log will be an empty object, if we run it with something selected.
Catch and throw an error
Similar to when we do routing - we now need to make a catch for the error validation.
If the error happened, we need to render out page in an error state, if not we can continue with the redirect.
// place this in your routes.js filerouter.post('/where-do-you-live', (req, res) => { // uses optional chaining to check if whereDoYouLive excisit // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining if(req.body?.whereDoYouLive) { // redirect to the next page res.redirect("/next-page"); } else { // render of the orginal view file res.render("where-do-you-live.njk"); }});
If we look at the error summary and the error method on radio buttons it looks like both require a text message, we should try and generate our error message in a similar format.
Our final route now catches an error and sends this to the view render.
// place this in your routes.js filerouter.post('/where-do-you-live', (req, res) => { // uses optional chaining to check if whereDoYouLive excisit // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining if(req.body?.whereDoYouLive) { // redirect to the next page res.redirect("/next-page"); } else { // representation of the error based on the error summary // https://design-system.service.gov.uk/components/error-summary/ const errors = { whereDoYouLive: { text: "Select the country where you live", href: "#whereDoYouLive" } } // render of the orginal view file with error passed res.render("where-do-you-live.njk", { errors }); }});
Error Summary
The big thing about something like an error summary is we want it to only happen if errors are present, this means our view will work for both GET and POST.
{# place after <div class="govuk-grid-column-two-thirds"> #}{# check if errors has been supplied #}{% if errors %} {# use of govukErrorSummary with error from the route #} {{ govukErrorSummary({ titleText: "There is a problem", errorList: [ errors.whereDoYouLive ] }) }}{% endif %}
This could be moved into a layout to happen anytime an error happens but would need some work on the format.
Error on the field
In the same way as the error summary, we can trigger the error on the field with the errors.whereDoYouLive
as the errorMessage on the component.
Final View file
{% extends "layouts/main.html" %}
{% block pageTitle %} {% if errors %} Error: {% endif%} Where do you live - GOV.UK{% endblock %}
{% block content %} <div class="govuk-grid-row"> <div class="govuk-grid-column-two-thirds"> {# check if errors has been supplied #} {% if errors %} {# use of govukErrorSummary with error from the route #} {{ govukErrorSummary({ titleText: "There is a problem", errorList: [ errors.whereDoYouLive ] }) }} {% endif %} {# form to POST on the GET route #} <form method="post" action="#"> {# example of govuk radio button #} {{ govukRadios({ name: "whereDoYouLive", fieldset: { legend: { text: "Where do you live?", isPageHeading: true, classes: "govuk-fieldset__legend--l" } }, errorMessage: errors.whereDoYouLive, items: [ { value: "england", text: "England" }, { value: "scotland", text: "Scotland" }, { value: "wales", text: "Wales" }, { value: "northern-ireland", text: "Northern Ireland" } ] }) }}
{# button to submit the form #} {{ govukButton({ text: "Save and continue" }) }} </form>{% endblock %}
Exercises
- Define some validation for a more complex field (like a persons age)
- Switch to a text input component
- Define mutiple error messages i.e a age cannot be empty, age must be a number, age must be over 16 and age must be less then 110.