Official Documentation View Source on Github

Highlights

  1. Define the validation inside a function, which is then used by a directive.
  2. Set the custom validators by using directives on the fields.
  3. Access the validation errors through ngModel.

// app/custom-validation/customer-request.ts

export interface CustomerRequest {
  id: number;
  customerId: string;
  date: Date;
  message: string;
}
// app/custom-validation/not-in-year-validator.directive.ts

import { Directive, Input } from '@angular/core';
import {
  AbstractControl,
  NG_VALIDATORS,
  ValidationErrors,
  Validator,
  ValidatorFn,
} from '@angular/forms';

export function notInYearValidator(notInYear: number): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    if (!control.value) {
      return null;
    }
    const date = control.value as Date;
    return date.getFullYear() == notInYear
      ? { notInYear: { value: control.value } }
      : null;
  };
}

// IMPORTANT: The directive is not needed if you're using reactive forms
@Directive({
  selector: '[appNotInYear]',
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: NotInYearValidatorDirective,
      multi: true,
    },
  ],
})
export class NotInYearValidatorDirective implements Validator {
  @Input('appNotInYear')
  notInYear?: number;

  validate(control: AbstractControl): ValidationErrors | null {
    return this.notInYear ? notInYearValidator(this.notInYear)(control) : null;
  }
}
// app/custom-validation/valid-customer-id-validator.directive.ts

import { Directive } from '@angular/core';
import {
  AbstractControl,
  NG_VALIDATORS,
  ValidationErrors,
  Validator,
  ValidatorFn,
} from '@angular/forms';

export function validCustomerIdValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    if (!control.value) {
      return null;
    }
    const valid = /^[A-Z]{2}[0-9]{4}$/.test(control.value);
    return valid ? null : { validCustomerId: { value: control.value } };
  };
}

// IMPORTANT: The directive is not needed if you're using reactive forms
@Directive({
  selector: '[appValidCustomerId]',
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: ValidCustomerIdValidatorDirective,
      multi: true,
    },
  ],
})
export class ValidCustomerIdValidatorDirective implements Validator {
  validate(control: AbstractControl): ValidationErrors | null {
    return validCustomerIdValidator()(control);
  }
}
<!-- app/custom-validation/customer-request-form-ngxs/customer-request-form-ngxs.component.html -->

<h1>Customer Request (NGXS)</h1>

<mat-card>
  <form (ngSubmit)="submit()" #customerRequestForm="ngForm">
    <mat-form-field>
      <mat-label>Customer ID</mat-label>
      <input
        matInput
        required
        appValidCustomerId
        name="customerId"
        [ngModel]="(customerRequest$ | async)?.customerId"
        (ngModelChange)="setCustomerId($event)"
        #customerId="ngModel"
      />
      <mat-error *ngIf="customerId.errors?.required"
        >You must enter a value</mat-error
      >
      <mat-error *ngIf="customerId.errors?.validCustomerId"
        >The ID must start with two uppercase letters followed by four
        numbers</mat-error
      >
    </mat-form-field>
    <mat-form-field>
      <mat-label>Date</mat-label>
      <input
        matInput
        required
        [appNotInYear]="2020"
        name="date"
        [matDatepicker]="picker"
        [ngModel]="(customerRequest$ | async)?.date"
        (ngModelChange)="setDate($event)"
        #date="ngModel"
      />
      <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
      <mat-datepicker #picker></mat-datepicker>
      <mat-error *ngIf="date.errors?.required"
        >You must enter a value</mat-error
      >
      <mat-error *ngIf="date.errors?.notInYear"
        >The date cannot be in 2020</mat-error
      >
    </mat-form-field>
    <mat-form-field>
      <mat-label>Message</mat-label>
      <textarea
        matInput
        required
        name="message"
        [ngModel]="(customerRequest$ | async)?.message"
        (ngModelChange)="setMessage($event)"
        #message="ngModel"
      ></textarea>
      <mat-error *ngIf="message.errors?.required"
        >You must enter a value</mat-error
      >
    </mat-form-field>
    <div class="form-buttons">
      <button
        mat-raised-button
        type="submit"
        color="primary"
        [disabled]="(loading$ | async) || !customerRequestForm.form.valid"
      >
        Submit
      </button>
    </div>
  </form>
</mat-card>

<h2>Customer Request (JSON)</h2>

<pre
  >{{ customerRequest$ | async | json }}
</pre>
// app/custom-validation/customer-request-form-ngxs/customer-request-form-ngxs.component.ts

import { Component } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { withLatestFrom } from 'rxjs/operators';
import { CustomerRequest } from 'src/app/custom-validation/customer-request';
import { CustomerRequestFormNgxs } from 'src/app/custom-validation/customer-request-form-ngxs/customer-request-form-ngxs.actions';
import { CustomerRequestFormNgxsSelectors } from 'src/app/custom-validation/customer-request-form-ngxs/customer-request-form-ngxs.selectors';

@Component({
  selector: 'app-customer-request-form-ngxs',
  templateUrl: './customer-request-form-ngxs.component.html',
})
export class CustomerRequestFormNgxsComponent {
  @Select(CustomerRequestFormNgxsSelectors.customerRequest)
  customerRequest$!: Observable<CustomerRequest>;

  @Select(CustomerRequestFormNgxsSelectors.loading)
  loading$!: Observable<boolean>;

  constructor(
    private readonly store: Store,
    private readonly snackBar: MatSnackBar
  ) {}

  setCustomerId(customerId: string) {
    this.store.dispatch(new CustomerRequestFormNgxs.SetCustomerId(customerId));
  }

  setDate(date: Date) {
    this.store.dispatch(new CustomerRequestFormNgxs.SetDate(date));
  }

  setMessage(message: string) {
    this.store.dispatch(new CustomerRequestFormNgxs.SetMessage(message));
  }

  submit(): void {
    this.store
      .dispatch(new CustomerRequestFormNgxs.Submit())
      .pipe(withLatestFrom(this.customerRequest$))
      .subscribe(([, { id }]) => {
        this.snackBar.open(`Customer request saved with ID #${id}`, 'Close');
      });
  }
}
// app/custom-validation/customer-request.service.ts

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { CustomerRequest } from 'src/app/custom-validation/customer-request';

export interface CustomerRequestSaveResponse {
  id: number;
}

@Injectable({
  providedIn: 'root',
})
export class CustomerRequestService {
  save(
    customerRequest: CustomerRequest
  ): Observable<CustomerRequestSaveResponse> {
    console.log('Save:', customerRequest);
    return of({
      id: 1,
    }).pipe(delay(1000));
  }
}
// app/custom-validation/customer-request-form-ngxs/customer-request-form-ngxs.state.ts

import { Injectable } from '@angular/core';
import { Action, State, StateContext } from '@ngxs/store';
import { finalize, tap } from 'rxjs/operators';
import { CustomerRequest } from 'src/app/custom-validation/customer-request';
import { CustomerRequestFormNgxs } from 'src/app/custom-validation/customer-request-form-ngxs/customer-request-form-ngxs.actions';
import { CustomerRequestService } from 'src/app/custom-validation/customer-request.service';

export interface CustomerRequestFormNgxsStateModel {
  customerRequest: CustomerRequest;
  loading: boolean;
}

@State<CustomerRequestFormNgxsStateModel>({
  name: 'customerRequestFormNgxs',
  defaults: {
    customerRequest: {
      id: 0,
      customerId: '',
      date: new Date(),
      message: '',
    },
    loading: false,
  },
})
@Injectable()
export class CustomerRequestFormNgxsState {
  constructor(
    private readonly customerRequestService: CustomerRequestService
  ) {}

  @Action(CustomerRequestFormNgxs.SetCustomerId)
  setCustomerId(
    context: StateContext<CustomerRequestFormNgxsStateModel>,
    { customerId }: CustomerRequestFormNgxs.SetCustomerId
  ) {
    const state = context.getState();
    context.patchState({
      customerRequest: { ...state.customerRequest, customerId },
    });
  }

  @Action(CustomerRequestFormNgxs.SetDate)
  setDate(
    context: StateContext<CustomerRequestFormNgxsStateModel>,
    { date }: CustomerRequestFormNgxs.SetDate
  ) {
    const state = context.getState();
    context.patchState({
      customerRequest: { ...state.customerRequest, date },
    });
  }

  @Action(CustomerRequestFormNgxs.SetMessage)
  setMessage(
    context: StateContext<CustomerRequestFormNgxsStateModel>,
    { message }: CustomerRequestFormNgxs.SetMessage
  ) {
    const state = context.getState();
    context.patchState({
      customerRequest: { ...state.customerRequest, message },
    });
  }

  @Action(CustomerRequestFormNgxs.Submit)
  submit(context: StateContext<CustomerRequestFormNgxsStateModel>) {
    const state = context.getState();
    context.patchState({
      loading: true,
    });
    return this.customerRequestService.save(state.customerRequest).pipe(
      tap(({ id }) =>
        context.patchState({
          customerRequest: {
            ...state.customerRequest,
            id,
          },
        })
      ),
      finalize(() =>
        context.patchState({
          loading: false,
        })
      )
    );
  }
}
// app/custom-validation/customer-request-form-ngxs/customer-request-form-ngxs.actions.ts

export namespace CustomerRequestFormNgxs {
  export class SetCustomerId {
    static readonly type = '[CustomerRequestFormNgxs] SetCustomerId';
    constructor(readonly customerId: string) {}
  }

  export class SetDate {
    static readonly type = '[CustomerRequestFormNgxs] SetDate';
    constructor(readonly date: Date) {}
  }

  export class SetMessage {
    static readonly type = '[CustomerRequestFormNgxs] SetMessage';
    constructor(readonly message: string) {}
  }

  export class Submit {
    static readonly type = '[CustomerRequestFormNgxs] Submit';
  }
}
// app/custom-validation/customer-request-form-ngxs/customer-request-form-ngxs.selectors.ts

import { Selector } from '@ngxs/store';
import { CustomerRequest } from 'src/app/custom-validation/customer-request';
import {
  CustomerRequestFormNgxsState,
  CustomerRequestFormNgxsStateModel,
} from 'src/app/custom-validation/customer-request-form-ngxs/customer-request-form-ngxs.state';

export class CustomerRequestFormNgxsSelectors {
  @Selector([CustomerRequestFormNgxsState])
  static customerRequest({
    customerRequest,
  }: CustomerRequestFormNgxsStateModel): CustomerRequest {
    return customerRequest;
  }

  @Selector([CustomerRequestFormNgxsState])
  static loading({ loading }: CustomerRequestFormNgxsStateModel): boolean {
    return loading;
  }
}