Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handle form errors using components Angular - TypeScript

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.

like image 821
L Y E S - C H I O U K H Avatar asked Apr 10 '18 09:04

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


4 Answers

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>
like image 115
JayChase Avatar answered Oct 16 '22 11:10

JayChase


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.

like image 41
pixelbits Avatar answered Oct 16 '22 09:10

pixelbits


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.

like image 3
Aakash Uniyal Avatar answered Oct 16 '22 11:10

Aakash Uniyal


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.

like image 2
Robin Dijkhof Avatar answered Oct 16 '22 11:10

Robin Dijkhof