Az Angular reaktív formok az egyik legerősebb eszköz a komplex űrlapok kezelésére és validálására. A beépített validátorok, mint például a Validators.required
, Validators.minLength
vagy Validators.pattern
, alapvető funkciókat biztosítanak, de mi történik, ha az üzleti logikánk ennél sokkal összetettebb? Mi van, ha speciális, testreszabott szabályokra van szükségünk, amelyek túlmutatnak a standard ellenőrzéseken? Itt lépnek képbe az egyedi validátorok, amelyekkel teljes kontrollt szerezhetünk az űrlapok adatainak ellenőrzése felett. Ebben a cikkben részletesen bemutatjuk, hogyan hozhatunk létre szinkron és aszinkron egyedi validátorokat, hogyan kezelhetjük a paramétereket, a kereszmező validációt, és hogyan építhetünk ki robusztus validációs rendszereket az Angular alkalmazásainkban.
Miért Van Szükség Egyedi Validátorokra?
A beépített Angular validátorok kiváló kiindulópontot jelentenek, de gyakran szembesülünk olyan forgatókönyvekkel, amelyek speciálisabb logikát igényelnek:
- Komplex üzleti szabályok: Például egy jelszó nem csak egy minimális hosszt kell, hogy teljesítsen, hanem tartalmaznia kell nagybetűt, kisbetűt, számot és speciális karaktert is. Vagy egy telefonszám formátuma régiónként eltérő lehet.
- Kereszmező validáció: Amikor egy mező érvényessége egy másik mező értékétől függ. Tipikus példa a jelszó és jelszó megerősítése, ahol a két mezőnek meg kell egyeznie. Hasonlóan, egy „kezdeti dátum” mezőnek korábbinak kell lennie, mint egy „befejezési dátum” mezőnek.
- Aszinkron validáció: Amikor az ellenőrzéshez szerveroldali adatokra van szükség. Például egy felhasználónévnek vagy e-mail címnek egyedinek kell lennie az adatbázisban, amit egy API hívással tudunk ellenőrizni.
- Dinamikus szabályok: A validációs szabályok futásidőben változnak bizonyos feltételek alapján.
Ezekben az esetekben az egyedi validátorok nyújtanak rugalmas és hatékony megoldást, lehetővé téve számunkra, hogy bármilyen ellenőrzési logikát implementáljunk, ami alkalmazásunk igényeihez igazodik.
A Validátorok Működési Elve Angularban
Az Angularban egy validátor alapvetően egy függvény, ami egy AbstractControl
példányt vesz paraméterül, és visszatér egy ValidationErrors
objektummal (ha az ellenőrzés sikertelen) vagy null
értékkel (ha sikeres).
import { AbstractControl, ValidationErrors, ValidatorFn, AsyncValidatorFn } from '@angular/forms';
// Szinkron validátor függvény aláírása
export type CustomValidatorFn = (control: AbstractControl) => ValidationErrors | null;
// Aszinkron validátor függvény aláírása
export type CustomAsyncValidatorFn = (control: AbstractControl) => Promise | Observable;
AbstractControl
: Ez egy absztrakt osztály, amely aFormControl
,FormGroup
ésFormArray
osztályok közös őse. Ez biztosítja, hogy a validátoraink rugalmasan alkalmazhatók legyenek bármilyen űrlapvezérlőn.ValidationErrors
: Egy objektum, ami kulcs-érték párokat tartalmaz. A kulcs általában a hiba típusa (pl.'noSpaces'
,'passwordStrength'
), az érték pedig tetszőleges adat, ami a hiba részleteit írja le (pl.{ requiredStrength: 'medium' }
). Ha a validáció sikeres,null
-t adunk vissza.
A szinkron validátorok azonnal visszaadják az eredményt, míg az aszinkron validátorok Promise
vagy Observable
segítségével, nem blokkoló módon, a jövőben adnak vissza értéket. Ez utóbbi különösen hasznos hálózati kérések esetén.
Szinkron Egyedi Validátor Létrehozása
Kezdjük egy egyszerű szinkron validátorral, ami ellenőrzi, hogy egy szövegmező nem tartalmaz-e szóközt.
Alapok: Egyszerű Validátor
Tegyük fel, hogy van egy felhasználónév mezőnk, és nem szeretnénk, ha szóközt tartalmazna:
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function noSpacesValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const isWhitespace = (control.value || '').includes(' ');
const isValid = !isWhitespace;
return isValid ? null : { 'noSpaces': true };
};
}
Magyarázat:
- A
noSpacesValidator
egy „factory” függvény. Ez azt jelenti, hogy egyValidatorFn
típusú függvényt ad vissza. Ez a minta lehetővé teszi, hogy később paramétereket is adjunk át a validátorunknak. - A visszatérő függvény (maga a validátor) kapja meg az
AbstractControl
objektumot. - Ellenőrizzük, hogy a vezérlő értéke tartalmaz-e szóközt. Ha igen, egy
'noSpaces': true
objektumot adunk vissza, jelezve a hibát. Ha nem, akkornull
-t, jelezve, hogy a vezérlő érvényes.
Használat a FormGroupban
Ezt a validátort könnyen hozzáadhatjuk egy FormControl
-hoz:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { noSpacesValidator } from './validators/custom-validators'; // Feltételezve, hogy itt van a validátor
@Component({
selector: 'app-user-form',
template: `
`,
styleUrls: ['./user-form.component.css']
})
export class UserFormComponent implements OnInit {
userForm!: FormGroup;
get usernameControl() {
return this.userForm.get('username')!;
}
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.userForm = this.fb.group({
username: ['', [Validators.required, noSpacesValidator()]]
});
}
onSubmit(): void {
if (this.userForm.valid) {
console.log('Form Submitted!', this.userForm.value);
} else {
console.log('Form is invalid.');
}
}
}
Validátor Paraméterekkel: Magasabb Rendű Függvények
Mi van, ha a validátorunknak konfigurációs adatokra van szüksége? Például egy mezőnek egy adott előtaggal kell kezdődnie. Ehhez a „magasabb rendű függvény” mintát használjuk, ahol a külső függvény veszi át a paramétereket, és visszaadja a tényleges validátor függvényt.
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function startsWithValidator(prefix: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (!control.value) {
return null; // Ha nincs érték, ne validáljuk ezt a szabályt (más validátorok tehetik)
}
const startsWithPrefix = (control.value as string).startsWith(prefix);
return startsWithPrefix ? null : { 'startsWith': { requiredPrefix: prefix, actualValue: control.value } };
};
}
Használat:
// ... a FormBuilder-ben ...
this.userForm = this.fb.group({
productId: ['', [Validators.required, startsWithValidator('PROD-')]]
});
// ... a sablonban ...
A termékazonosítónak 'PROD-' előtaggal kell kezdődnie.
Figyeljük meg, hogy a hibajelző objektumba további információt is belefoglaltunk (requiredPrefix
, actualValue
), amit később felhasználhatunk a hibaüzenet finomítására.
Aszinkron Egyedi Validátor Létrehozása
Az aszinkron validátorok akkor szükségesek, amikor a validációhoz időigényes műveletekre van szükség, például API hívásra. Ezek Promise
vagy Observable
objektumot adnak vissza. Az Angular figyeli ezeket a „folyamatban lévő” validátorokat, és egy pending
állapotot állít be az AbstractControl
-on, amíg az eredmény meg nem érkezik.
Példa: Egyedi Felhasználónév Ellenőrzése
Tegyük fel, hogy ellenőrizni szeretnénk, hogy egy felhasználónév már foglalt-e az adatbázisban.
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, of, timer } from 'rxjs';
import { map, switchMap, debounceTime, distinctUntilChanged, take } from 'rxjs/operators';
// Ez egy szimulált szolgáltatás, ami ellenőrzi, foglalt-e a felhasználónév
// Valós alkalmazásban ez egy HttpClient hívás lenne.
const USERNAMES_TAKEN = ['admin', 'moderator', 'guest'];
function isUsernameTaken(username: string): Observable {
// Szimulálunk egy szerveroldali hívást, ami 500 ms-ig tart.
return timer(500).pipe(
map(() => USERNAMES_TAKEN.includes(username.toLowerCase()))
);
}
export function uniqueUsernameValidator(): AsyncValidatorFn {
return (control: AbstractControl): Promise | Observable => {
if (!control.value) {
return of(null);
}
// Fontos: debounceTime és distinctUntilChanged a teljesítményért
// Csak a felhasználói beviteli szünet után indítjuk az ellenőrzést,
// és csak akkor, ha az érték valóban megváltozott.
return timer(500).pipe( // Adjunk egy kis debounce időt az induláshoz
switchMap(() => isUsernameTaken(control.value)),
map(isTaken => (isTaken ? { 'uniqueUsername': true } : null)),
take(1) // Csak egy értéket figyeljünk, utána fejezzük be
);
};
}
Magyarázat:
- A
uniqueUsernameValidator
szintén egy factory függvény, amely visszaad egyAsyncValidatorFn
-t. - A validátor függvényen belül ellenőrizzük, hogy van-e érték. Ha nincs,
of(null)
-lal azonnal visszatérünk (Observable
csomagolásban). - A
timer(500)
és aswitchMap
kombinációja biztosítja adebounceTime
funkcionalitást: a tényleges ellenőrzést csak akkor indítjuk el, ha a felhasználó 500 ms-ig nem írt. - Az
isUsernameTaken
(szimulált) szolgáltatás hívását aswitchMap
operátorral végezzük el. Ez megszakítja az előző, még be nem fejezett API hívást, ha a felhasználó időközben újra ír. - Az eredményt a
map
operátorral alakítjuk átValidationErrors
objektummá vagynull
-ra. - A
take(1)
biztosítja, hogy azObservable
lezáruljon az első érték után, ami fontos a memóriakezelés és az erőforrások szempontjából.
Aszinkron Validátor Használata
Az aszinkron validátorokat a FormBuilder.group()
harmadik paramétereként adjuk meg:
// ...
import { uniqueUsernameValidator } from './validators/custom-validators';
// ...
export class UserFormComponent implements OnInit {
userForm!: FormGroup;
get usernameControl() {
return this.userForm.get('username')!;
}
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.userForm = this.fb.group({
username: ['', [Validators.required], [uniqueUsernameValidator()]] // Harmadik paraméter az aszinkron validátoroknak
});
}
// ...
}
A sablonban ellenőrizhetjük a pending
állapotot is, hogy jelezzük a felhasználónak, hogy a validáció folyamatban van:
Keresztmező Validáció (Cross-Field Validation)
Amikor több mező értéke függ egymástól, a validátort a FormGroup
szintjére kell alkalmazni, nem az egyes FormControl
-okra. A validátor ekkor a teljes FormGroup
-ot kapja meg AbstractControl
-ként, és hozzáférhet annak minden gyermekvezérlőjéhez.
Példa: Jelszó és Jelszó megerősítése egyezés
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function passwordMatchValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const password = control.get('password');
const confirmPassword = control.get('confirmPassword');
if (!password || !confirmPassword || !password.value || !confirmPassword.value) {
return null; // Ha valamelyik mező üres, nem ez a validátor a felelős érte
}
if (password.value !== confirmPassword.value) {
// Hozzáadjuk a hibát a confirmPassword mezőhöz
confirmPassword.setErrors({ 'passwordMismatch': true });
return { 'passwordMismatch': true }; // Jelezzük a FormGroup számára is a hibát
} else if (confirmPassword.hasError('passwordMismatch')) {
// Ha korábban volt hiba, de most már egyeznek, töröljük a hibát
confirmPassword.setErrors(null);
}
return null; // Minden rendben
};
}
Magyarázat:
- A validátor a
FormGroup
-ot kapja meg, így acontrol.get('mezőnév')
segítségével hozzáférhet a gyermekvezérlőkhöz. - Fontos, hogy ne csak a
FormGroup
-nak adjunk vissza hibát, hanem a releváns gyermekmezőnek is, jelen esetben aconfirmPassword
-nak, asetErrors()
metódussal. Ez biztosítja, hogy a hibaüzenetek a megfelelő helyen jelenjenek meg a sablonban. - Ha a feltétel (nem egyezés) már nem teljesül, de a
confirmPassword
még hibás, akkor asetErrors(null)
-lal törölnünk kell a hibát.
Keresztmező Validátor Használata
Ezt a validátort a FormGroup
második paramétereként adjuk meg:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { passwordMatchValidator } from './validators/custom-validators';
@Component({
selector: 'app-password-form',
template: `
`,
styleUrls: ['./password-form.component.css']
})
export class PasswordFormComponent implements OnInit {
passwordForm!: FormGroup;
get passwordControl() { return this.passwordForm.get('password')!; }
get confirmPasswordControl() { return this.passwordForm.get('confirmPassword')!; }
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.passwordForm = this.fb.group({
password: ['', [Validators.required, Validators.minLength(6)]],
confirmPassword: ['', [Validators.required]]
}, { validators: passwordMatchValidator() }); // Itt adjuk hozzá a FormGroup szintű validátort
}
onSubmit(): void {
if (this.passwordForm.valid) {
console.log('Password Form Submitted!', this.passwordForm.value);
} else {
console.log('Password Form is invalid.');
}
}
}
Egyedi Hibakezelés és Üzenetek
Az egyedi validátorokkal definiált hibák kezelése a sablonban ugyanúgy történik, mint a beépített validátorok esetében, a control.errors?.['hibakód']
használatával. Ha a hibánk paramétereket is tartalmaz, azokat is elérhetjük:
A termékazonosítónak '{{ productIdControl.errors?.['startsWith'].requiredPrefix }}' előtaggal kell kezdődnie.
A megadott érték: {{ productIdControl.errors?.['startsWith'].actualValue }}.
Ez lehetővé teszi, hogy dinamikus és felhasználóbarát hibaüzeneteket jelenítsünk meg.
Bevált Gyakorlatok és Tippek
Ahhoz, hogy az egyedi validátoraink hatékonyak, karbantarthatóak és jól szervezettek legyenek, érdemes betartani néhány bevált gyakorlatot:
- Újrafelhasználhatóság: Helyezzük az egyedi validátorokat egy különálló fájlba (pl.
src/app/validators/custom-validators.ts
), hogy könnyen importálhatók és újra felhasználhatók legyenek az alkalmazás különböző részein. - Tisztaság és olvashatóság: Adjunk értelmes neveket a validátor függvényeknek és a hibakódoknak. Ez megkönnyíti a hibák azonosítását és az űrlap működésének megértését.
- Tesztelhetőség: Az egyedi validátorok tiszta, izolált függvények, ami megkönnyíti az unit tesztelésüket. Ellenőrizzük az összes lehetséges kimenetet (
null
, különböző hibaobjektumok). - Teljesítmény (aszinkron validátoroknál):
- Mindig használjunk
debounceTime
ésdistinctUntilChanged
operátorokat azObservable
alapú aszinkron validátoroknál. Ez megakadályozza a túlzott API hívásokat minden billentyűleütéskor, és csak akkor indítja el az ellenőrzést, ha a felhasználó rövid időre megáll a gépelésben, vagy ha az érték valóban megváltozott. - A
switchMap
használata biztosítja, hogy a korábbi (még folyamatban lévő) kérések megszakadjanak, ha újabb kérés érkezik, elkerülve a versenyhelyzeteket és a felesleges feldolgozást.
- Mindig használjunk
- Hibakezelés: Gondoskodjunk róla, hogy a
ValidationErrors
objektumokban lévő információk elegendőek legyenek a hibaüzenetek pontos megjelenítéséhez. - Típusbiztonság: Használjuk a TypeScript adta lehetőségeket a validátorok bemeneti és kimeneti típusainak pontos definiálására, növelve a kód robusztusságát.
- Minimalista logika: A validátoroknak egyetlen felelősségük legyen: az adott szabály ellenőrzése. A hibaüzenetek megjelenítése a sablon feladata.
Összefoglalás
Az egyedi validátorok létrehozásának képessége az Angular reaktív formok egyik legértékesebb tulajdonsága. Lehetővé teszik számunkra, hogy bármilyen komplex validációs logikát implementáljunk, legyen szó szinkron vagy aszinkron ellenőrzésről, paraméterek kezeléséről vagy több mező közötti függőségekről. A megfelelő tervezéssel és a bevált gyakorlatok betartásával robusztus, felhasználóbarát és könnyen karbantartható űrlapokat építhetünk, amelyek pontosan megfelelnek az alkalmazásunk egyedi igényeinek. Ne habozzon kihasználni ezt az erőt, hogy űrlapjai ne csak funkcionálisak, hanem intelligensek is legyenek!
Leave a Reply