Handle form errors using components Angular - TypeScript

38,572

Solution 1

You can move the validation errors into a component and pass in the formControl.errors as an input property. That way all the validation messages can be re-used. Here is an example on StackBlitz. The code is using Angular Material but still should be handy even if you aren't.

validation-errors.component.ts

import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
import { FormGroup, ValidationErrors } from '@angular/forms';

@Component({
  selector: 'validation-errors',
  templateUrl: './validation-errors.component.html',
  styleUrls: ['./validation-errors.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ValidationErrorsComponent implements OnInit {
  @Input() errors: ValidationErrors;

  constructor() {}

  ngOnInit() {}

}

validation-errors.component.html

<ng-container *ngIf="errors && errors['required']"> Required</ng-container>
<ng-container *ngIf="errors && errors['notUnique']">Already exists</ng-container>
<ng-container *ngIf="errors && errors['email']">Please enter a valid email</ng-container>

For the back validation messages set the error manually on the form control.

const nameControl = this.userForm.get('name');
nameControl.setErrors({
  "notUnique": true
});

To use the validation component on the form:

   <form [formGroup]="userForm" (ngSubmit)="submit()">
      <mat-form-field>
        <input matInput placeholder="name" formControlName="name" required>
        <mat-error *ngIf="userForm.get('name').status === 'INVALID'">
          <validation-errors [errors]="userForm.get('name').errors"></validation-errors>      
        </mat-error>
      </mat-form-field>
      <mat-form-field>
        <input matInput placeholder="email" formControlName="email" required>
        <mat-error *ngIf="userForm.get('email').status === 'INVALID'">
          <validation-errors [errors]="userForm.get('email').errors"></validation-errors>
        </mat-error>
      </mat-form-field>
      <button mat-raised-button class="mat-raised-button" color="accent">SUBMIT</button>
    </form>

Solution 2

Demo

You can inject NgForm and access the FormControlName directive through @ContentChild within a custom validator component to achieve re-use:

@Component({
  selector: '[validator]',
  template: `
    <ng-content></ng-content>
    <div *ngIf="formControl.invalid">
        <div *ngIf="formControl.errors.required && (form.submitted || formControl.dirty)">
             Please provide {{ formControl.name }}
        </div>
        <div *ngIf="formControl.errors.email && (form.submitted || formControl.dirty)">
             Please provide a valid email
        </div>
        <div *ngIf="formControl.errors.notstring && (form.submitted || formControl.dirty)">
             Invalid name
        </div>

    </div>
`})

export class ValidatorComponent implements OnInit {
   @ContentChild(FormControlName) formControl;
   constructor(private form: NgForm) { 

   }

   ngOnInit() { }

}

To use it, you would wrap all your form controls (which has a formControlName) with an HTML element and add a validator attribute:

<form #f="ngForm" (ngSubmit)="onSubmit(f)" novalidate>
<div [formGroup]="myForm">
     <label>Name</label>
     <div validator>
         <input type="text" formControlName="name">
     </div>
     <label>Lastname</label>
     <div validator>
         <input type="text" formControlName="lastname">
     </div>
     <label>Email</label>
     <div validator>
         <input type="text" formControlName="email">
     </div>
</div>
<button type="submit">Submit</button>
</form>

This will work for synchronous and asynchronous validators.

Solution 3

I had the same requirement , nobody likes to re-write the same code twice.

This can be done by creating custom form controls. The idea is you create your custom form controls , have a common service that Generates a custom formControl object and inject appropriate Validators based on the data type provided into the FormControl Object.

Where did the Data type come from ?

Have a file in your assets or anywhere which contains types like this :

[{
  "nameType" : {
   maxLength : 5 , 
   minLength : 1 , 
   pattern  :  xxxxxx,
   etc
   etc

   }
}
]

This you can read in your ValidatorService and select appropriate DataType with which you can create your Validators and return to your Custom Form Control.

For Example ,

<ui-text name="name" datatype="nameType" [(ngModel)]="data.name"></ui-text>

This is a brief description of it on a high level of what I did to achieve this. If you need additional information with this , do comment. I am out so cannot provide you with code base right now but sometime tomorrow might update the answer.

UPDATE for the Error Showing part

You can do 2 things for it , bind your formControl's validator with a div within the control and toggle it with *ngIf="formControl.hasError('required)"` , etc.

For a Message / Error to be displayed in another generic place like a Message Board its better to put that Message Board markup somewhere in the ParentComponent which does not get removed while routing (debatable based on requirement) and make that component listen to a MessageEmit event which your ErrorStateMatcher of your formControl will fire whenever necessary(based on requirement).

This is the design we used and it worked pretty well , you can do a lot with these formControls once you start Customising them.

Solution 4

For the html validation I would write a custom formcontrol which will basically be a wrapper around an input. I would also write custom validators which return an error message (Build-in validators return an object I believe). Within your custom formcontrol you can do something like this:

<div *ngIf="this.formControl.errors">
    <p>this.formControl.errors?.message</p>
</div>

For the backend validator you can write an async validator.

Solution 5

To make template code clear and avoid duplicated code of validating messages, we should change them to be more reusable, here creating a custom directive which adds and removes validating message code block is an option(shown in below demo).

Show/Hide validating messages

In the directive, we can access to directive' host form control and add/remove validating message based on validate status of it by subscribing to it's valueChanges event.

@Directive(...)
export class ValidatorMessageDirective implements OnInit {

  constructor(
    private container: ControlContainer,
    private elem: ElementRef,          // host dom element
    private control: NgControl         // host form control
  ) { }

  ngOnInit() {
    const control = this.control.control;

    control.valueChanges.pipe(distinctUntilChanged()).subscribe(() => {
      this.option.forEach(validate => {
        if (control.hasError(validate.type)) {
          const validateMessageElem = document.getElementById(validate.id);
          if (!validateMessageElem) {
            const divElem = document.createElement('div');
            divElem.innerHTML = validate.message;
            divElem.id = validate.id;
            this.elem.nativeElement.parentNode.insertBefore(divElem, this.elem.nativeElement.nextSibling);
          }
        } else {
          const validateMessageElem = document.getElementById(validate.id);
          if (validateMessageElem) {
             this.elem.nativeElement.parentNode.removeChild(validateMessageElem);
          }
        }
      })
    });
  }
}

Validate options

The directive adds and removes validating message based on corresponding validate errors. So the last step we should do is to tell directive which types of validate errors to watch and what messages should be shown, that's the @Input field by which we transport validating options to directive.


Then we can simply write template code as below:

<form [formGroup]="form">
  <input type="text" formControlName="test" [validate-message]="testValidateOption"><br/>
  <input type="number" formControlName="test2" [validate-message]="test2ValidateOption">
</form>

Refer working demo.

Share:
38,572
L Y E S  -  C H I O U K H
Author by

L Y E S - C H I O U K H

Husband • Father • SoftwareEngineer • Full-stack • #Java &amp; #JavaScript enthousiast • #Angular • #RESTAPIs • #Algorithm • #Cloud

Updated on June 01, 2021

Comments

  • L Y E S  -  C H I O U K H
    L Y E S - C H I O U K H almost 3 years

    I am currently working on a form in Angular/Typescript of several fields (more than 10 fields), and I wanted to manage the errors more properly without duplicating code in my html page.

    Here is an example of a form :

    <form [formGroup]="myForm">
         <label>Name</label>
         <input type="text" formControlName="name">
         <p class="error_message" *ngIf="myForm.get('name').invalid && (myForm.submitted || myForm.get('name').dirty)">Please provide name</p>
         <label>Lastname</label>
         <input type="text" formControlName="lastname">
         <p class="error_message" *ngIf="myForm.get('lastname').invalid && (myForm.submitted || myForm.get('lastname').dirty)">Please provide email</p>
         <label>Email</label>
         <input type="text" formControlName="email">
         <p class="error_message" *ngIf="myForm.get('email').hasError('required') && (myForm.submitted || myForm.get('email').dirty)">Please provide email</p>
         <p class="error_message" *ngIf="myForm.get('email').hasError('email') && (myForm.submitted || myForm.get('email').dirty)">Please provide valid email</p>
    </form>

    In my case, I have two types of validation for my form :

    • Html validation : required, maxSize, ... etc.
    • Back validation : For example, invalid account, size of loaded file, ... etc.

    I try to using a directive as mentioned here

    <form [formGroup]="myForm">
         <label>Name</label>
         <input type="text" formControlName="name">
         <div invalidmessage="name">
            <p *invalidType="'required'">Please provide name</p>
         </div>
         <label>Lastname</label>
         <input type="text" formControlName="lastname">
         <div invalidmessage="lastname">
            <p *invalidType="'required'">Please provide lastname</p>
         </div>
         <label>Email</label>
         <input type="text" formControlName="email">
         <div invalidmessage="email">
            <p *invalidType="'required'">Please provide email</p>
            <p *invalidType="'email'">Please provide valid email</p>
         </div>
    </form>

    But even with this solution the code is always duplicated and no ability to handle both types of validation.

    Do you have another approach ? Is use components appropriate in this case ? If yes, how can do it.

    Thank you in advance for your investment.