Have you ever wanted to create a form that tracks user behavior and automatically manages validations? With the use of Angular Reactive forms, you can do just that. This blog will teach you how to:
Create a form from scratch
keep a field clean before it is interacted with
display validations on touch
clear validations while the user is typing
if invalid, trigger validation as soon as the user stops typing
trigger validations in case the user clicks on the submit button before interacting with mandatory fields
Setup
Creating a new Angular project
Create a new module and a new component.
ng g c todos/todo-form
Creating Modules
Todo Form Module
Add the Reactive Forms Module to your module file:
declarations: [
TodoFormComponent
],
imports: [
CommonModule,
ReactiveFormsModule
],
exports: [
TodoFormComponent
]
})
export class TodoFormModule { }
Add Todo Form Module to the App Module
app.module.ts
import { BrowserModule } from ‘@angular/platform-browser‘;
import { AppComponent } from ‘./app.component‘;
import { TodoFormModule } from ‘./todos/todo-form/todo-form.module‘;
@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
TodoFormModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Now you should be able to use a todo form in App Component.
app.component.ts
Creating form skeleton
The form contains two fields title and description.
todo-form.component.ts
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 {
}
}
todo-form.component.html
<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>
Validations
Adding Form Validations
The todo title has several validations:
it’s required
has a minimum length of characters
has a maximum length of 100 characters
must use only letters (Regex)
The description field has only length validation – up to 300 characters allowed.
this.todoForm = this.fb.group({
[FormFields.Title]: [
”,
[
Validators.required,
Validators.minLength(3),
Validators.maxLength(100),
Validators.pattern(new RegExp(/^[a-zA-Zs]+$/)),
],
],
[FormFields.Description]: [”, [Validators.maxLength(300)]],
});
}
Create Getters
Create a getter for each form control to access it in the template easily:
return this.todoForm.get(FormFields.Title) as AbstractControl;
}
get descriptionControl(): AbstractControl {
return this.todoForm.get(FormFields.Description) as AbstractControl;
}
Applying Validations in the template
todo-form.component.html
<div class=“title-wrapper”>
<label>Title</label>
<input formControlName=“title” type=“text” />
<div class=“error-message” *ngIf=“titleControl.invalid && (titleControl.dirty || titleControl.touched)”>
<div *ngIf=“titleControl.hasError(‘required’)”>Title is required</div>
<div *ngIf=“titleControl.hasError(‘maxlength’)”>Title is too long</div>
<div *ngIf=“titleControl.hasError(‘minlength’)”>Title is too short</div>
<div *ngIf=“titleControl.hasError(‘pattern’)”>Title contains numbers or symbols</div>
</div>
</div>
<!– Description –>
<div class=“description-wrapper”>
<label>Description</label>
<textarea formControlName=“description”></textarea>
<div class=“error-message” *ngIf=“descriptionControl.invalid && (descriptionControl.dirty || descriptionControl.touched)”>
Maximum number of characters is 300!
</div>
</div>
Clarifications
To prevent errors from popping up as soon as the page loads, you want to add the following line as a prerequisite:
What it does is validate field behavior.
If control validation failed, control.invalid will be true
If the control is dirty or touched means the user either clicked or started typing on input
Once the user interacts with the form and the form is in an invalid state, then you proceed with validating each validation rule set in the TypeScript component file:
Component
Template
Validators.required
titleControl.hasError(‘required’)
Validators.minLength(3)
titleControl.hasError(‘minlength’)
Validators.maxLength(100)
titleControl.hasError(‘maxlength’)
Validators.pattern(new RegExp(…))
titleControl.hasError(‘pattern’)
And display an appropriate error for each.
Trigger validations after the user stops typing
Create a dictionary-like structure that holds form fields.
todo-form.component.ts
[FormFields.Title]: true,
[FormFields.Description]: true
}
The FormGroup (TodoForm) exposes a valueChanges property that is available on each form control. The valueChanges returns an Observable, that you can pipe operators to and then subscribe.
In this case, use the debounceTime(n) operator that emits the Observable only after the n number of milliseconds passed.
In between, toggle field state via this.formFieldCanBeValidated[field] using the tap() operator and the subscribe() function.
Now create a function that toggles validation rules when a user starts and stops typing.
todo-form.component.ts
private toggleValidationRules(field: FormFields) {
this.todoForm.get(field)?.valueChanges
.pipe(
// clear validation as soon the user starts typing
tap(() => this.formFieldCanBeValidated[field] = false),
// hold for 500ms after user stopped typing
debounceTime(500),
)
// set validation when user stops
.subscribe(() => this.formFieldCanBeValidated[field] = true)
}
Call the function above in ngOnInit for each form field
this.setupForm();
this.toggleValidationRules(FormFields.Title)
this.toggleValidationRules(FormFields.Description)
}
Finally, use formFieldCanBeValidated in the template.
todo-form.component.html
<label>Title</label>
<input formControlName=“title” type=“text” />
<div class=“error-message” *ngIf=“formFieldCanBeValidated[‘title’] && titleControl.invalid && (titleControl.dirty || titleControl.touched)”>
<div *ngIf=“titleControl.hasError(‘required’)”>Title is required</div>
<div *ngIf=“titleControl.hasError(‘maxlength’)”>Title is too long</div>
<div *ngIf=“titleControl.hasError(‘minlength’)”>Title is too short</div>
<div *ngIf=“titleControl.hasError(‘pattern’)”>Title contains numbers or symbols</div>
</div>
</div>
This ensures that validation and error messages in the template are displayed only when a user stops using the form field for 500ms or in other words, when formFieldCanBeValidated[‘title’] = true.
Prevent form submit
Using Reactive forms you can prevent form submission if the form is in an invalid state. You can verify the form validity using the valid property on the FormGroup (TodoForm).
todo-form.component.ts
// will not pass this line if there is any error on the form
if (!this.todoForm.valid) {
return
}
// read form values
console.log(this.todoForm.values);
// {title: ‘Hello’, description: ‘World’}
}
A common practice is to disable the submit button until the form is in a valid state.
<button class=“submit-btn” type=“submit” [disabled]=“!todoForm.valid”>Submit</button>
However, you can trigger validation rules to display on UI on submit if the user hasn’t interacted with the form.
Create a function that validates all form fields and marks them as touched:
todo-form.component.ts
Object.keys(this.todoForm.controls).forEach(field => {
const control = this.todoForm.get(field);
control.markAsTouched({ onlySelf: true });
});
}
Apply the previous function in the onSubmit() call.
onSubmit(): void {
// will not pass this line if there is any error on the form
if (!this.todoForm.valid) {
this.triggerValidationOnSubmit();
return;
}
// … do other stuff
Now, when a user hits the submit button ahead of time, it will display errors for all invalid fields.
Clean Observables
Finally, unsubscribe from all active Observables to prevent memory leaks.
1) Create a subject to track Observables
todo-form.component.ts
2) Put the subject inside the pipe
this.todoForm.get(field)?.valueChanges
.pipe(
tap(() => this.formFieldCanBeValidated[field] = false),
debounceTime(500),
takeUntil(this.unsubscribed$) // <– subject to unsubscribe
)
.subscribe(() => this.formFieldCanBeValidated[field] = true)
}
3) Unsubscribe once the component is destroyed
…
ngOnDestroy(): void {
this.unsubscribed$.next();
this.unsubscribed$.complete();
}
That’s all from me today.
If you’d like to learn more be sure to check out my other blog on Medium and follow me on Twitter to stay up to date with my content updates.
Get Full Code
Bye for now 👋