As mentioned in the section about updating the state the validate update function takes one or more validation functions as a parameter and uses them to validate the value of a form state. 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 undefined (and for email and pattern empty strings and for minLength empty strings and arrays) 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.

The following table lists all validation functions provided by ngrx-forms.

Function Description
required Requires the value to be non-empty (i.e. non-null, non-empty string, non-empty array 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
notEqualTo Requires the value to be not 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 or array value to have a minimum length. Empty strings and arrays are always valid to allow for optional form controls. Use this function together with required if those values should not be valid
maxLength Requires a string or array value to have a maximum length
email Requires a string value to be a valid e-mail address. Empty strings are always valid to allow for optional form controls. Use this function together with required if empty strings should not be valid
number Requires the value to be a number
pattern Requires a string value to match a regular expression. Empty strings are always valid to allow for optional form controls. Use this function together with required if empty strings should not be valid

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 validateMyForm = updateGroup<MyFormValue>({
  someTextInput: validate(required),
  nested: updateGroup<NestedValue>({
    someNumber: validate(required, greaterThanOrEqualTo(2), lessThan(10)),
  }),
});

Each of the provided validation functions augments the ValidationErrors interface used for the errors property of form states with the error object the function produces which provides some type safety when accessing the errors property. You are encouraged to do the same for your own custom validation functions. Below is an example of how this can be achieved.

export interface MyCustomValidationError {
  someProperty: string;
}

// @ts-ignore
declare module 'ngrx-forms' {
  export interface ValidationErrors {
    myCustomError?: MyCustomValidationError;
  }
}

Asynchronous Validation

In addition to the synchronous validation via update functions explained above ngrx-forms supports asynchronous validation for form states. 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.

If you prefer to use your own custom actions for coordinating the asynchronous validation you can use the update functions startAsyncValidation, setAsyncError and clearAsyncError in your reducer instead of dispatching the actions.

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 StartAsyncValidationAction for 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 SetAsyncErrorAction for exists with value true (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 StartAsyncValidationAction for 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 ClearAsyncErrorAction for 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"))
  );