Skip to content

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

  1. The Screen
  2. Define your validation
  3. POST route for processing the data
  4. Catch and throw an error
  5. Error summary
  6. 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.

/app/views/where-do-you-live.njk
{% 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.

/app/routes.js
// place this in your routes.js file
router.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.

/app/routes.js
// place this in your routes.js file
router.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.

/app/routes.js
// place this in your routes.js file
router.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.

/app/views/where-do-you-live.njk
{# 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

/app/views/where-do-you-live.njk
{% 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

  1. Define some validation for a more complex field (like a persons age)
  2. Switch to a text input component
  3. 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.