Wróć do strony głównej

ANGULAR 2 Custom Validators

W poprzednich artykułach dotyczących Model Driven Forms, pokazałem jak korzystać z wbudowanej walidacji, takiej jak Required lub MinLength. W artykule custom validators omówię następujące kwestie:

  • budowa własnego walidatora dla pojedynczego FormControl / ngModel
  • budowa własnego walidatora dla całego FormGroup / ngModelGroup
  • walidatory z parametrami
  • użycie własnych walidatorów na widoku oraz w klasie

CUSTOM VALIDATORS

Zapewne większość czytelników orientuje się do czego można wykorzystać własne walidatory. Dla mniej wtajemniczonych czytelników, podam parę przykładów:

1. Walidacja pojedynczej wartości (FormControl lub ngModel). Walidacja:

  • wszelkich liczb z użyciem wyrażeń regularnych (IP, telefonu, karty kredytowej, peselu, nr dowodu, kod pocztowy etc., w dalszej części kodu pokażę jak sprawdzić czy input jest liczbą)
  • wartości, min, max, range
  • wartości, która musi się rozpoczynać od określonego znaku (przykład, który omówię)

2. Walidacja grupy (FormGroup lub ngModelGroup)

  • zgodność adresu email
  • czy przynajmniej jedno pole jest wypełnione, typowy przypadek dla filtrów (przykład, który omówię)
  • walidacja adresu jako grupy, zamiast każdego inputa osobno

Efekt, który osiągniemy:

Skoro już wiemy, w czym mogą nam pomóc własne walidatory, czas przejść do konkretów. Jeśli chcesz, możesz najpierw spojrzeć na kod:

LIVE EXAMPLE

CZYM JEST WALIDATOR?

Walidator jest funkcją, która jako parametr przyjmuje control : AbstractControl (control reprezentuje pojedynczy <input> na widoku) i zwraca error reprezentowany przez obiekt lub NULL w przypadku gdy walidacja jest poprawna.

Zacznę od przedstawienia dobrych praktyk, na które warto zwrócić uwagę:

  • nasze walidatory powinny być jak najbardziej generyczne (chyba, że są pod konkretny przypadek)
  • dobrze przygotować osobny moduł dla własnych walidatorów
  • walidatory trzymamy jako metody statyczne w jednej klasie (chyba, że wielkość i złożoność aplikacji, wymusza na nas jakiś konkretny podział, to warto pogrupować do osobnych klas)

Przejdę teraz do kodu pierwszego walidatora.

WALIDATOR DLA POJEDYNCZEJ KONTROLKI (FormControl / ngModel)

Pierwszy przykład jaki pokażę, sprawdza czy wartość przekazana przez użytkownika, jest liczbą.

Zacznę od przygotowania modułu dla naszych walidatorów:

Oraz klasy, w której zaimplementuję kod pierwszego walidatora:

Jak widać idea jest prosta – metoda statyczna przyjmuje parametr control, następnie wykonuje odpowiedni warunek, jeśli zostanie spełniony, otrzymujemy error.

Teraz się nasuwa pytanie, jak użyć naszego walidatora? Pokażę to w dalszej części artykułu. Ale zanim to zrobię, musisz wiedzieć, że:

  • w Model Driven Forms, wystarczy sięgnąć po powyższą metodę startsWith w klasie komponentu i przekazać ją jako walidator w formBuilderze
  • w Template Driven Forms, potrzebujemy dyrektywę, która obsłuży powyższy walidator.

DYREKTYWA WALIDATORA DLA TEMPLATE DRIVEN FORMS

Jak wspomniałem, potrzebujemy dyrektywy, która pozwoli nam korzystać z naszego walidatora nie tylko w Model Driven Forms. W katalogu z modułem walidatorów, polecam stworzyć osobny katalog na dyrektywy.

Kod dyrektywy dla walidatora Digits, prezentuje się następująco:

Rozłożę powyższy kod na czynniki:

Powżyszy kod określa nam providera, którego przekażemy do tablicy providers naszej dyrektywy. Multi providers w Angularze umożliwia nam rejestrację wielu zależności pod jednym tokenem (w naszym przypadku Token to NG_VALIDATORS). Jeśli sięgniesz po zależność dla tego tokena, otrzymasz listę wszystkich zarejestrowanych wartości. Najłatwiej spojrzeć do kodu źródłowego Angulara, dotyczącego walidatorów:

Validators.ts

Widzimy, że wszystkie wbudowane walidatory są już zarejestrowane pod tokenem NG_VALIDATORS. Jeśli chcemy, aby Angular mógł egzekwować nasze własne walidatory, musimy je zarejestrować pod tym tokenem.

Natomiast dzięki użyciu useExisting nie tworzy nam się nowa instancja providera. Generalnie Providery i tokeny (np. OpaqueToken, którym jest NG_VALIDATORS) są to bardziej złożone tematy, którym nie będę poświęcał uwagi w tym artykule.

Wytłumaczę jeszcze, czym jest forwardRef i dlaczego go tutaj użyliśmy.Być może się zastanawiasz, czemu nie po prostu:

