Angular 2 Multiple validators

TypescriptAngular

Typescript Problem Overview


Is it possible to have multiple validators on a form field? I tried this, but it resulted in some strange errors (field was never valid, even if requirements were met)

this.username = new Control('', Validators.minLength(5), Validators.required);

How can I use multiple validators?

Typescript Solutions


Solution 1 - Typescript

You can combine validators using Validators.compose()

this.username = new Control('', 
    Validators.compose(
        [Validators.minLength(5), Validators.required]));

for async validators use

this.username = new Control('', null,
    Validators.composeAsync(
        [someAsyncValidator, otherAsyncValidator]));

There are open issues with async validators, especially sync validators combined with async validators don't work

To make sync validators work with async validators, wrap the sync validators in promises and compose them as async valdiators like

this.username = new Control('', null,
    Validators.composeAsync([
        (control:Control) => Promise.resolve(Validators.minLength(5)(control)), 
        (control:Control) => Promise.resolve(Validators.required(control)),
        someAsyncValidator, otherAsyncValidator
    ]));

Solution 2 - Typescript

this problem has been addressed

you can make an array of validators

this.username = new FormControl('', [ Validators.minLength(5), Validators.required ]); 

Solution 3 - Typescript

I suggest utilizing the Validators.compose() method for combining all non-async validators and separately passing in the Validators.composeAsync() for any async calls.

Basically the constructor args for FormControl are as follows:

  1. formState (or simply the initial starting value)
  2. validators (I suggest using Validators.compose([...]) here)
  3. asyncValidators (I suggest using Validators.composeAsync([...]) here)

Example using FormBuilder (feel free to use straight up Control):

this.acctForm = this.fb.group({
            'name': [
                '',
                Validators.compose([
                    Validators.required, Validators.minLength(2), Validators.maxLength(20), Validators.pattern('[a-zA-Z]')
                ])
            ],
            'cellNumber': [
                '',
                Validators.compose([
                    Validators.required, Validators.pattern('[0-9]{10}')
                ]),
                Validators.composeAsync([
                    this.checkPhoneValid.bind(this)
                ])
            ]
        });

This helps avoid async validation until the non-async validators are valid (excl. the initial check, which can easily be handled, see further below).

Everything Combined Example (validators, asyncValidators & debouncing):

import { Component, Injectable, OnInit } from '@angular/core';
import { Http } from '@angular/http';
import { FormBuilder, FormGroup, Validators, AbstractControl } from '@angular/forms';


@Component({
    selector: 'app-sandbox',
    templateUrl: './sandbox.component.html',
    providers: []
})
export class FormControlsDemoComponent implements OnInit {
    private debouncedTimeout;
    public acctForm: FormGroup;

    constructor(private http: Http, private fb: FormBuilder) {
        // @note Http should never be directly injected into a component, for simplified demo sake...
    }

    ngOnInit() {
        this.acctForm = this.fb.group({
            // Simple Example with Multiple Validators (non-async)
            'name': [
                '',
                Validators.compose([
                    Validators.required, Validators.minLength(2), Validators.maxLength(20), Validators.pattern('[a-zA-Z]')
                ])
            ],
            // Example which utilizes both Standard Validators with an Async Validator
            'cellNumber': [
                '',
                Validators.compose([
                    Validators.required, Validators.minLength(4), Validators.maxLength(15), Validators.pattern('[0-9]{10}')
                ]),
                Validators.composeAsync([
                    this.checkPhoneValid.bind(this) // Important to bind 'this' (otherwise local member context is lost)
                    /*
                        @note if using a service method, it would look something like this...
                        @example:
                            this.myValidatorService.phoneUniq.bind(this.myValidatorService)
                    */
                ])
            ],
            // Example with both, but Async is implicitly Debounced
            'userName': [
                '',
                Validators.compose([
                    Validators.required, Validators.minLength(4), Validators.maxLength(15), Validators.pattern('[a-zA-Z0-9_-]')
                ]),
                Validators.composeAsync([
                    this.checkUserUniq.bind(this) // @see above async validator notes regarding use of bind
                ])
            ]
        });

    }

    /**
     * Demo AsyncValidator Method
     * @note - This should be in a service
     */
    private checkPhoneValid(control: AbstractControl): Promise<any> {
        // Avoids initial check against an empty string
        if (!control.value.length) {
            Promise.resolve(null);
        }

        const q = new Promise((resolve, reject) => {
            // determine result from an http response or something...
            let result = true;

            if (result) {
                resolve(null);
            } else {
                resolve({'phoneValidCheck': false});
            }
        });
        return q;
    }

    /**
     * Demo AsyncValidator Method (Debounced)
     * @note - This should be in a service
     */
    private checkUserUniq(control: AbstractControl): Promise<any> {
        // Avoids initial check against an empty string
        if (!control.value.length) {
            Promise.resolve(null);
        }

        clearTimeout(this.debouncedTimeout);

        const q = new Promise((resolve, reject) => {

            this.debouncedTimeout = setTimeout(() => {

                const req = this.http
                    .post('/some/endpoint', { check: control.value })
                    .map(res => {
                        // some handler logic...
                        return res;
                    });

                req.subscribe(isUniq => {
                    if (isUniq) {
                        resolve(null);
                    } else {
                        resolve({'usernameUnique': false });
                    }
                });

            }, 300);
        });
        return q;
    }

}

Some people like to hack in debounced async validator by binding to the control's Observable valueChanges like this:

this.someControl.debounceTime(300).subscribe(val => {
      // async call...
});

I personally don't recommend this for most cases, as it adds unnecessary complications.

> NOTE: This should work, as of the latest version of Angular (2 & 4) since the > writing of this post.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSebastian OlsenView Question on Stackoverflow
Solution 1 - TypescriptGünter ZöchbauerView Answer on Stackoverflow
Solution 2 - TypescriptAnkit RaonkaView Answer on Stackoverflow
Solution 3 - TypescriptTimothy PerezView Answer on Stackoverflow