Reputation: 5080
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 :
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.
Upvotes: 31
Views: 42581
Reputation: 183
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
Upvotes: 0
Reputation: 1
<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>
Upvotes: 0
Reputation: 7168
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;
If we want constant error for any error then it should be like
new FormError('Password is not right')
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.
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.
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.
Upvotes: 1
Reputation: 11525
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>
Upvotes: 30
Reputation: 15271
The best way is to implement custom ControlValueAccessor
s 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>
Upvotes: 2
Reputation: 1559
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.
Upvotes: 3
Reputation: 38171
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.
Upvotes: 1
Reputation: 52847
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.
Upvotes: 7
Reputation: 19843
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>
Upvotes: 1
Reputation: 19288
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.
Upvotes: 2
Reputation: 7733
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>
Upvotes: 1