Najpierw wytłumaczę, gdzie leży problem. W momencie, gdy w jednym pliku określamy dwie klasy, w której jedna korzysta z drugiej, JavaScript interpreter „nie hoistuje” klas do góry, w przeciwieństwie do function constructor w ES5.

Więc jeśli klasa wyżej zadeklarowana, używa klasy niżej zadeklarowanej, stykamy się z problemem, że dolna klasa jest jeszcze „undefined”. Brak hoistingu klas ma swój cel – jest to spowodowane tym, że jeśli zaczniemy używać „extends” (dziedziczenia) wraz z klasami w jednym pliku, może dojść do pewnych problemów. Nie chcę odbiegać zbytnio od tematu, więc nie będę dalej się wgłębiać.

Może myślisz, że prostym rozwiązaniem jest przerzucić deklarację klasy DigitsValidator nad @Directive i const DIGITS_VALIDATOR. Niestety nic to nam nie da, kod nam się nie skompiluje.

Deklaracja klasy dyrektywy musi wystąpić po dekoratorze @Directive({…}).

W rozwiązaniu problemu przychodzi nam fowardRef().

FowardRef

forwardRef – w momencie gdy useExisting sięga po klasę DigitsValidator,  nie jest ona jeszcze zdefiniowana. ForwardRef() bierze jako parametr funkcję, która zwraca klasę. Co ważne, ów funkcja nie zostaje od razu wywołana – czeka aż klasa, którą ma zwrócić, zostanie zdefiniowana. Podsumowując, użycie forwardRef pozwala nam użyć klasy w naszej dyrektywie, zanim została określona, rozwiązując nam problem kolejności deklaracji klas.

Nie będziesz się często spotykać z tym problemem w Angularze, bo standardowo wszystkie klasy mamy w oddzielnych plikach.

Wracając do kodu dyrektywy:

Powyższy kod zapewne jest dla Ciebie jasny, ale warto dodać, że Angular w łatwy sposób umożliwia nam zapis, z jaką inną dyrektywą musi być użyta nasza nowa dyrektywa:

Po przecinku podajemy kolejne dyrektywy, powtarzając nasz selektor. Została jeszcze do omówienia jedyna metoda w dykretywie, validate():

Klasa dyrektywy implementuje interfejs Validator, który zobowiązuje nas do stworzenia metody validate z parametrem control, która finalnie zwraca naszą metodę statyczną (czyli nasz walidator).

Tak przygotowaną dyrektywę, możemy już użyć na widoku w Template Driven Forms (pod warunkiem, że zadeklarowałeś i wyeksportowałeś ją w ValidatorsModule!).

WALIDATOR Z PARAMETREM

Teraz pokażę, jak tworzyć walidatory z parametrem. Przykładowy walidator, który omówię, wygląda następująco:

  • sprawdza, czy wartość przekazana przez użytkownika do pola tekstowego, rozpoczyna się na wymuszony przez nas znak
  • znak będzie przekazany jako parametr do walidatora
  • w przypadku złego znaku, wyświetlimy użytkownikowi errora, informującego, na jaką literkę musi się rozpoczynać wartość

Zatem, do kodu!

Do klasy MyValidators, dokładamy kolejną statyczną metodę:

Zapisanie wartości literki do property startsWith, pozwoli nam ją pokazać przy wyświetleniu błędu na widoku. Jeśli nie planowałbyś użytkownika informować, jaka literka jest niedozwolona, równie dobrze możesz zwrócić obiekt:

Walidator z parametrem gotowy!

Podsumowując:

  • w walidatorze bez parametru od razu dodajemy logikę i zwracamy obiekt z errorem
  • w walidatorze z parametrem, zwracamy funkcję, która zwraca nam obiekt z errorem

DYREKTYWA WALIDATORA STARTS-WITH

Dyrektywa dla walidatora z parametrem jest nieco bardziej złożona:

Co się wydarzyło w kodzie dyrektywy:

  • dodaliśmy provider (omówiony w poprzednim przykładzie)
  • stworzyliśmy prywatne pole validator, do której przypisaliśmy wartość Validators.nullValidator (jest to no-operation validator)
  • zaimplementowaliśmy hook ngOnChanges – któremu poświeciłem osobny artykuł. Następnie w środku ngOnChanges, nadpisaliśmy wartość this.validator, wartością naszego walidatora. Używamy ngOnChanges, ponieważ uruchamia się kiedy do Input() zostaje przekazana wartość (oraz za każdym razem, kiedy zostaje zmieniona)

Jeśli chcesz przekazać wartość bezpośrednio do nazwy dyrektywy, tzn:

to Input() musi mieć taką samą nazwę jak selektor dyrektywy.

Podsumowując dyrektywy:

  • dodajemy provider aby podpiąć się pod token NG_VALIDATORS
  • w walidatorze bez parametru, dodajemy wyłącznie metodę Validate(), w której zwracamy naszą metodę statyczną zawierającą logikę walidatora
  • dla walidatora z parametrem, również dodajemy metodę Validate(), ale oprócz tego musimy dodać Input(), który pobierze parametr z widoku, oraz dodać hook ngOnChanges, w którym przypiszemy nasz walidator

