govuk-eva-validation
v2.3.4
Published
Eva validation system
Downloads
60
Readme
[[TOC]]
What is Eva?
It's a tiny little validation library! Eva aims to be a simple and light fluent validation library for use within our Angular web-components (govuk-angular). The API is readable and allows for easy creation of complex validations.
Current status
Very much in development! :-)
Install
npm i govuk-eva-validation
Not another validation library!
When working on an application using the gov.uk design system and Angular or any SPA application, we have a requirement for a validation system, a system that can be used to check our models and return a validation results. Results which can then be used by our web-components to show validation errors to our customers. Those error messages and how they are displayed must conform to the patterns outlined by the gov.uk design system.
With the above requirements our options are limited.
- Write our own tiny validation system.
- Use one of the many validation libraries ( Joi, Yup, v8n.. and others)
So we wrote our own!!! :-) see below for details. Out of all the options we had this was the simplest way to meet our validation requirements. The other libraries above would have needed adapting to work with our web-components, that would have been a fair bit of work.
Why is it named Eva?
Eva is just an abbreviation of evaluate, as in evaluate this model for errors.
Features
- Fluent and chainable API.
- Useful standard validation rules.
- Allows for custom validation messages.
- Allows for flexible validation results.
- Works in conjunction with the govuk-angular component library
The source code and specs files provide a great source of information on the library. Dig in to find more examples.
Schema Validation
If you just want to validate a model, then the example below is a good start. When creating a schema, the property names must match your model property names. This example uses the string validation.
const model = {
surname : "",
forename: "smith",
}
// create a schema for that model
const schema: EvaSchema = {
surname : eva()
.string()
.pattern(/^[A-Z][a-z]*/)
.required(),
forename : eva()
.string()
.pattern(/^[A-Z]*/)
.required()
};
// Execute the validation
const result = eva().validate(model, schema);
What about the validation result?
After executing the validate function, the result type is the "model-state".
The result is shown below, there is :
- The errorSummary which hold each ValidationResult
- For each property a validation result
{
"hasBeenValidated": true,
"hasError": true,
"errorSummary": {
"hasError": true,
"errors": [
{
"id": "surname",
"hasError": true,
"fullError": "Surname is not a valid format",
"shortError": "Invalid format"
},
{
"id": "forename",
"hasError": true,
"fullError": "Forename is not a valid format",
"shortError": "Invalid format"
}
]
},
"surname": {
"id": "surname",
"hasError": true,
"fullError": "Surname is not a valid format",
"shortError": "Invalid format"
},
"forename": {
"id": "forename",
"hasError": true,
"fullError": "Forename is not a valid format",
"shortError": "Invalid format"
}
}
Validation Chain
Eva allows you to chain together as many validators as you want. When the chain of validators is executed, eva will stop when one of them returns an error. If all of the validators pass, then the validation is succesful.
There is an exception, sometimes you might want to stop the chain from running if a condition is true. See the validWhenEmpty validator
ValidationResult
A validationResults consists of :
- id, normally the property name
- hasError, error indicator
- fullError, full error using the property name and a standard error message
- shortError, short error, without the property name
Overriding the ValidationResult label
By default the fullError message contains the property name from the schema. but you might what to change that. It's fairly easy, in this example we want the error message to contain Customer Surname and not just Surname. This can be acheived by passing in a label value, see below.
// create a schema for that model
const schema: EvaSchema = {
surname : eva({label : "Customer Surname"})
.string()
.required(),
};
"surname": {
"id": "surname",
"hasError": true,
"fullError": "Customer Surname is not a valid format",
"shortError": "Invalid format"
},
Overriding the ValidationResult fullError
The fullError and in-fact any of the properties of the ValidationResult can be overriden.
// create a schema for that model
const schema: EvaSchema = {
surname : eva()
.string()
.required({fullError: "Nope that's not good!",
shortError: "Dam!"}),
};
"surname": {
"id": "surname",
"hasError": true,
"fullError": "Nope that's not good!",
"shortError": "Dam",
"customeResult": {
"fullError": "Nope that's not good!",
"shortError": "Dam"
}
},
String
The spec are a good source of documentation for how to use the string validators
https://gitlab.com/anthony-griff/govuk-eva/-/blob/master/src/string-validation/string-validation.spec.ts
Required
Property must have a value.
const schema = {
name : eva()
.string()
.required()
}
RequiredWhen - Conditional Validation
Sometimes a field is only required if (x) is true, e.g If date is required but only when it's a Monday! That sort of thing. Or date is required but only when name is empty. This validator takes a whenPredicate, it's a function that returns true or false. It's function is to: only run the required validator when the whenPredicate returns true.
Note : You might want to add the validWhenEmpty validator after the requiredWhen, this will stop the validation chain if the value is empty, after all sometimes it's required sometimes it's not.
const schema = {
name : eva()
.string()
.requiredWhen(() => true)
.validWhenEmpty()
.numbersOnly()
.min(4)
.length(4)
.max(4,)
}
const schema = {
name : eva()
.string()
.requiredWhen(dateIsEmpty)
}
const schema = {
name : eva()
.string()
.requiredWhen(isMonday)
}
Pattern
String must match the regex.
const schema = {
age : eva()
.string()
.pattern(/^\d+$/)
}
Min
String must be greater then or equal to the min length
const schema = {
title : eva()
.string()
.min(3)
}
Max
String must be less than or equal to the max length
const schema = {
title : eva()
.string()
.max(3)
}
Length
String must be the exact length
const schema = {
title : eva()
.string()
.max(3)
}
NumbersOnly
String must be a number
const schema = {
title : eva()
.string()
.numbersOnly();
}
Valid when empty
If you want an empty string to be valid the add the validWhenEmpty property to the validator. If the string is empty then the numbersOnly validator will not be triggered.
const schema = {
title : eva()
.string()
.validWhenEmpty()
.numbersOnly();
}
DateString
String must match one of the passed in date formats and be a valid date. Note, under the hood the validation system is using day.js to validate the date.
If you want to extend which date formats are valid see the day.js documentation.
"DD MMMM YYYY" : 02 March 2020 "DD MMMM YYYY" : 02 March 2020
const schema = {
title : eva()
.string()
.dateString(["DD/MM/YYYY", "DD-MM-YYYY"]);
}
This validator is just a wrapper around day.js, shown below :
return dayjs(dateString, acceptableFormats, 'en', strictMode).isValid();
DateStringValidWhen
The model value is only a valid dateString when the predicate function returns true. Can be used when the dateString validation does not cover all the use cases.
const schema = {
title : eva()
.string()
.dateStringValidWhen(() => true);
}
LessThan GreaterThan
This validator allow us to create an error when one string value is lessThan another string value. In this example start must be less than start. For the error message to read correctly you also need to pass in the name of the other property/field.
An alternative approach would be to just use the errorWhen validator, and write out the rules as a small function. In this can comparing one string to another.
const model = {
start: 'Z',
end: 'A'
};
const schema: EvaSchema = {
start: eva()
.string()
.lessThan( { label: "End", value: () => model.to} )
};
//fullError : Start value must be less than End value
ErrorWhen
The catch all validator, you must supply an error message you won't like the default!!
This validator allows you to generate an error on any predicate function. That is any function that returns true or false. The error is created when true.
In the example below, we want an error to be created for the day property, when today is a Monday. In the example isMonday is a function that returns true or false. Don't make the mistake of executing the isMonday function. You only need to pass the function NOT the result of executing the function. If you don't know the difference then ... well I can't help go google.
The error message can be any message you want, however you should include the name of the property/label in the error. i.e. "Day is invalid"
const schema = {
day: eva()
.string()
.errorWhen( isMonday, {fullError : "Day : I don't like Mondays", shortError : "Tell me why"})
}
const schema = {
title : eva()
.string()
.errorWhen(isValidTitle, { fullError : "Title is too short", shortError : "You got a short error"})
}
InArray
const model = { name: 'jone' };
const schema = {
name: eva().string()
.required()
.inArray(['bill', 'bob', 'barry', ''], { shortError: 'No Match' }),
};
const result = eva().validate(model, schema);
Custom
If you want to do something more complex, then you can write your own validator!
In the example below, we have created a mustBeBeforeLetter validator, it should return a function of type: (fieldRef: fieldRef): ValidationResult
In case that was not clear, a function that will return another function that takes a fieldRef and returns a ValidationResult!!
In your custom validator, fieldRef.value contains the field value. Validate that value is valid and return a ValidationResult with an appropriate error message.
const mustBeBeforeLetter = (beforeLetter: string) => (fieldRef: FieldRef): ValidationResult => {
if (fieldRef.value > beforeLetter) {
return {
id: fieldRef.id,
hasError: true,
fullError: `Letter ${fieldRef.value} should be before ${beforeLetter}`,
shortError: 'You got an issue!',
};
}
return EmptyValidationResult(fieldRef.id);
}
const model = {
nameStartLetter: 'D',
};
const schema = {
nameStartLetter: eva()
.string()
.required()
.custom(mustBeBeforeLetter("A"))
};
const result = eva().validate(model, schema);
Compare field with field
Your custom validator, will also have access to the model if you want to do a comparison type validation, where one field is compaired to another.
If you really want you can ignore the fieldRef.value and just got straight to the model, you have access to the entire model and all the values.
const letterFromValidator = (fieldRef: FieldRef): ValidationResult => {
if (fieldRef.value > this.model.nameEndLetter) {
return {
id: fieldRef.id,
hasError: true,
fullError: `Letter ${fieldRef.value} should be before ${beforeLetter}`,
shortError: 'You got an issue!',
};
}
return EmptyValidationResult(fieldRef.id);
}
const model = {
nameStartLetter: 'D',
nameEndLetter: 'A',
};
const schema = {
nameStartLetter: eva()
.string()
.required()
.custom(letterFromValidator)
};
const result = eva().validate(model, schema);
Valid when empty
Stop the validation chain in it's tracks with the validWhenEmpty validator. There are two flavors to this validator shown below.
In the example below appNumber must be a number, but it's not a required field. So the appNumber isValidWhenEmpty(). We don't want to check that it's a number if it's empty. We also don't want to check the lenght if it's empty.
So validWhenEmpty will stop the rest of the validator that appear after it from executing.
const schema = {
appNumber : eva()
.string()
.validWhenEmpty()
.numbersOnly()
.min(10)
};
Config settings
This is a config setting on the string builder, it can be use to prevent the validation from running when the string is empty. As an example below age must be a number, but it's not required. Without the config setting, the numbersOnly validator would be triggered.
The setting stops all validators in the chain from running when set to true
const schema = {
age: eva()
.string({validWhenEmpty : true})
.numbersOnly()
};
Each validator also has a validWhenEmpty setting. In the example below the numbersOnly will not be triggered for empty values. NOTE : This at a different level to the string setting shown above.
const schema = {
age: eva()
.string()
.numbersOnly({validWhenEmpty : true})
.min(4, {validWhenWmpty: : true})
};
CheckOption
https://gitlab.nonprod.dwpcloud.uk/anthony.a.griffiths/govuk-eva/-/blob/docs-update/src/check-option-validation/check-option-validation.spec.ts
The checkOption validators are designed to validate an array, containing objects, where the objects have a property called checked!! If you want to validate a frontend CheckBox list then this is the way to go.
An example is probably the best way to understand this one. In the example below, one of the items in myColour must be have checked set to true, if it's not then the validation error is triggered.
const model = {
myColour: [
{ text: 'Red', value: 'red', checked: false},
{ text: 'Blue', value: 'blue', checked: false },
{ text: 'Green', value: 'green', checked: false },
{ text: 'Yello', value: 'yellow', checked: false },
],
};
const schema = {
myColour: eva({ label: 'What colours do you like?' })
.checkedOption()
.required(),
};
const modelState = eva().validate(model, schema);
Required
One item in the array must be checked
const schema = {
myColour: eva()
.checkedOption()
.required()
};
AtLeast(n)
To be valid at least 2 items must be checked, you can check more than 2 and that's just fine.
const schema = {
myColour: eva()
.checkedOption()
.atLeast(2)
};
AtMost(n)
To be valid no more than two items should be true. If 3 items are checked then boom, validation error.
const schema = {
myColour: eva()
.checkedOption()
.atMost(2)
};
SelectOption
https://gitlab.nonprod.dwpcloud.uk/anthony.a.griffiths/govuk-eva/-/blob/docs-update/src/select-option-validation/select-option-validation.spec.ts
The select option validator is designed, to work with the govuk-radio-inline and govuk-radio-group angular components.
There is only one validator and that is required.
const model = {
nationality: { selected: '' },
};
const schema: EvaSchema = {
nationality: eva()
.selectOption()
.required()
};
const modelState = eva().validate(model, schema);