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:
1 2 3 4 5 6 7 |
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:
1 2 3 |
setSubscription(notifyBy : string) : void { ... } |
Oraz w HTML:
1 2 3 4 5 |
<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ę:
1 2 3 4 5 6 7 8 9 10 |
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:
1 |
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:
1 2 3 4 5 6 7 8 9 10 |
<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:
1 2 3 4 5 6 7 8 9 10 11 12 |
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.
1 |
<button (click)="setCardValidation()">Add card?</button> |
I nasza metoda setCardValidation():
1 2 3 4 5 6 7 8 9 |
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.
Hej 🙂
1. Napotkałam w kodzie:
const cardGroupControlsKeys = Object.keys(cardGroup.controls);
na taki błądProperty 'controls' does not exist on type 'AbstractControl'.)
. Nie mogę znależć przyczyny problemu :/2. W Twoim plunkerze znalazłam
isCardValidationEnabled: false;
, gdzie tego używasz?Hej:)
isCardValidationEnabled – coś sprawdzałem podczas tworzenia arta i zapomniałem to usunąć, dzięki!:)
Zdarza się, że kompilacja AOT w Angularze ma czasem problemy z .dot notation. Spróbuj:
Object.keys(cardGroup["controls"])
i powiedz czy dalej jest błąd?
Tak jest lepiej 🙂
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(); }); }
Dzięki!
Pingback: ANGULAR 2 MODEL DRIVEN FORMS, cz. III - Angular.love