Czas na walidator dla całej grupy.

FORM GROUP VALIDATOR

Przedstawię teraz przykład walidatora, który będzie sprawdzał, czy jakiekolwiek pole w FormGroup jest wypełnione. Zaczynamy od metody statycznej:

Tym razem, zamiast control : FormControl, przekazujemy  group : FormGroup.

Puszczamy pętlę, która przeiteruje nam obiekcie group.controls (są tu wszystkie FormControls grupy), pushujemy je do tablicy Fields. Kiedy mamy już złapane wszystkie FormControls, implementujemy logikę walidatora. Użycia Array.some() nie będę tłumaczyć.

Finalnie powyższy walidator, zwróci nam error jeśli żaden FormControl nie ma wartości. Czas na dyrektywę, abyśmy również mogli użyć powyższego walidatora w Template Driven Forms.

DYREKTYWA WALIDATORA ONE-REQUIRED

Kod dyrektywy dla FormGroupy, wygląda identycznie jak dla FormControla:

Myślę, że po przedstawieniu dwóch poprzednich dyrektyw, powyższy kod jest dla Ciebie całkowicie jasny.

UŻYCIE DYREKTYW NA WIDOKU

Czas wykorzystać walidatory do pokazania błędów na widoku.

a) Model Driven Forms (pełny przykład w Plunkerze)

  1. Dodajemy walidatory startsWith(lettter) i digits (digits dla lastname jest bez sensu, ale to tylko przykład) w klasie komponentu:

Oraz walidator oneRequired, np. w metodzie, która tworzy grupę:

2. Pokazujemy błąd na widoku, poprzez użycie dyrektywy *ngIf:

Oraz dla FormGroup (w naszym moim plunkerze, znajduje się dodatkowo w FormArray, stąd zmienna „i”):

Oczywiście można się pobawić w trzymanie textu errorów w klasie komponentu, co pokazałem w pierwszym artykule o Model Driven Forms.

b) Driven Template Forms (pełny przykład w Plunkerze)

W przypadku TDF, używamy wyłącznie dyrektyw na widoku, najwygodniej to zrobić używając template variable, której przypisujemy „ngModel” lub „ngModelGroup”:

Jeśli walidacja jest niepoprawna, to obiekt errors zawiera nasze error objects, które są zwracane w metodach statycznych z walidacją. Stąd kolejno errors.digits, errors.startsWith i errors.oneRequired.

Poniżej link do plunkera, zawierający wszystkie walidatory i dyrektywy:

LIVE EXAMPLE

PODSUMOWANIE

Pisanie własnych walidatorów jest proste i przyjemne. Możemy zarówno walidować pojedyncze inputy, jaki całe grupy inputów, gdzie możemy implementować bardziej skomplikowane scenariusze. Np. ostatnio w pracy skorzystałem z walidacji grupy, która trzyma 4 date pickery i 4 time pickery, a zależnie od wybranych dat i godzin, poszczególne dni i godziny zostają zablokowane w pozostałych datepickerach, dzięki użyciu control.enable() / control.disable(). Jak dla mnie coś świetnego, ekipa Angulara stanęła na wysokości zadania.

O autorze

Tomasz Nastały

JavaScript Developer, entuzjasta frameworka Angular oraz TypeScripta. Na co dzień lubię dzielić się wiedzą poprzez prowadzenie zajęć w jednym z trójmiejskich bootcampów i nagrywaniem kursów z Angulara.

Zapisz się do naszego newslettera. Bądź na bieżąco z najnowszymi trendami, poradami, meetupami i stań się częścią społeczności Angulara w Polsce. Rynek pracy docenia członków społeczności.

4 komentarzy

  1. Pingback: ANGULAR 2 – Custom Form Controls

  2. kuka

    Super blog, pierwszy blog polskojęzyczny tak fajnie opisujący angular2.

    Mam pytanie:
    Chcę zrobić valdiator który będzie sprawdzał w bazie czy emaila jest unikalny. Mam wszystko zrobione tak jak w Twoim artykule, ale w samym MyValidators, zwracam observer-a z this.http.post, i tutaj nie wyłapuje mi błędu. W tablicy control.errors mam obiekt observer a nie wynik.
    Moja klasa z walidacją:

    static email(customValidatorsService : CustomValidatorsService) : ValidatorFn {
    return (control: AbstractControl) : {[key: string]: any} => {
    let observer = customValidatorsService.checkEmail(control.value);
    osbserver.subscribe( (data) => {
    console.log(„datadata”, data);
    });
    return observer;
    }
    }

    a service:

    return this.http.post(url, JSON.stringify({ [source]: value }), options)
    .map(res => res.json())
    .catch(this.handleError)
    .map((res: Response) => {
    if (res.status === ResponseValue.Success) {
    return null;
    } else {
    return {’uniqueEmail’: true};
    };
    });

    Gdzie popełniłem błąd?
    A drugie pytanie, w jaki sposób zrobić tak aby ten validator uruchamiał się jak zmieni się fokus na jakimś polu formularza?

  3. Pingback: ANGULAR 2 VALIDATION SERVICE - Angular.love

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *