As mentioned in the section about updating the state the validate update function takes a function as a parameter that validates the value of a form control or group. ngrx-forms provides a set of validation functions out of the box (imported via ngrx-forms/validation) that can be used as arguments to validate. Most of these functions treat null (and for email and pattern empty strings) as valid to allow for optional form controls. If the control is not optional simply combine the corresponding validation function with the required validation function.
| Function | Description |
|---|---|
required |
Requires the value to be non-empty (i.e. non-null, non-empty string etc.) |
requiredTrue |
Requires the boolean value to be true |
requiredFalse |
Requires the boolean value to be false |
equalTo |
Requires the value to be equal to another value |
lessThan |
Requires the number value to be less than another number |
lessThanOrEqualTo |
Requires the number value to be less than or equal to another number |
greaterThan |
Requires the number value to be greater than another number |
greaterThanOrEqualTo |
Requires the number value to be greater than or equal to another number |
minLength |
Requires a string value to have a minimum length |
maxLength |
Requires a string value to have a maximum length |
email |
Requires a string value to be a valid e-mail address |
pattern |
Requires a string value to match a regular expression |
Below you can see an example of how these functions can be used:
import { updateGroup, validate } from 'ngrx-forms';
import { required, greaterThanOrEqualTo, lessThan } from 'ngrx-forms/validation';
export interface NestedValue {
someNumber: number;
}
export interface MyFormValue {
someTextInput: string;
someCheckbox: boolean;
nested: NestedValue;
}
const updateMyFormGroup = updateGroup<MyFormValue>({
someTextInput: validate(required),
nested: updateGroup({
someNumber: validate([required, greaterThanOrEqualTo(2), lessThan(10)]),
}),
});In addition to the synchronous validation via update functions explained above ngrx-forms supports asynchronous validation for form elements. However, since asynchronous validations are by nature not side-effect free they need to be handled differently.
ngrx-forms provides a set of three actions that can be used to perform asynchronous validation. These actions can be dispatched however you like, be that from a service or from within effects. The first of these actions is the StartAsyncValidationAction which takes the name of the validation to be performed. This name is added to the pendingValidations of the control state and the isValidationPending flag is set to true (if it was not already) for the control and all its parents. However, the validity of the control is not affected by this action. This means, if you e.g. want to disable the submit button of your form while the form is invalid or currently validating you need to check two properties, e.g. [disabled]="formState.isInvalid || formState.isValidationPending". You can have as many asynchronous validations running at the same time as you like. The isValidationPending flag will be true as long as at least one validation has not yet completed.
The last two actions are used to complete the validation. The SetAsyncErrorAction takes the name of the validation and an arbitrary value and adds an error with the given name (prefixed with a $) and value to the state's errors. It also removes the validation from the control's pendingValidations. The $ prefix marks all asynchronous errors which allows the synchronous validation via update functions to preserve these errors. That means you can safely combine synchronous validation and asynchronous validation. By adding the error the control will be marked as invalid if it was not already. The other action is the ClearAsyncErrorAction which takes only the name of the validation and removes the error if it was present. This action also removes the validation from the control's pendingValidations.
Below you can find an example of the steps that occur during such an asynchronous validation. Each step shows a slice of the control's state at the time. The scenario is a book search in a book store.
- The user types a search
{
"value": "some book I am looking for",
"isValid": true,
"isInvalid": false,
"errors": {},
"pendingValidations": [],
"isValidationPending": false
}- Your code dispatches a
StartAsyncValidationActionfor the name "exists"
{
"value": "some book I am looking for",
"isValid": true,
"isInvalid": false,
"errors": {},
"pendingValidations": ["exists"],
"isValidationPending": true
}- The search returns that the book does not exist, i.e. your code dispatches a
SetAsyncErrorActionfor "exists" with valuetrue(this value can be freely chosen and only exists so that you may use it to store metadata that you want to attach to the error)
{
"value": "some book I am looking for",
"isValid": false,
"isInvalid": true,
"errors": {
"$exists": true,
},
"pendingValidations": [],
"isValidationPending": false
}- The user types another search and your code dispatches another
StartAsyncValidationActionfor the name "exists"
{
"value": "lord of the rings",
"isValid": false,
"isInvalid": true,
"errors": {
"$exists": true,
},
"pendingValidations": ["exists"],
"isValidationPending": true
}- The search returns that the book does exist, so your code dispatches a
ClearAsyncErrorActionfor "exists"
{
"value": "lord of the rings",
"isValid": true,
"isInvalid": false,
"errors": {},
"pendingValidations": [],
"isValidationPending": false
}If you are using @ngrx/effects your validation might look like this:
@Effect()
validateBookExists$: Observable<Action> = this.actions$
.ofType(StartBookSearchAction.TYPE)
.switchMap(a =>
this.http.get(`api/books/search/${a.searchTerm}`)
.map(resp =>
resp.status === 404
? new SetAsyncErrorAction(a.controlId, "exists", true)
: new ClearAsyncErrorAction(a.controlId, "exists")
)
// controlId may either be sent with the action or obtained from the store via withLatestFrom
.startWith(new StartAsyncValidationAction(a.controlId, "exists"))
);