31

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.

3
  • prideparrot.com/blog/archive/2019/2/… Commented Feb 19, 2019 at 4:44
  • Hello @l-y-e-s-c-h-i-o-u-k-h, I have the same problem with these messages, Do you found a solution? if yes, can you share a working code in angular? Commented Jun 17, 2022 at 2:47
  • 1
    BTW after all I opened this issue in the Angular repo. Hopefully we reach to a better solution. Commented Jun 17, 2022 at 5:44

11 Answers 11

30
+50

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>
Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for you respons. I have a small question : What about if I want to write multiple error messages for 'required' error ? <ng-container *ngIf="errors && errors['required']"> Required name</ng-container> <ng-container *ngIf="errors && errors['required']"> The file is required</ng-container>
You could have a prefix as an input on the validation component which would allow for some customisation. stackblitz.com/edit/angular-bcm51p
this is such a clean way to do it. I love it. Muchos!
@JayChase when I use something like this my page crashes quite a bit. any idea what it could be and how I can fix it? is there also another way to do this?
I opened this issue in the Angular repo. Hopefully we reach to a better solution. @JayChase please contribute in the issue if possible
7

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.

1 Comment

Hi @pixelbits : Thank you for your reply,. How can I check for non-html errors (for my back errors ) with this approach ?
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.

Comments

2

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.

Comments

2

The best way is to implement custom ControlValueAccessors for each type of input, combining <label>, <input> and some tags for displaying error message (in my project I simply use title attribute for this purpose) in a single component.

All value accessors should implement the same interface or extend base abstract class, providing methods to set and clear error message and any other methods which you may want to call from validator directives.

Also, you will need to implement custom validator directives for each validation type (i had to re-implement even required and maxlength), validators must return error objects in uniform way i.e. for email validator {email: "Invalid email address"}. Validator directives can get reference to your control value accessors via injection - @Inject(NG_VALUE_ACCESSOR) controls:AbstractFormComponent<any>[] (usually array with one element, AbstractFormComponent is your base class for accessors), use this reference to set or clear accessor error message.

You can also implement two additional types of validator directives: sync and async, which can receive validator function via @Input i.e. [async]="loginValidatorFn", where loginValidatorFn is defined in component class and returns Observable<ValidationErrors>.

This is real code from our application:

<div class="input" [caption]="'SSN: '" name="ssn" type="text" [(ngModel)]="item.ssn" [async]="memberSsnValidatorFn" required></div>

1 Comment

Thank you for your reply. I find this solution interesting.
1

You could create a custom component ValidationMessagesComponent :

Template :

<p class="error_message" *ngIf="form.get(controlName).hasError('required') && (form.submitted || form.get(controlName).dirty)">Please provide {{controlName}}</p>
<p class="error_message" *ngIf="form.get(controlName).hasError('email') && (form.submitted || form.get(controlName).dirty)">Please provide valid {{controlName}}</p>
...other errors

And with the inputs :

@Input() controlName;
@Input() form;

Then use it like this :

<validation-messages [form]="myForm" controlName="email"></validation-messages>

3 Comments

Thank you for your response. How can i manage 'non-htlm elemnts' with this components ? For my 'back errors' as mentioned in my question. This errors are not in my form.
When the backend call fails, your backend should send a list of error codes that can be interpreted by your custom component. Here is a stackblitz example stackblitz.com/edit/stackoverflow-49750645
Thank you for your reply. I find this solution very interesting and easy to set up, I will try it
1

You can use this repo which has default validation messages and you can customize them as well

example usage will be like this

<form [formGroup]="editorForm" novalidate>
    <label>First Name</label>
    <input formControlName="firstName" type="text">
    <ng2-mdf-validation-message [control]="firstName" *ngIf="!firstName.pristine"></ng2-mdf-validation-message>
</form>

1 Comment

Thank you for your reply. I need to perform validation without using external libraries.
1

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.

Comments

1

Here is some part of code I used in library to generate dynamic forms.

This is FormError.ts which is used to get error and custom messages if we want.

import { AbstractControl } from "@angular/forms";

type ErrorFunction = (errorName: string, error: object) => string;
export type ErrorGetter =
    string | { [key2: string]: string } | ErrorFunction;

export class FormError {
    constructor(private errorGetter?: ErrorGetter) { }
    hasError(abstractControl: AbstractControl) {
        return abstractControl.errors && (abstractControl.dirty || abstractControl.touched);
    }
    getErrorMsgs(abstractControl: AbstractControl): string[] {
        if (!this.hasError(abstractControl))
            return null;
        let errors = abstractControl.errors;
        return Object.keys(errors).map(anyError => this.getErrorValue(anyError, errors[anyError]));
    }
    getErrorValue(errorName: string, error: object): string {
        let errorGetter = this.errorGetter;
        if (!errorGetter)
            return predictError(errorName, error);
        if (isString(errorGetter))
            return errorGetter;
        else if (isErrorFunction(errorGetter)) {
            let errorString = errorGetter(errorName, error);
            return this.predictedErrorIfEmpty(errorString, errorName, error)
        }
        else {
            let errorString = this.errorGetter[errorName];
            return this.predictedErrorIfEmpty(errorString, errorName, error)
        }
    }
    predictedErrorIfEmpty(errorString: string, errorName: string, error: object) {
        if (errorString == null || errorString == undefined)
            return predictError(errorName, error);
        return errorString;
    }


}
function predictError(errorName: string, error: object): string {
    if (errorName === 'required')
        return 'Cannot be blank';
    if (errorName === 'min')
        return `Should not be less than ${error['min']}`;
    if (errorName === 'max')
        return `Should not be more than ${error['max']}`;
    if (errorName === 'minlength')
        return `Alteast ${error['requiredLength']} characters`;
    if (errorName === 'maxlength')
        return `Atmost ${error['requiredLength']} characters`;
    // console.warn(`Error for ${errorName} not found. Error object = ${error}`);
    return 'Error';
}
export function isString(s: any): s is string {
    return typeof s === 'string' || s instanceof String;
}
export function isErrorFunction(f: any): f is ErrorFunction {
    return typeof f === "function";
}

Custom Messages

 class FormError {
    constructor(private errorGetter?: ErrorGetter) { }
    }

Now ErrorGetter is like

type ErrorFunction = (errorName: string, error: object) => string;
type ErrorGetter =
    string | { [key2: string]: string } | ErrorFunction;
  1. If we want constant error for any error then it should be like

    new FormError('Password is not right')

  2. If we want constant error for specific error then it should be like

    new FormError({required:'Address is necessary.'})

    For other errors it will go in predict error.

  3. If we want use function for specific error then it should be like

    new FormError((errorName,errorObject)=>{ if(errorName=='a') return '2';})

    For other errors it will go in predict error.

  4. Modify predictError function according to your need.

FormError component

form-error.html

<ng-container *ngIf="formError.hasError(control)">
  <div class='form-error-message' *ngFor='let error of  formError.getErrorMsgs(control)'>{{error}}</div>
</ng-container>

form-error.scss

form-error {
    .form-error-message {
        color: red;
        font-size: .75em;
        padding-left: 16px;
    }
}

form-error.ts

@Component({
  selector: 'form-error',
  templateUrl: 'form-error.html'
})
export class FormErrorComponent {
  @Input() formError: FromError;
  @Input() control: AbstractControl;
}

Usage

<form-error [control]='thatControl' ></form-error>

Obviously FormError is not the best design. Modify however you like.

2 Comments

There can be some problem as I modified some part of code before posting.
Thank you for your reply. I find this solution interesting, nevertheless, it is more or less complicated for simple forms.
0

<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>

Comments

0

You can use NPM package. Its simple easy to use and customize for both reactive and template driven forms.

Code snippet:

HTML

<form [formGroup]="demoForm">
    <div>
         <label for="name">Name</label>
         <input type="text" formControlName="name" name="name" placeholder="Name validator">
         <tn-form-error [control]="demoForm.controls.name" [field]="'Name'"></tn-form-error>
    </div>
</form>

Component

<p>
 this.demoForm = new FormGroup({
      name: new FormControl(''[Validators.required])
 });

Play around here

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.