ANGULAR 2 MODEL DRIVEN FORMS, cz. I – tworzymy formularz z walidacją

W ostatnim artykule poruszyłem temat budowania formularzy techniką Template Driven Forms. W tym arcie przyjrzę się drugiemu wariantowi – Model Driven Forms (znane także jako reactive forms). Na potrzeby artykułu, będę się posługiwać skrótem MDF.

Jeśli od razu chcesz zajrzeć do kodu, który omówimy, kliknij na poniższy link:

LIVE EXAMPLE

MODEL DRIVEN FORMS (MDF)

Rozpoczęcie pracy z MDF wygląda podobnie jak z TDF. Importujemy odpowiedni moduł (ReactiveFormsModule):

A następnie przygotowujemy formularz:

Zanim przejdziemy do kolejnych zmian w kodzie, spójrzmy, jak budowane są formularze MDF. Wszystko czego potrzebujemy, to dwie dyrektywy po stronie HTML oraz stworzenie instancji dyrektyw w klasie komponentu:

  • dyrektywa FormGroup, którą nakładamy na tag <form>,
  • dyrektywa FormControlName, którą nakładamy na każdy <input>,
  • każdy FormGroup oraz FormControl wymaga stworzenia instancji w klasie komponentu

 

Zatem użyjmy wspomnianych dyrektyw. Do tagu <form> dodajemy dykrektywę formGroup.

Oraz na każdym inpucie, dyrektywę formControlName, którą możemy postrzegać jako odpowiednik ngModelName w Template Driven Forms.

Co warto zapamiętać na tym etapie:

  • formularz (<form>) jest reprezentowany przez FormGroup, który śledzi wartości poszczególnych FormControl’sów oraz stan walidacji,
  • pojedynczy input to jest reprezentowany przez dykrektywę FormControl
  • formGroup zbiera wartości z poszczególnych FormControl i tworzy z nich obiekt, którego klucze są pobrane z formControlName

Teraz pozostaje nam przejść do klasy komponentu i stworzyć nową instancję dyrektywy formGroup.

IMPLEMENTACJA MDF PO STRONIE KLASY KOMPONENTU

Jak wspomniałem, potrzebujemy nowych instancji FormGroup oraz FormControl:

Tadam! To wszystko, MDF gotowy. Jednak powyższa praktyka to nienajlepszy pomysł. Panowie z Angulara przygotowali dla nas klasę FormBuilder, która w profesjonalny sposób pozwoli nam obsłużyć formularz po stronie klasy komponentu. Zatem powyższy przykład, potraktujmy jako gorszy wariant.

FORM BUILDER – LEPSZA PRAKTYKA

Teraz pokażę jak tworzyć MDF z pomocą klasy FormBuilder – jest to najwłaściwsza metoda tworzenia MDF (zresztą po to FormBuilder został stworzony), dzięki niej kod formularza jest czystszy i lepiej zorganizowany.

W przypadku użycia FormBuildera, nasz kod wygląda następująco:

Pracę rozpoczęliśmy od wstrzyknięcia FormBuildera do konstruktora:

FormBuilder możemy traktować jako fabrykę, która tworzy FormGroup i FormControl za pomocą metody group().

Pozostaje nam tylko użyć wstrzykniętego serwisu na instancji naszej dyrektywy:

Metoda group() tworzy nam nową instancję FormGroup, a każde pole obiektu, tworzy nową instancję FormControl.

Przypisany pusty string, reprezentuje początkową wartość danej kontrolki (inputa).

Na tym etapie, mamy ponownie działający formularz. Jednak formularz bez walidacji, to jak żołnierz bez karabinu;)

Poniżej link do Plunkera, podsumowujący tą część artykułu:

LIVE EXAMPLE

WALIDACJA MODEL DRIVEN FORMS

Angular udostępnia nam parę wbudowanych walidacji:

  • minLength
  • maxLength
  • required
  • requiredTrue
  • pattern

Znaczenia powyższych walidacji chyba nie muszę tłumaczyć. Dla innych walidacji, musimy przygotować swój custom code (będzie na to poświęcony osobny artykuł). Spróbujemy zaimplementować powyższe walidacje do naszego formularza. Importujemy klasę Validators z @angular/forms, oraz z pustego stringa, przechodzimy na array syntax:

Pierwszy element tablicy, to wartość początkowa inputa, natomiast drugi to synchroniczny walidator, w przypadku paru walidatorów – przekazujemy tablicę walidatorów. Nasz array może przyjąć także trzeci parametr, którym jest asynchroniczny walidator (nie ma go w powyższym przykładzie). Asynchroniczną walidacją, może być np. walidacja po stronie back-endu.

Warto pamiętać, że asynchroniczny walidator nie zostanie wykonany, póki wszystkie synchroniczne walidacje nie będą poprawne.

Nic nie stoi na przeszkodzie mieszania składni w formBuilderze – dodajemy array syntax tam, gdzie potrzebujemy walidacji.

WYŚWIETLENIE WALIDACJI W WIDOKU

Pozostaje nam teraz wyświetlić informację użytkownikowi. Przygotowujemy 2 obiekty, jeden, który trzyma teksty dla walidacji, oraz drugi, który będzie trzymał aktualną wartość walidacji do wyświetlenia dla danego FormControla:

Oraz przygotujemy metodę, która obsłuży nam listę możliwych errorów:

