MODEL DRIVEN FORMS – zmiana zasad walidacji w locie
Jeśli zapoznałeś się już z podstawami Model Driven Forms, to czas na tzw. mięsko! W tym artykule zaprezentuję, jak podczas runtime’u zmieniać zasady walidacji (conditional validation).
Rozpatrzymy przypadek formularza, w którym:
- użytkownik podaje obowiązkowo adres email, oraz jeśli chce, to również numer telefonu
- użytkownik może wybrać, czy chce otrzymywać powiadomienia na maila (domyślnie), czy na telefon
- jeśli wybierze, że na telefon, to pole z numerem staje się obowiązkowe (required).
- jeśli wybierze płatność kartą, wszystkie pola dotyczące karty stają się obowiązkowe
Zanim przejdziemy do dalszej części artykułu, polecam spojrzeć jak wygląda stan od którego zaczynamy:
KLASA ABSTRACT CONTROL
Jeśli zapoznałeś się już z kodem, spójrzmy raz jeszcze, jak wygląda struktura naszego formularzu w kodzie:
this.modelForm = this.formBuilder.group({
firstname: ['', Validators.required],
lastname: ['',[Validators.required, Validators.minLength(3)]],
email: ['', Validators.required],
phone: '',
subscription: 'email'
});
Naszym celem jest dynamiczne dodawanie i usuwanie Validators.required do pola „phone”, jeśli użytkownik zaznaczy lub odznaczy radio button „sms”.
Rozpoczniemy od napisania metody, którą przypniemy na (click) przy każdym radio buttonie:
setSubscription(notifyBy : string) : void {
...
}
Oraz w HTML:
<label>Email</label>
<input type="radio" (click)="setSubscription('email')" value="email" formControlName="subscription">
<label>SMS</label>
<input type="radio" (click)="setSubscription('sms')" value="sms" formControlName="subscription">
Jak słusznie zauważyłeś (bądź nie), formControlName może być identyczny dla wielu inputów. Przejdźmy do implementacji metody setSubscription, która obsłuży nam dynamiczną walidację:
setSubscription(notifyBy : string) : void {
const control = this.modelForm.get('phone');
if (notifyBy === "sms") {
control.setValidators(Validators.required);
} else {
control.clearValidators();
}
control.updateValueAndValidity();
}
Rozkładając powyższy kod na czynniki, znajome już Ci FormGroup oraz FormControl dziedziczą po klasie AbstractControl, która udostępnia nam min. następujące metody:
- setValidators(newValidator) – jako parametr przyjmuje walidator, który chcemy ustawić. Jeśli nasz FormControl posiada już walidatory dodane w formBuilder.group(), zostaną one nadpisane.W przypadku dodania paru walidatorów, jako parametr przekazujemy tablicę z walidatorami. np:
control.setValidators([Validators.required, Validators.minLength(3)])
- setAsyncValidators(newValidator) – j/w, z tym, że dla asnychronicznych walidatorów
- clearValidators() – czyści listę synchronicznych walidatorów
- clearAnsyncValidators() – czyści listę asynchronicznych walidatorów
- updateValueAndValidity({onlySelf? : boolean, emitEvent? : boolean}) – wykonuje re-kalkulację wartości i stanu walidacji FormControla / FormGroupa. Domyślnie update wartości i walidacji dotyczy również ancestorów (czyli FormGroupa, który jest nad FormControlem, lub wszystkich FormGroup’ów w górę, jeśli mamy zagnieżdżone FormGroup). Metoda przyjmuje opcjonalny parametr, przekazać możemy obiekt z dwoma opcjonalnymi właściwościami, w przypadku przekazania onlySelf jako true, refresh walidacji i wartości, będzie dotyczyć tylko danej kontrolki.
Brzmi klarownie. Jednak nie musimy się ograniczać do pojedynczego FormControl.
WALIDACJA W LOCIE GRUPY KONTROLEK
Możemy również dodawać walidację w locie na całe grupy kontrolek. Wyobraźmy sobie przykład, że użytkownik może wybrać płatność kartą, gdzie musi podać kod CVV, datę ważności karty, oraz numer karty. W takim przypadku nie ma sensu ustawiać walidatora na każdy FormControl osobno, tylko musimy zebrać to w grupę, co możemy zrobić w następujący sposób:
<fieldset formGroupName="card">
<label>CVV:</label>
<input name="cvv" formControlName="cvv">
<label>Card number:</label>
<input name="cardNumber" formControlName="cardNumber">
<label>Expiration date:</label>
<input name="expirationDate" formControlName="expirationDate">
</fieldset>
W templatce korzystamy z dyrektywy formGroupName, która wymaga obecności dyrektywy FormGroup. Wrzucamy do środka kontrolki, które mają zostać podpięte pod grupę. Następnie w formBuilderze:
this.modelForm = this.formBuilder.group({
firstname: ['', Validators.required],
lastname: ['',[Validators.required, Validators.minLength(3)]],
email: ['', Validators.required],
phone: '',
subscription: 'email',
card: this.formBuilder.group({
cvv: '',
cardNumber: '',
expirationDate: ''
})
});
Szybko i prosto – wołamy jeszcze raz formBuilder.group(). Na tym etapie, wartość modelu formularza wygląda następująco:

Zagnieżdżony FormGroup, został wrzucony jako obiekt do głównego obiektu z wartościami całego formularza.
Na potrzeby artykułu, walidację całej grupy dodamy po prostu na kliknięcie guzika.
<button (click)="setCardValidation()">Add card?</button>
I nasza metoda setCardValidation():
setCardValidation() : void {
const cardGroup = this.modelForm.controls['card'];
const cardGroupControlsKeys = Object.keys(cardGroup.controls);
cardGroupControlsKeys.forEach((key) => {
cardGroup.controls[key].setValidators(Validators.required);
cardGroup.controls[key].updateValueAndValidity();
});
}
Każdy formGroup posiada property controls, które zawiera wszystkie zagnieżdzone kontrolki (formControls).
W powyższym kodzie przeiterowaliśmy po każdym property obiektu card (czyli po każdej kontrolce grupy card), i ustawiliśmy walidację na required.
Dzięki takiemu trickowi, możesz dowolnej grupie przestawić wszystkie pola dynamicznie na required, zamiast np. wołać X razy setValidators na każdym polu, dublując kod.
Plunker podsumowujący artykuł:
PODSUMOWANIE
Wykorzystanie takich metod jak setValidators oraz clearValidators, jest bardzo przydatne w przypadku formularzy, gdzie użytkownik posiada wiele scenariuszy wypełniania formularza (np. użytkownik wybiera, że chce fakturę na firmę – automatycznie dodajemy walidację do pól związanych z firmą, NIP etc.). Więc warto pamiętać, że Angular nam to umożliwia.