Handle form errors using components Angular - TypeScript

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.


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.


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>

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.