Skip to content

Instantly share code, notes, and snippets.

@MirzaLeka
Last active March 31, 2024 02:05
Show Gist options
  • Save MirzaLeka/a4baf194d25c0cfb0243603efa5c7b87 to your computer and use it in GitHub Desktop.
Save MirzaLeka/a4baf194d25c0cfb0243603efa5c7b87 to your computer and use it in GitHub Desktop.
Reactive Validation Triggers in Angular Forms

Reactive Validation Triggers in Angular Forms

Learn how to enhance Angular form validations using Reactive Forms and a few gimmicks.

Demos

Input + Time based validation trigger

Form submit triggers errors to display

<app-todo-form></app-todo-form>
import { NgModule } from '@angular/core';
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 { }
<form [formGroup]="todoForm" (ngSubmit)="onSubmit()">
<h2>Create new todo</h2>
<!-- Title -->
<div class="title-wrapper">
<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>
<!-- Description -->
<div class="description-wrapper">
<label>Description</label>
<textarea formControlName="description"></textarea>
<div class="error-message" *ngIf="formFieldCanBeValidated['description'] && descriptionControl.invalid && (descriptionControl.dirty || descriptionControl.touched)">
Maximum number of characters is 300!
</div>
</div>
<button class="submit-btn" type="submit">Submit</button>
</form>
form {
display: flex;
flex-direction: column;
width: 500px;
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
padding: 5%;
margin: 5%;
}
.title-wrapper {
width: 100%;
margin-bottom: 2rem;
& > input {
width: 100%;
border-radius: 5px;
border: 1px solid #007BFF;
outline: none;
height: 20px;
padding: 0 5px;
box-sizing: border-box;
height: 30px;
}
}
.description-wrapper {
width: 100%;
& > textarea {
width: 100%;
border-radius: 5px;
border: 1px solid #007BFF;
outline: none;
resize: none;
height: 80px;
box-sizing: border-box;
}
}
.submit-btn {
cursor: pointer;
background: #007BFF;
color: #FFF;
margin-top: 20px;
height: 30px;
border: none;
&:hover {
background: #2890fe;
}
}
.error-message {
color: red;
}
import { Component, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
FormBuilder,
FormGroup,
Validators,
} from '@angular/forms';
import { debounceTime, Subject, takeUntil, tap } from 'rxjs';
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, OnDestroy {
todoForm!: FormGroup;
private readonly unsubscribed$ = new Subject<void>();
formFieldCanBeValidated = {
[FormFields.Title]: true,
[FormFields.Description]: true,
};
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.setupForm();
this.toggleValidationRules(FormFields.Title);
this.toggleValidationRules(FormFields.Description);
}
ngOnDestroy(): void {
this.unsubscribed$.next();
this.unsubscribed$.complete();
}
private toggleValidationRules(field: FormFields) {
this.todoForm
.get(field)
?.valueChanges.pipe(
// clear validation as soon you start typing
tap(() => (this.formFieldCanBeValidated[field] = false)),
// hold for 500ms after user stopped typing
debounceTime(500),
takeUntil(this.unsubscribed$)
)
.subscribe(() => (this.formFieldCanBeValidated[field] = true));
}
get titleControl(): AbstractControl {
return this.todoForm.get(FormFields.Title) as AbstractControl;
}
get descriptionControl(): AbstractControl {
return this.todoForm.get(FormFields.Description) as AbstractControl;
}
private setupForm(): void {
this.todoForm = this.fb.group({
[FormFields.Title]: [
'',
[
Validators.required,
Validators.minLength(3),
Validators.maxLength(100),
Validators.pattern(new RegExp(/^[a-zA-Z\s]+$/)),
],
],
[FormFields.Description]: ['', [Validators.maxLength(300)]],
});
}
private triggerValidationOnSubmit(): void {
Object.keys(this.todoForm.controls).forEach((field: string) => {
const control = this.todoForm.get(field);
control?.markAsTouched({ onlySelf: true });
});
}
onSubmit(): void {
console.log(this.todoForm.value);
if (!this.todoForm.valid) {
this.triggerValidationOnSubmit();
return;
}
console.log('done');
}
}
import { NgModule } from '@angular/core';
import { TodoFormComponent } from './todo-form.component';
import { ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [
TodoFormComponent
],
imports: [
CommonModule,
ReactiveFormsModule
],
exports: [
TodoFormComponent
]
})
export class TodoFormModule { }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment