This document will teach you how to validate from control in Angular using Reactive Forms. Start by creating a simple form:
@NgModule({
declarations: [
TodoFormComponent
],
imports: [
CommonModule,
ReactiveFormsModule
],
exports: [
TodoFormComponent
]
})
export class TodoFormModule { }
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
enum FormFields {
Title = 'title',
Description = 'description'
}
@Component({
selector: 'app-todo-form',
templateUrl: './todo-form.component.html',
styleUrls: ['./todo-form.component.scss'],
})
export class TodoFormComponent implements OnInit {
todoForm!: FormGroup;
constructor(
private fb: FormBuilder
) {}
ngOnInit(): void {
this.setupForm();
}
// form structure
private setupForm(): void {
this.todoForm = this.fb.group({
[FormFields.Title]: [''], // default values
[FormFields.Description]: ['']
});
}
onSubmit(): void {
}
}
<form [formGroup]="todoForm" (ngSubmit)="onSubmit()">
<div class="title-wrapper">
<input formControlName="title" type="text" />
</div>
<div class="description-wrapper">
<textarea formControlName="description"></textarea>
</div>
<!-- This button invokes onSubmit() method upon clicking -->
<button class="submit-btn" type="submit">Submit</button>
</form>
private setupForm(): void {
this.todoForm = this.fb.group({
[FormFields.Title]: ['', [
Validators.required, // field is mandatory
Validators.maxLength(100), // field must not exceed 100 characters
Validators.pattern(new RegExp('^\\p{L}+$', 'u'))] // field must follow this pattern (only letters allowed)
],
[FormFields.Description]: ['', [Validators.maxLength(300)]]
});
}
Create getter to retrieve from control that you'll use in the template
get titleControl(): AbstractControl {
return this.todoForm.get(FormFields.Title) as AbstractControl;
}
Apply validation checks in template
<div class="title-wrapper">
<input formControlName="title" type="text" />
<div class="error-message" *ngIf="titleControl.invalid && (titleControl.dirty || titleControl.touched)">
<div *ngIf="titleControl.hasError('required')">String is required</div>
<div *ngIf="titleControl.hasError('maxlength')">String is too long</div>
<div *ngIf="titleControl.hasError('pattern')">String contains numbers, symbols or spaces</div>
</div>
</div>
titleControl.invalid && (titleControl.dirty || titleControl.touched)
- If control validation failed,
control.invalid
will betrue
- If control is
dirty
ortouched
means user either clicked or started typing on input - You want to display validation error when form is invalid only after user interacted with the field
<div *ngIf="titleControl.hasError('required')">String is required</div>
<div *ngIf="titleControl.hasError('maxlength')">String is too long</div>
<div *ngIf="titleControl.hasError('pattern')">String contains numbers, symbols or spaces</div>
These three display an error for each validation parameter that was set in the component
Create a dictionary-like structure that holds form fields.
formFieldCanBeValidated = {
[FormFields.Title]: true,
[FormFields.Description]: true
}
Create a function that will toggle validation rules when user starts and stops typing
private readonly unsubscribed$ = new Subject<void>();
// this will enable/disable validation for each field (title or description)
private toggleValidationRules(field: FormFields) {
this.todoForm.get(field)?.valueChanges
.pipe(
tap(() => this.formFieldCanBeValidated[field] = false), // clear validation as soon the user starts typing
debounceTime(333), // hold for 333ms after user stopped typing
takeUntil(this.unsubscribed$) // subject to unsubscribe
)
.subscribe(() => this.formFieldCanBeValidated[field] = true) // set validation when user stops
}
Call function above in ngOnInit for each form field
ngOnInit(): void {
this.setupForm();
this.toggleValidationRules(FormFields.Title)
this.toggleValidationRules(FormFields.Description)
}
Apply this in the template
<div class="title-wrapper">
<input formControlName="title" type="text" />
<div class="error-message" *ngIf="formFieldCanBeValidated['title'] && titleControl.invalid && (titleControl.dirty || titleControl.touched)">
<div *ngIf="titleControl.hasError('required')">String is required</div>
<div *ngIf="titleControl.hasError('maxlength')">String is too long</div>
<div *ngIf="titleControl.hasError('pattern')">String contains numbers, symbols or spaces</div>
</div>
</div>
Now the validation message will be displayed only after you stop typing (debounce expires).
Using the previously created unsubscribed$ Subject, implement OnDestroy hook and use it to terminate the Subject.
export class TodoFormComponent implements OnInit, OnDestroy
...
ngOnDestroy(): void {
this.unsubscribed$.next();
this.unsubscribed$.complete();
}
This can be done either in template or the component. Here is how you apply it in the component:
onSubmit(): void {
// user will not pass this line if there is any error on the form
if (!this.todoForm.valid) {
return
}
// ... do other stuff
}
You can read form values using .value
flag. This returns an object, where each field is key and value is field value.
console.log(this.todoForm.value)
// {title: 'Hello', description: 'World'}