Form validation with GDS error templates, works with Govuk Prototype Kit
npm install @nubz/gds-validation!example workflow

Require this package in your node server apps, including govuk prototypes and use the methods to validate forms and fields on the server to
return error messages that are templated to GDS recommendations. Response formats are optimised for use in govuk prototype
Nunjucks error summary components, however there is no dependency on any govuk libraries so other apps can also use this package and
consume error responses as they see fit or even overwrite the GDS error templating with custom error messages in the field model.
```
npm install --save @nubz/gds-validation$3
Examples of simple and complex use cases can be seen on https://prototype-strategies.herokuapp.com/errors/
The main functions that are exposed are getPageErrors() which returns an errors object and isValidPage() which
returns a boolean value. Both functions require you to pass in the dataset/payload to be analysed and the model to
validate against.
The dataset to be analysed should be a flat map of answers to all fields in order to work with this package. For simple,
single page validations where the request body contains all fields required, then passing in the body is enough. In
other, more integrated validations, then the entire dataset may be required to cross-reference the values of other named
fields. For example, in govuk prototypes the entire data set is contained within the session data attached to the
request request.session.data so if we had a page model with a field that references another field from another page or `
system value then we would need this entire dataset to be passed in.typescript
interface Payload {
[key: String]: String | Number | Array
}
type getPageErrors = (data: Payload, pageModel: PageModel) => Errors
type isValidPage = (data: Payload, pageModel: PageModel) => boolean
`
Page models are constructed by you to describe what fields are on a page. A valid page model will use field names as
keys to field objects. If the field relates to a form control in a template you need to ensure the HTML field name
matches the key used in your page model, dates can be composed of separate day, month and year inputs - so in this case
the fields on the page are expected to use the date field name those inputs are composed into.
`typescript
// example model
const pageModel = {
fields: {
'full-name': {
type: 'nonEmptyString', // if left blank we will see "Enter your full name"
name: 'your full name'
},
'date-of-birth': {
type: 'date',
name: 'your date of birth',
beforeToday: true // if a future date is entered we will see "Your date of birth must be in the past"
},
'number-of-days-available': {
type: 'number',
name: 'how many days you are available',
min: 5 // if a number less than 5 is submitted we will see "How many days you are available must be 5 or more", field names are capitalised if they begin an error message
}
}
}
// using TypeScript interfaces as documentation
interface PageModel {
fields: FieldsMap
includeIf?: (data: Payload) => Boolean
}
interface FieldsMap {
[key: FieldKey]: FieldObject
}
type FieldKey = String // the id of a field, this should match any HTML input name in templates
type IsoDateString = ${number}${number}${number}${number}-${number}${number}-${number}${number}array
interface FieldObject {
type: 'date' | 'currency' | 'enum' | 'optionalString' | 'nonEmptyString' | 'number' | 'file' | 'array'
name: String // the description of the field for use in messages, in GDS templates this represents [whatever it is] placeholders
validValues?: Array, value of enum will be compared to values listed hereour records
matches?: Array
matchingExclusions?: Array
noMatchText?: String // for use in error message to describe what the input is matched against - defaults to if missing${fieldDescription} is not valid
includeIf?: (data: Payload) => Boolean // field will not be validate if returns false e.g. data => data.otherField === 'Yes'
regex?: RegExp
exactLength?: Number // number of characters long
minLength?: Number // number of characters long
maxLength?: Number // number of characters long
inputType?: 'characters' | 'digits' | 'numbers' | 'letters and numbers' | 'letters' // any description of permitted keys
min?: FieldKey | IsoDateString | Number | Function<(data: Payload) => Number | IsoDateString> // supported by date, number and currency field types, if a FieldKey is used then the value stored in that field will be used, if a number is used then that is the amount, if a function is used it must return a date or a number to match teh field type e.g. data => parseFloat(data.otherField) / 2
max?: FieldKey | IsoDateString | Number | Function<(data: Payload) => Number | IsoDateString> // supported by date, number and currency field types
minDescription?: String // optional description of the minimum amount or date e.g. 'half the amount of the other field' or 'the date you joined'
maxDescription?: String // optional description of the maximum amount or date e.g. 'the amount you have in the bank' or 'the date you left'
beforeToday?: Boolean
afterToday?: Boolean
patternText?: String // description of regex for error messages - defaults to
errors?: CustomErrors // instead of using templated errors it's possible to include an error object with keys as the error case name (see ErrorTemplateName values) to overwrite the template with a custom string (or function that has access to data for interpolation)
transform?: (data: Payload) => any // is used to assign a new value to validate for the field object e.g. stripping out hyphens and spaces from sort-code value means we can return a new value with this method: data => data['sort-code'].replace(/-/g, '').replace(/\s+/g, '')
}
interface CustomErrors {
[key: ErrorTemplateName]: String | Function<(field: FieldObject) => String>
}
type ErrorTemplateName = 'required' | 'betweenMinAndMax' | 'betweenMinAndMaxNumbers' | 'betweenMinAndMaxDates' | 'tooShort' | 'tooLong' | 'exactLength' | 'number' | 'currency' | 'numberMin' | 'currencyMin' | 'numberMax' | 'currencyMax' | 'pattern' | 'enum' | 'missingFile' | 'date' | 'beforeToday' | 'afterToday' | 'afterFixedDate' | 'beforeFixedDate' | 'noMatch' | 'dayRequired' | 'monthRequired' | 'yearRequired' | 'dayAndYearRequired' | 'dayAndMonthRequired' | 'monthAndYearRequired'
`
function will return true or false, this method is useful when iterating a group of pages
together. The getPageErrors() function will return an errors object for use in templates, even when there are no
errors within, so to assert there are no errors we could test the value of hasErrors which is a boolean.
`typescript
type FieldKey = String // the key of a field which should match the HTML name of the field
type DateInput = 'day' | 'month' | 'year'
interface Errors {
summary: Array
inline: InlineErrors
text: ErrorMessages
hasErrors: Boolean
}
interface Error {
id: FieldKey
text: String
href: String
inputs: Array // an array of all inputs in error - date errors, such as dayAndYearRequired, can cover multiple inputs e.g. ['day', 'year']
}
interface InlineErrors {
[key: FieldKey]: Error
}
interface ErrorMessages {
[key: FieldKey]: String
}
`Example usage of the errors object
In an Express route handler for a post you could pass the posted data alongside a page model to the getPageErrors method
and this would return an error object that either contains errors or not. In this example we are writing the page model
directly into the call as second parameter which can often be a quick way to get error handling on a page, we could,
alternatively, create a models.js file and export all of our models from there into our routes file.`ecmascript 6
const validation = require('@nubz/gds-validation')router.post('/test-page', (req, res) => {
const errors = validation.getPageErrors(req.body, {
fields: {
'full-name': {
type: 'nonEmptyString',
name: 'Your full name'
},
'date-of-birth': {
type: 'date',
name: 'Your date of birth',
beforeToday: true
}
}
})
if (errors.hasErrors) {
res.render('/test-page', { errors })
} else {
res.redirect('next-page')
}
})
`
We can create Nunjucks macros to pass the errors returned into, these macros work with govuk prototypes:
`
{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}{% macro summary(data = {}) %}
{% if data.errors %}
{{ govukErrorSummary({
titleText: "There is a problem",
errorList: data.errors
}) }}
{% endif %}
{% endmacro %}
{% macro inline(data = {}) %}
{% if data.errors[data.key] %}
{% endif %}
{% endmacro %}
`
Then we can use these macros in a Gov Prototype kit template with our errors object, if there are no errors the template just skips over the macros.
`html
{% block content %}
{{ errorMacros.summary({errors: errors.summary}) }}