Skip to content

Instantly share code, notes, and snippets.

@MirzaLeka
Created October 11, 2024 13:52
Show Gist options
  • Save MirzaLeka/3aa4d1ea53eca4f6fa864be6f83c4e1b to your computer and use it in GitHub Desktop.
Save MirzaLeka/3aa4d1ea53eca4f6fa864be6f83c4e1b to your computer and use it in GitHub Desktop.
Angular Reactive Form with Async Validations

Angular Reactive Forms with Validations & Async Validations

<div class="form-wrapper">
<h1>Sign up form example</h1>
<form (submit)="handleSubmit()" [formGroup]="signUpForm">
<input type="text" placeholder="username" formControlName="username" class="form-field-input" />
@if (usernameControl.invalid && (usernameControl.dirty || usernameControl.touched))
{
@if (usernameControl.hasError('required')) {
<p class="error-text">Username is mandatory!</p>
} @else if (usernameControl.hasError('maxlength')) {
<p class="error-text">Username is too long!</p>
} @else if (usernameControl.hasError('minlength')) {
<p class="error-text">Username is too short!</p>
<!-- Async validator 👇 -->
} @else if (usernameControl.hasError('isExistingUser')) {
<p class="error-text">Username is already taken!</p>
}
}
<input type="password" placeholder="password" formControlName="password" class="form-field-input"/>
@if (signUpForm.get('password')?.invalid &&
(signUpForm.get('password')?.dirty || signUpForm.get('password')?.touched))
{
<p class="error-text">Password is mandatory!</p>
}
<input type="text" placeholder="age" formControlName="age" class="form-field-input"/>
@if (ageControl.invalid && (ageControl.dirty || ageControl.touched))
{
@if (ageControl.hasError('min')) {
<p class="error-text">You have to be at least 12 years old!</p>
} @else if (ageControl.hasError('pattern')) {
<p class="error-text">Only numbers allowed!</p>
}
}
<div>
<input type="checkbox" formControlName="rememberMe" /> Remember me?
</div>
<button type="submit" class="button-primary">Sign up</button>
</form>
</div>
.form-wrapper {
margin-top: 9rem;
margin-left: 2rem;
padding: 2.5%;
}
form {
display: block;
flex-direction: column;
width: 250px;
justify-content: space-between;
// border: 2px solid red;
}
.form-field-input {
width: 95.5%;
margin-bottom: 0.5rem;
}
input[type=checkbox] {
margin-bottom: 1rem;
}
button {
width: 100%;
}
.error-text {
color: red;
}
import { Component, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UsersValidator } from './users.validator';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent implements OnInit {
signUpForm!: FormGroup;
constructor(
private fb: FormBuilder,
private readonly usersValidator: UsersValidator
) {}
ngOnInit(): void {
this.signUpForm = this.fb.group({
username: [
'',
[Validators.required, Validators.minLength(3), Validators.maxLength(24)],
[this.usersValidator.userExistsValidator()]
],
password: ['', [Validators.required, Validators.minLength(10)]],
age: ['', [Validators.min(12), Validators.pattern("^[0-9]*$")]], // numbers only
rememberMe: [true]
});
}
get usernameControl(): AbstractControl {
return this.signUpForm.get('username') as AbstractControl;
}
get passwordControl(): AbstractControl {
return this.signUpForm.get('password') as AbstractControl;
}
get ageControl(): AbstractControl {
return this.signUpForm.get('age') as AbstractControl;
}
get rememberMeControl(): AbstractControl {
return this.signUpForm.get('rememberMe') as AbstractControl;
}
handleSubmit() {
const usernameControl = this.signUpForm.get('username') as AbstractControl;
console.log('usernameControl.value :>> ', usernameControl.value);
console.log('usernameControl.valid :>> ', usernameControl.valid);
const isFormValid = this.signUpForm.valid;
console.log('isFormValid :>> ', isFormValid);
if (!isFormValid) {
return;
}
const formValue = this.signUpForm.value;
console.log('formValue :>> ', formValue);
}
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import {
provideHttpClient,
withInterceptorsFromDi,
} from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [AppComponent],
bootstrap: [AppComponent],
imports: [
BrowserModule,
AppRoutingModule,
ReactiveFormsModule,
],
providers: [
provideHttpClient(withInterceptorsFromDi()),
],
})
export class AppModule {}
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
export interface ICheckExistingUser {
isExistingUser: boolean;
}
@Injectable({
providedIn: 'root'
})
export class UsersService {
constructor(private http: HttpClient) {}
checkIfUserExists(username: string): Observable<ICheckExistingUser> {
return this.http.get<ICheckExistingUser>(`http://localhost:3000/api/user/${username}`);
}
}
import { Injectable } from '@angular/core';
import {
ICheckExistingUser,
UsersService,
} from './features/core/services/users.service';
import {
AbstractControl,
AsyncValidatorFn,
ValidationErrors,
} from '@angular/forms';
import { Observable, catchError, map, of, switchMap, timer } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class UsersValidator {
constructor(private usersService: UsersService) {}
userExistsValidator(): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
if (!control.value) {
return of(null);
}
// Start a 300ms timer before calling the API
return timer(300).pipe(
// Switch to API call after debounce
switchMap(() => this.usersService.checkIfUserExists(control.value)),
// Check the validity
map((response: ICheckExistingUser) => (response.isExistingUser ? { isExistingUser: true } : null)),
// Handle the error if any
catchError(() => of(null))
);
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment