Created
October 11, 2024 13:52
-
-
Save MirzaLeka/3aa4d1ea53eca4f6fa864be6f83c4e1b to your computer and use it in GitHub Desktop.
Angular Reactive Form with Async Validations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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