How to display error message based on custom validation rules in Angular 2?

eloquent_poltergeist picture eloquent_poltergeist · Jul 25, 2016 · Viewed 47.3k times · Source

I am using a template-driven approach to building forms in Angular 2 and I have successfully created custom validators that I can use in the template.

However, I can't find a way to display specific error message bound to specific errors. I want to differentiate why the form is not valid. How do I achive that?

        import { Component } from '@angular/core';

    import { NgForm } from '@angular/forms';

    import { Site } from './../site';

    import { BackendService } from './../backend.service';

    import { email } from './../validators';

    import { CustomValidators } from './../validators';

    @Component({
        templateUrl: 'app/templates/form.component.html',
        styleUrls: ['app/css/form.css'],
        directives: [CustomValidators.Email, CustomValidators.Url, CustomValidators.Goof],
        providers: [BackendService]
    })

    export class FormComponent {
        active = true;
        submitted = false;
        model = new Site();

        onSubmit() {
            this.submitted = true;
            console.log(this.model);
        }

        resetForm() {
            this.model = new Site();
            this.submitted = false;
            this.active = false;
            setTimeout(() => this.active = true, 0);
        }

        get diagnostics() {
            return JSON.stringify(this.model)
        }
    }

import { Directive, forwardRef } from '@angular/core';
import { NG_VALIDATORS, FormControl } from '@angular/forms';
import { BackendService } from './backend.service';

function validateEmailFactory(backend:BackendService) {
    return (c:FormControl) => {
        let EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;

        return EMAIL_REGEXP.test(c.value) ? null : {
            validateEmail: {
                valid: false
            }
        };
    };
}

export module CustomValidators {

    @Directive({
        selector: '[email][ngModel],[email][formControl]',
        providers: [
            {provide: NG_VALIDATORS, useExisting: forwardRef(() => CustomValidators.Email), multi: true}
        ]
    })
    export class Email {
        validator:Function;

        constructor(backend:BackendService) {
            this.validator = validateEmailFactory(backend);
        }

        validate(c:FormControl) {
            return this.validator(c);
        }
    }

    @Directive({
        selector: '[url][ngModel],[url][formControl]',
        providers: [
            {provide: NG_VALIDATORS, useExisting: forwardRef(() => CustomValidators.Url), multi: true}
        ]
    })
    export class Url {
        validator:Function;

        constructor(backend:BackendService) {
            this.validator = validateEmailFactory(backend);
        }

        validate(c:FormControl) {
            var pattern = /(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/;

            return pattern.test(c.value) ? null : {
                validateEmail: {
                    valid: false
                }
            };
        }
    }

    @Directive({
        selector: '[goof][ngModel],[goof][formControl]',
        providers: [
            {provide: NG_VALIDATORS, useExisting: forwardRef(() => CustomValidators.Goof), multi: true}
        ]
    })
    export class Goof {
        validate(c:FormControl) {
            return {
                validateGoof: {
                    valid: false
                }
            };
        }
    }
}

Answer

Paul Samsotha picture Paul Samsotha · Jul 25, 2016

You can just check the AbstractControl#hasError(...) method to see if the control has a specific error. FormGroup and FormControl are both AbstractControls. for FormControl you just pass as an argument the error name. For example

function regexValidator(control: FormControl): {[key:string]: boolean} {
  if (!control.value.match(/^pee/)) {
    return { 'badName': true };
  }
}

<div *ngIf="!nameCtrl.valid && nameCtrl.hasError('badName')"
     class="error">Name must start with <tt>pee</tt>.
</div>

The validator method should return a string/boolean map, where the key is the name of the error. This is the name that you check for in hasError method.

For FormGroup you can pass as an extra parameter the path to the FormControl.

<div *ngIf="!form.valid && form.hasError('required', ['name'])"
     class="error">Form name is required.</div>

name is simply the identifier of the FormControl for the input.

Here is an example with both the FormControl and the FormGroup check.

import { Component } from '@angular/core';
import {
  FormGroup,
  FormBuilder,
  FormControl,
  Validators,
  AbstractControl,
  REACTIVE_FORM_DIRECTIVES
} from '@angular/forms';

function regexValidator(control: FormControl): {[key:string]: boolean} {
  if (!control.value.match(/^pee/)) {
    return { 'badName': true };
  }
}

@Component({
  selector: 'validation-errors-demo',
  template: `
    <div>
      <h2>Differentiate Validation Errors</h2>
      <h4>Type in "peeskillet"</h4>
      <form [formGroup]="form">
        <label for="name">Name: </label>
        <input type="text" [formControl]="nameCtrl"/>
        <div *ngIf="!nameCtrl.valid && nameCtrl.hasError('required')"
             class="error">Name is required.</div>
        <div *ngIf="!nameCtrl.valid && nameCtrl.hasError('badName')"
             class="error">Name must start with <tt>pee</tt>.</div>
        <div *ngIf="!form.valid && form.hasError('required', ['name'])"
             class="error">Form name is required.</div>
      </form>
    </div>
  `,
  styles: [`
    .error {
      border-radius: 3px;
      border: 1px solid #AB4F5B;
      color: #AB4F5B;
      background-color: #F7CBD1;
      margin: 5px;
      padding: 10px;
    }
  `],
  directives: [REACTIVE_FORM_DIRECTIVES],
  providers: [FormBuilder]
})
export class ValidationErrorsDemoComponent {
  form: FormGroup;
  nameCtrl: AbstractControl;

  constructor(formBuilder: FormBuilder) {
    let name = new FormControl('', Validators.compose([
      Validators.required, regexValidator
    ]));
    this.form = formBuilder.group({
      name: name
    });
    this.nameCtrl = this.form.controls['name'];
  }
}

UPDATE

Ok so I got it working, but it's a little verbose. I couldn't figure out how to properly get access to the individual FormControl of the inputs. So what I did was just create a reference to the FormGroup

<form #f="ngForm" novalidate>

Then then to check for validity, I just use the hasError overload that passed the path of the form control name. For <input> that use name and ngModel, the name value gets added to the main FormGroup with that name as the FormControl name. So you can access it like

`f.form.hasError('require', ['nameCtrl'])`

assuming name=nameCtrl. Notice the f.form. The f is the NgForm instance which has a FormGroup member variable form.

Below is the refactored example

import { Component, Directive } from '@angular/core';
import {
  FormControl,
  Validators,
  AbstractControl,
  NG_VALIDATORS,
  REACTIVE_FORM_DIRECTIVES
} from '@angular/forms';

function validateRegex(control: FormControl): {[key:string]: boolean} {
  if (!control.value || !control.value.match(/^pee/)) {
    return { 'badName': true };
  }
}
@Directive({
  selector: '[validateRegex]',
  providers: [
    { provide: NG_VALIDATORS, useValue: validateRegex, multi: true }
  ]
})
export class RegexValidator {
}

@Component({
  moduleId: module.id,
  selector: 'validation-errors-template-demo',
  template: `
    <div>
      <h2>Differentiate Validation Errors</h2>
      <h4>Type in "peeskillet"</h4>
      <form #f="ngForm" novalidate>
        <label for="name">Name: </label>

        <input type="text" name="nameCtrl" ngModel validateRegex required />

        <div *ngIf="!f.form.valid && f.form.hasError('badName', ['nameCtrl'])"
             class="error">Name must start with <tt>pee</tt>.</div>

        <div *ngIf="!f.form.valid && f.form.hasError('required', ['nameCtrl'])"
             class="error">Name is required.</div>
      </form>
    </div>
  `,
  styles: [`
    .error {
      border-radius: 3px;
      border: 1px solid #AB4F5B;
      color: #AB4F5B;
      background-color: #F7CBD1;
      margin: 5px;
      padding: 10px;
    }
  `],
  directives: [REACTIVE_FORM_DIRECTIVES, RegexValidator]
})
export class ValidationErrorsTemplateDemoComponent {

}