Przyda nam się teraz nasłuchiwanie na zmiany wartości inputów, aby wyświetlić odpowiedni error. Posłuży nam do tego valueChanges, które:

  • w przypadku użycia na FormGroup, wystawia nam Observable z wartościami całego formularza
  • w przypadku użycia na FormControl, wystawia nam Observable z wartością konkretnego Inputa

Wykorzystamy możliwość subskrypcji, aby wywołać funkcję walidacyjną:

W powyższej metodzie, za każdym razem jak zmieni się wartość inputa, zostaje wywołana nasza funkcja onControlValueChanged z  sekundowym opóźnieniem (debounceTime(1000), z biblioteki RxJS).

Jeszcze nieco prostego kodu w templatce, z *ngIf:

Podsumowujący plunker:

LIVE EXAMPLE

PODSUMOWANIE

Jakie są podstawowe różnice pomiędzy Model Driven Forms i Template Driven Forms?

  • w Template Driven Forms, jesteśmy w stanie podstawową logikę wykonać po stronie templatki
  • w Model Driven Forms musimy się nieco bardziej napracować, przygotowanie formularza wymaga zarówno kodu HTML oraz JavaScriptu w klasie komponentu
  • W Template Driven Forms, stan formularza jest trzymany w widoku, więc napisane UnitTestów jest niemożliwe. Chyba, że masz referencję do formularza poprzez dekorator ViewChild i cześć logiki przenosisz do klasy
  • W przypadku Model driven forms, jesteśmy w stanie pisać unit testy, ponieważ od razu mamy obiekt reprezentujący stan formularza (wartości i walidację) w klasie komponentu.
  • w MDF możemy zmieniać zasady walidacji w czasie RunTime’u (pokażę to w kolejnym artykule)
  • MDF pozwala również na dynamiczne zmiany modelu formularza, tzn. dodawanie nowych FormGroup oraz FormControl w locie
  • MDF udostępnia nam Observable z wartością konkretnego Inputa lub całego formularza
  • w MDF jesteśmy w stanie oczyścić templatkę, przenosząc logikę i wiadomości walidacji do klasy komponentu (np. możemy trzymać wiadomości do walidacji w osobnym pliku (Constants), co przy dużej ilości formularzy będzie pomocne)
  • FormGroup w FormGroup możemy zagnieżdżać dowolną ilość razy, tworząc dowolnie skomplikowany obiekt

Wnioski:

  • do skomplikowanych formularzy polecam Model Driven Forms, posiada wiele ciekawych możliwości, którym poświęcę uwagę w kolejnych częściach
  • Template Driven Forms  nadają się do prostych formularzy, tym niemniej nie przepadam za obsługą formularza w templatce – lubię widzieć obiekt reprezentujący formularz w kodzie

MDF 1:0 TDF !

 

5 Comments

  1. takajednacosieuczyng2

    Hej, dzięki za blog! Super inicjatywa 🙂

    Pisałam sobie dziś trochę kodu wraz z Twoim tutorialem i musiałam trochę zmodyfikować metodę onControlValueChanged(), gdyż tslint podpowiadał for (... in ...) statements must be filtered with an if statement
    http://www.codexpedia.com/javascript/jslint-the-body-of-a-for-in-should-be-wrapped-in-an-if-statement-to-filter-unwanted-properties-from-the-prototype/


    onControlValueChanged() {
    const form = this.modelForm;

    for (const field in this.formErrors) {
    if (this.formErrors.hasOwnProperty(field)) {
    this.formErrors[field] = '';
    const control = form.get(field);

    if (control && control.dirty && !control.valid) {
    const validationMessages = this.validationMessages[field];
    for (const key in control.errors) {
    if (control.errors.hasOwnProperty(key)) {
    this.formErrors[field] += validationMessages[key] + ' ';
    }
    }
    }
    }
    }
    }

    Co o tym myślisz?

    Pozdrawiam 🙂

    • mcsqueeb

      Hej! 🙂 Cieszę się, że blog Ci się podoba.
      Oczywiście podpowiedzi IDE trzeba traktować z rozsądkiem i nie trzeba się zawsze do nich stosować.
      Najpierw trzeba zrozumieć, kiedy obiekt ma hasOwnProperty, a kiedy nie. Przykładowo masz konstruktor:

      function Animal(name) {
      this.name = name;
      }

      Teraz dodamy property przez prototype:
      Animal.prototype.age = 23;

      var pluto = new Animal(‚pluto’);

      pluto.name -> ‚pluto’
      pluto.age -> 23

      pluto.hasOwnProperty(‚name’) zwróci nam true.
      pluto.hasOwnProperty(‚age’) zwróci nam false.

      Wniosek – używanie hasOwnProperty w pętlach ma sens, jeśli używamy prototypów, lub nie jesteśmy pewni, czy prototypy gdzieś nie zostały użyte po drodze.

      W przypadku naszego obiektu:
      formErrors = {
      firstname: ”,
      lastname: ”
      }

      formErrors.hasOwnProperty(‚fristname’) -> zwraca true,
      formErrors.HasOwnProperty(‚lastname’) -> zwraca true,

      Więc jeśli masz property zadeklarowane bezpośrednio w obiekcie tak jak wyżej, hasOwnProperty mija się z celem, także możesz je usunąć:)

      I mały tip, jeśli chcesz sprwadzić listę own property danego obiektu, bez tych z prototypów:
      var keys = Object.keys(pluto);
      console.log(keys); -> [‚name’], nie ma age, mimo, że wpisując pluto.age, widzimy liczbę 20

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *