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

51,167

Solution 1

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 {

}

Solution 2

I wrote set of directives similar to ng-messages from AngularJs to tackle this problem in Angular. https://github.com/DmitryEfimenko/ngx-messages

<div [val-messages]="myForm.get('email')">
  <span val-message="required">Please provide email address</span>
  <span val-message="server" useErrorValue="true"></span>
</div>
Share:
51,167
eloquent_poltergeist
Author by

eloquent_poltergeist

Updated on July 19, 2022

Comments

  • eloquent_poltergeist
    eloquent_poltergeist almost 2 years

    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
                    }
                };
            }
        }
    }
    
  • eloquent_poltergeist
    eloquent_poltergeist almost 8 years
    This is helpful, however you are using model-driven approach to building forms (using FormBuilder). When I use template-driven, I don't know how to expose the AbstractControl in html to ask it if it has error. I always get hasError is not a function or something similar.
  • Paul Samsotha
    Paul Samsotha almost 8 years
    In your template, these models are created implicitly to represent the inputs. You can still access them from the template without have any member model variables at all. You just need to create reference variables in your template. If you post a more complete example, I can try and help you with that
  • Paul Samsotha
    Paul Samsotha almost 8 years
    For instance, in your form you can do <form #f="ngForm">, and now f is the variable that represent the FormGroup. ngForm is something that is provided, so you don't need to worry about where that comes from. Also for your different inputs you can use <input #nameInput [formControl]="nameInput">, and the nameInput would be added to the f form as a FormControl. There are a lot of different ways to set up declarative templates. If you need to more help, I will probably need to see your setup in your template
  • eloquent_poltergeist
    eloquent_poltergeist almost 8 years
    I would like a simple form with 1 input (declaratively) that is two-way bound with model AND I have access to the AbstractControl form the template to check it's validity and custom errors. [formControl]="nameInput" throws Can't bind to 'formControl' since it isn't a known native property
  • Paul Samsotha
    Paul Samsotha almost 8 years
    Check out my update. I got it working, but not they way I thought it should work. Seems my assumptions were incorrect.