Angular & Formly – rozbudowane formularze i tylko kilka linijek HTMLa
Front-end aplikacji to przede wszystkim JavaScript, HTML i CSS. JavaScript i jego różne “wariacje” to podstawa przy pracy na “froncie”. Od kilku dobrych lat czysty JavaScript wspomagany jest przez różnego rodzaju frameworki (Angular, React, Vue …). W Software House Altkom Software & Consulting od wielu lat budujemy aplikacje biznesowe dla branż takich jak: ubezpieczeniowa, bankowa czy też medyczna. Rozwiązania te posiadają bardzo często jedną wspólną cechę – ich lwią część stanowią rozbudowane formularze do wprowadzania skomplikowanych danych. Z takimi formularzami zawsze jest sporo zabawy. W głowie już od dawna pojawiały nam się pewne pytania.
Jak usprawnić i przyspieszyć budowę formularzy, skupić się na bezbłędnej implementacji logiki biznesowej i przyjaznym dla użytkownika wyglądzie aplikacji?
Jak zmniejszyć liczbę użyć metody Copy’ego-Pasta w trakcie developmentu?
Jak zmniejszyć liczbę modyfikowanych linii kodu wynikającą z biznesowych zmian w wymaganiach?
Jako, że na co dzień pracujemy głównie wykorzystując Angulara, zaczęliśmy szukać czegoś, co pomoże nam rozwiązać powyższe problemy i jest kompatybilne z tym frameworkiem. Podczas tych poszukiwań znaleźliśmy Angular Formly – bibliotekę, która sprawia, że możemy stworzyć skomplikowany formularz pisząc dosłownie 5 linijek HTMLa. Brzmi fajnie? Nas też to bardzo zainteresowało i zaintrygowało 🙂
Cytując dokumentacje, Angular Formly zapewnia twoim formularzom spójność, łatwość obsługi, prostotę, elastyczność i rozsądek. Postanowiliśmy sprawdzić, czy te górnolotnie brzmiące sformułowania są prawdziwe i czy biblioteka ta nie dostanie zadyszki przy pierwszym, bardziej skomplikowanym przypadku użycia.
Kod źródłowy użyty w tym artykule i kilka dodatkowych ćwiczeń wykonanych wokół Formly, dostępny jest na naszym GitHubie: https://github.com/asc-lab/ngx-formly-playground
Poniżej zamieszczony został dodatkowo screen z ekranu głównego aplikacji, opisujący co zostało dodane do formularzy w kolejnych ćwiczeniach.
Biznesowy przypadek użycia
Pewien bank chciałby świadczyć nową usługę swoim najzamożniejszym klientom. Tacy klienci mogliby korzystać z usług konsjerża. Bank potrzebuje aplikacji z formularzem, na którym jego klienci będą mogli zgłaszać zlecenia wykonania dla nich zakupów (gdzie w jednym zgłoszeniu może zostać zlecone wykonanie zakupów więcej niż jednego produktu). W trakcie rozmowy pada też deklaracja, że zlecenia zakupów to dopiero początek. W planach są również zlecenia rezerwacji usług (np. fryzjera, lekarza) oraz kilka innych typów zadań.
Formularz ma oczywiście, cytując, “pięknie wyglądać na telefonach, laptopach i dużych monitorach”.
Przedstawiona została makieta obrazująca wygląd formularza:
Formularz powinien być podzielony na tematyczne sekcje:
- Order Identification – z polami identyfikującymi kartę oraz z możliwością wskazania przedmiotu zapotrzebowania.
- Shoppings – sekcja pozwalająca na wprowadzenie kolejnych zleceń zakupów/usunięcia elementu w przypadku pomyłki, z polami pozwalającymi konsjerżowi na identyfikacje przedmiotu jaki ma zostać zakupiony, wraz ze wskazaniem w jakim przedziale cenowym powinien zostać wykonany zakup.
- Additional Comments – z polem pozwalającym na wprowadzenie dłuższego komentarza.
- Confirmations – w pierwszym etapie klient powinien móc potwierdzić numer telefonu i email.
- Statements – w tej sekcji klient powinien mieć możliwość zapoznania się z warunkami świadczenia usługi.
Dodatkowo formularz powinien posiadać walidacje weryfikujące wymagalność pól oraz poprawność wprowadzonych danych.
Formularz bez Angular Formly
Formularz budować będziemy z wykorzystaniem Angular 7 oraz biblioteki Angular Material.
Bez porównania ciężko wyciągać wnioski, dlatego też na początek zbudujemy formularz bez wykorzystania Angular Formly, za to wykorzystując wbudowany w Angulara mechanizm zwany Reactive Forms.
Na początku skupmy się na pierwszej sekcji, czyli Order Identification. Sekcja powinna posiadać 3 pola – card id, card token oraz order type. HTML z definicją tych 3 pól wygląda tak:
<div class="app-content">
<button mat-raised-button routerLink="../../home" routerLinkActive="active" fxLayoutAlign="">Back to list
</button>
<form [formGroup]="form" (ngSubmit)="submit()">
<mat-card class="app-content-card">
<mat-card-title>
<span>Order Identification - Reactive Forms</span>
</mat-card-title>
<mat-card-content fxLayout="column">
<mat-form-field>
<input type="text" matInput placeholder="Card ID" name="cardId" formControlName="cardId">
<mat-error *ngIf="!fc.cardId.valid">
<span *ngIf="fc.cardId.errors.required">This field is required</span>
<span *ngIf="fc.cardId.errors.minlength">Card ID length is {{fc.cardId.errors.minlength.requiredLength}}
characters</span>
<span *ngIf="fc.cardId.errors.maxlength">Card ID length is {{fc.cardId.errors.maxlength.requiredLength}}
characters</span>
<span *ngIf="fc.cardId.errors.cardExist">This Card ID does not exist please check your identification
card</span>
</mat-error>
</mat-form-field>
<mat-form-field>
<input type="text" matInput placeholder="Card token" name="cardToken" formControlName="cardToken">
<mat-error *ngIf="!fc.cardToken.valid">
<span *ngIf="fc.cardToken.errors.required">This field is required</span>
<span *ngIf="fc.cardToken.errors.minlength">Card token length is
{{fc.cardToken.errors.minlength.requiredLength}} characters</span>
<span *ngIf="fc.cardToken.errors.maxlength">Card token length is
{{fc.cardToken.errors.maxlength.requiredLength}} characters</span>
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-select placeholder="Order type" name="orderType" formControlName="orderType">
<mat-option *ngFor="let order of orderTypes" [value]="order.key">
{{order.value}}
</mat-option>
</mat-select>
<mat-error *ngIf="!fc.orderType.valid">
<span *ngIf="fc.orderType.errors.required">This field is required</span>
</mat-error>
</mat-form-field>
</mat-card-content>
</mat-card>
<button type="submit" mat-raised-button class="app-primary-btn">Submit</button>
</form>
</div>
Plik z TypeScript’em wygląda natomiast tak:
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { RequestToConcierge } from '@app/shared/model/RequestToConcierge';
import { RequestService, DictService } from '@app/shared/services';
import { DictionaryItem } from '@app/shared/model/common';
@Component({
selector: 'app-exercise-three-rf',
templateUrl: './exercise-three-rf.component.html',
styleUrls: ['./exercise-three-rf.component.scss']
})
export class ExerciseThreeRfComponent implements OnInit {
form = new FormGroup({});
RequestToConcierge: RequestToConcierge = new RequestToConcierge();
orderTypes: DictionaryItem[] = this.dictionaryService.getDictionaryItems('ORDER_TYPE');
constructor(
public requestService: RequestService,
public dictionaryService: DictService,
private fb: FormBuilder) { }
ngOnInit() {
this.applyDisplayMode();
}
applyDisplayMode() {
this.form = this.fb.group({
cardId: ['', Validators.compose([Validators.required, Validators.minLength(5), Validators.maxLength(5)])],
cardToken: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(5)]],
orderType: ['', Validators.required],
});
}
get fc() { return this.form.controls; }
submit() {
if (this.form.valid) {
this.RequestToConcierge = this.form.value;
this.requestService.saveRequest(this.RequestToConcierge);
}
}
}
Prawda, że sporo tego wyszło jak na tak proste zadanie? Przecież to tylko formularz z trzema polami i prostą walidacją.
Jeszcze ciekawiej robi się jak dodamy wszystko, o co prosił bank. Komponent formularza po dodaniu wszystkich wskazanych w wymaganiach elementów można podejrzeć w tym katalogu.
Angular Formly – inne spojrzenie na formularze
Angular Formly to zupełnie inne podejście. Koniec z pisaniem dziesiątek linijek opakowujących kolejnego div’a w div’ie, żeby zapewnić odpowiednie RWD. HTML w tym nowym podejściu to tylko kilka wierszy.
<div class="app-content">
<form [formGroup]="form" (ngSubmit)="submit()">
<formly-form [model]="model" [fields]="fields" [options]="options" [form]="form">
<button type="submit" mat-raised-button class="app-primary-btn" [disabled]="!form.valid">Submit</button>
</formly-form>
</form>
</div>
To jest cała definicja naszego formularza w HTML. Można powiedzieć, że ciężar logiki tworzenia UI formularza zdecydowanie się zmniejsza. Część logiki przenosi się do pliku TS, ponieważ to właśnie tam definiujemy elementy naszego formularza.
Formly to zestaw gotowych szablonów, więc staramy się wykorzystać ich jak najwięcej.
Wybraliśmy na początku Angular Material jako bibliotekę UI, więc od razu przechodzimy do szablonów Angular Material, z którymi Formly jest zintegrowane. Jeśli komuś nie podoba się Angular Material lub po prostu woli pracować z inną biblioteką komponentów UI to do wyboru ma jeszcze Boostrap’a, Ionic’a, PrimeNG, Kendo oraz NativeScript.
W dokumentacji można znaleźć dużo użytecznych przykładów z kodem, który faktycznie działa – to duża zaleta, bo nie zawsze tak jest.
Na początku, chcielibyśmy, by nasze sekcje “opakowane” były w Komponent Card z Angular Material. W tym celu tworzymy coś, co nazywa się custom wrapper. Custom wrapper to w uproszczeniu szablon, w który możemy opakować pola formularza.
Stworzenie takiego wrappera jest bardzo proste i sprowadza się do stworzenia odpowiedniego komponentu z kawałkiem HTMLa i dodaniu definicji wrappera do ustawień Formly. Więcej o procesie tworzenia custom wrapper’ów można przeczytać w dokumentacji.
Po dodaniu wrapper’a przechodzimy do dodawania pól do naszego formularza. Pola definiujemy w pliku TS, korzystając ze wspomnianych wyżej szablonów. Podajemy parametry poszczególnych pól naszego formularza, które dzięki Angular Formly zostaną przekonwertowane do pliku HTML.
Pojedyncze pole z użyciem Angular Formly i bez niego
Przykładowo, bez użycia Formly definicja prostego pola Card ID (wymagane pole tekstowe) w HTMLu wygląda tak:
<mat-form-field>
<input matInput
type="text"
placeholder="Card ID"
name="cardId"
formControlName="cardId"
class="form-control"
/>
<mat-error *ngIf="!fc.cardId.valid">
<span *ngIf="fc.cardId.errors.required">This field is required</span>
</mat-error>
</mat-form-field>
Dodatkowo, wykorzystując Reactive Forms, w pliku TS należałoby dopisać:
this.form = this.fb.group({
cardId: ['', [Validators.required]]
});
Wykorzystując Formly mamy tylko plik TS, w którym pole Card ID będzie wyglądało tak:
fieldGroup: [{
key: 'cardId',
type: 'input',
templateOptions: {
type: 'text',
label: 'Card ID',
required: true
}
}]
Dodawanie kolejnych pól do formularza sprowadza się do dodawania kolejnych obiektów typu FormlyFieldConfig do tablicy fieldGroup.
Walidacje
Powyższy przykład pokazał czym różni się stworzenie bardzo prostego pola, z najprostszą możliwą walidacją – sprawdzeniem wymagalności. Przejdźmy do przykładów, które bardziej przypominają prawdziwe życie.
Dodawanie walidacji z wykorzystaniem Reactive Forms i Angular Material (bez Formly) polega na dopisywaniu kolejnych reguł w definicji pola oraz dodawaniu komunikatu walidacyjnego za pomocą brzydkiego if’a. Przykładowo, definicja pola Card ID wzbogacona o sprawdzenie długości wprowadzanego tekstu:
cardId: [”, Validators.compose([Validators.required, Validators.minLength(5), Validators.maxLength(5)])]
W pliku HTML mamy natomiast:
<mat-form-field>
<input type="text" matInput placeholder="Card ID" name="cardId" formControlName="cardId">
<mat-error *ngIf="!fc.cardId.valid">
<span *ngIf="fc.cardId.errors.required">This field is required</span>
<span *ngIf="fc.cardId.errors.minlength">Card ID length is {{fc.cardId.errors.minlength.requiredLength}}
characters</span>
<span *ngIf="fc.cardId.errors.maxlength">Card ID length is {{fc.cardId.errors.maxlength.requiredLength}}
characters</span>
</mat-error>
</mat-form-field>
Dodanie takiej samej walidacji z Angular Formly sprowadza się do dopisania dwóch linijek w TypeScripcie, nie trzeba kopiować po raz n-ty tagów <mat-error>.
{
key: 'cardId',
type: 'input',
templateOptions: {
label: this.translate.instant('RequestToConcierge.cardId'),
description: this.translate.instant('RequestToConcierge.cardIdDesc'),
type: 'text',
required: true,
minLength: 5,
maxLength: 5
}
Może pojawić się pytanie – gdzie definiowane są komunikaty błędów? Formly posiada kilka wbudowanych walidacji (built-in-validations), dzięki którym komunikaty walidacyjne takie jak: wymagalność pola, minimalna / maksymalna liczba znaków definiujemy tylko raz, a użycie konkretnej walidacji, przy konkretnym polu sprowadza się do dodania w definicji pola w templateOptions specyficznego atrybutu, np. required: true.
Część walidacji oczywiście trzeba definiować przy wybranych polach i tu korzystamy z pomocy dokumentacji i przykładów custom-validation.
Używając Formly definiujemy dwa elementy – wyrażenie, które ma zostać sprawdzone oraz komunikat który zostanie wyświetlony w przypadku błędu.
Poniżej pełny przykład, w którym dodane zostały niestandardowa walidacja na długość pola (w celu użycia innego komunikatu walidacyjnego) oraz walidacja asynchroniczna wykorzystująca serwis do sprawdzenia czy karta o podanym id nie została wcześniej dodana.
{
key: 'cardId',
type: 'input',
templateOptions: {
type: 'text',
label: 'Card ID',
description: 'Use one of this card IDs: 12345, 54321, 11111'
},
validators: {
cardId: {
expression: (fc) => !fc.value || fc.value.length === 5,
message: (err, field: FormlyFieldConfig) => `Card ID length is 5 characters`
}
},
asyncValidators: {
existingCardIdCheck: {
expression: (fc: FormControl) => {
return this.requestService.checkIfCardExist(fc);
},
message: 'This Card ID does not exist please check your identification card',
}
}
}
Powtarzalne sekcje
Do utworzenia sekcji Shoppings, w której użytkownik ma mieć możliwość dodawania więcej niż jednego zlecenia wykorzystamy specjalny typ pola, w dokumentacji opisany jako repeating section. Autorzy Formly przewidzieli sytuację, która dosyć często występuje w skomplikowanych formularzach – powtarzające się sekcje z kilkoma polami do wypełnienia. W naszym przypadku użytkownik aplikacji może wprowadzić do systemu kilka zleceń zakupów, które można porównać do wprowadzania kolejnych pozycji na liście zakupów.
Aby móc stworzyć taką sekcję w swoim formularzu, należy zdefiniować komponent dziedziczący po klasie FieldArrayType.
import { Component } from '@angular/core';
import { FieldArrayType } from '@ngx-formly/core';
@Component({
selector: 'app-repeat-section',
templateUrl: './repeat-section.component.html',
styleUrls: ['./repeat-section.component.scss']
})
export class RepeatSectionComponent extends FieldArrayType {
}
W HTML definiujemy ogólny wygląd sekcji. Oczywiście bez definicji konkretnych pól, ponieważ sekcja ta ma być reużywalna. Pola definiowane będą w momencie jej użycia.
<div *ngFor="let field of field.fieldGroup; let i = index;" class="app-section-item">
<div fxLayout="column">
<h3 fxLayoutAlign="start">{{ to.sectionTitle }}</h3>
<div fxLayout="row" fxLayout.xs="column">
<formly-field [field]="field" fxFlex="90%" fxFlex.xs="100%"></formly-field>
<div fxFlex="10%" fxFlex.xs="100%">
<button mat-raised-button type="button" (click)="remove(i)">{{ 'Global.Btn.remove' | translate }}</button>
</div>
</div>
</div>
</div>
<div>
<button mat-raised-button type="button" (click)="add()" class="app-secondary-btn">{{ to.addItem }}</button>
</div>
Sekcja posiada swój tytuł, oraz przyciski do usunięcia i dodania nowego elementu.
Wykorzystując sekcje w konkretnym komponencie należy zdefiniować fieldArray z konkretnymi polami, które mają się znaleźć w jej obrębie.
{
key: 'shoppings',
type: 'repeat-section',
templateOptions: {
sectionTitle: this.translate.instant('RequestToConcierge.shoppingItem'),
addItem: this.translate.instant('RequestToConcierge.addShoppingItem')
},
fieldArray: {
fieldGroup: [
{
key: 'order',
type: 'input',
templateOptions: {
type: 'text',
label: this.translate.instant('RequestToConcierge.order'),
required: true
},
},
{
key: 'description',
type: 'textarea',
templateOptions: {
type: 'text',
label: this.translate.instant('RequestToConcierge.description'),
maxLength: 6000,
rows: 5
}
},
[...]
}
Pełna definicja
Poniżej pełny przykład definicji pierwszej sekcji z wykorzystaniem tych samych service’ów oraz modelu co w przykładzie jak wyżej.
import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
import { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core';
import { RequestToConcierge } from '@app/shared/model/RequestToConcierge';
import { DictionaryItem } from '@app/shared/model/common';
import { RequestService, DictService } from '@app/shared/services';
@Component({
selector: 'app-exercise-two',
templateUrl: './exercise-three.component.html',
styleUrls: ['./exercise-three.component.scss']
})
export class ExerciseThreeComponent {
form = new FormGroup({});
model: any = {
RequestToConcierge: new RequestToConcierge()
};
orderTypes: DictionaryItem[] = this.dictionaryService.getDictionaryItems('ORDER_TYPE');
options: FormlyFormOptions = {};
fields: FormlyFieldConfig[] = [
{
key: 'RequestToConcierge',
wrappers: ['card'],
templateOptions: { cardTitle: 'Order Identification' },
fieldGroup: [
{
key: 'cardId',
type: 'input',
templateOptions: {
type: 'text',
label: 'Card ID',
description: 'Use one of this card IDs: 12345, 54321, 11111',
required: true,
},
validators: {
cardId: {
expression: (fc) => !fc.value || fc.value.length === 5,
message: (err, field: FormlyFieldConfig) => `Card ID length is 5 characters`
}
},
asyncValidators: {
existingCardIdCheck: {
expression: (fc: FormControl) => {
return this.requestService.checkIfCardExist(fc);
},
message: 'This Card ID does not exist please check your identification card',
}
}
},
{
key: 'cardToken',
type: 'input',
templateOptions: {
type: 'text',
label: 'Card token',
required: true,
},
validators: {
cardToken: {
expression: (fc: FormControl) => !fc.value || fc.value.length === 5,
message: (err, field: FormlyFieldConfig) => `Card Token length is 5 characters`
}
}
},
{
key: 'orderType',
type: 'select',
templateOptions: {
label: 'Order type',
options: this.orderTypes,
required: true,
},
}
],
},
];
constructor(public requestService: RequestService, public dictionaryService: DictService) { }
submit() {
if (this.form.valid) {
this.requestService.saveRequest(this.model.RequestToConcierge);
}
}
}
Jak widać powyżej, definicja pól formularza przeniosła się do pliku TypeScript. Natomiast nie definiujemy tam dziesiątek div’ów w div’ach. Skupiamy się bardziej na logice, a UI generowany jest przez silnik wewnątrz Formly.
Oczywiście jeśli potrzebujemy wprowadzić zmiany w wyglądzie lub też zastosować bardziej skomplikowane rozłożenie pól to jest taka możliwość (więcej o tym w dalszej części artykułu).
Komponent po dodaniu wszystkich wskazanych w wymaganiach elementów można podejrzeć w tym folderze.
Efekt końcowy w obu przypadkach ten sam, widoczny na screenie poniżej.
Różnicą jest sposób wykonania. Jest to bardzo dobrze widoczne podczas porównywania zawartości tych dwóch folderów: formularz bez Formly oraz formularz z Formly.
Podsumowując powyższe, naszym zdaniem najważniejszymi zaletami Angular Formly są:
- wsparcie dla kilku najpopularniejszych bibliotek komponentów UI – w naszym projekcie korzystamy z UI Material,
- łatwość budowania własnych szablonów wykorzystując Custom Wrapper,
- nawet bardziej zaawansowane scenariusze posiadają działające, całkiem dobrze opisane przykłady w dokumentacji (np. Repeating Section),
- wbudowane walidacje built-in validations definiowane globalnie pozwalają ograniczyć duplikacje kodu.
Czego unikamy, dzięki wykorzystaniu Angular Formly:
- ciągnącego się na setki linii kodu HTML, z wieloma zagnieżdżonymi tagami,
- powtarzających się co pole kodu wywołującego walidacje,
- przeklejania nazw pól z pliku TS do HTML.
Nowe elementy, nowe wymagania
Nowe wymagania / zmiana wyglądu formularza to codzienność w każdym projekcie. Załóżmy, że nasz klient ma nowe wymagania:
- na formularzu powinna zostać dodana możliwość rezerwacji usługi (np. lekarza, stolika w restauracji, fryzjera),
- formularz powinien zostać podzielony na kroki (chcemy zmniejszyć liczbę pól, jaką widać w danym momencie na ekranie),
- pola takie jak “Cena od”, “Cena do” powinny być wyświetlane w jednej linii,
- niezmienne pozostaje wymaganie iż formularz powinien być dostosowany do prezentacji zarówno na laptopach jak i telefonach.
Formularz bez Angular Formly
Po dodaniu kolejnego typu zadania dla konsjerża, HTML rozrósł się i zaczyna wyglądać nieczytelnie. Liczy już sobie 312 linii i mnogość zagnieżdżeń zaczyna na pierwszy rzut oka przerażać.
<mat-card class="app-content-card" *ngIf="fc.orderType.value==='SERVICES'">
<mat-card-title>
<span>Services„</span>
</mat-card-title>
<mat-card-content>
<div formArrayName="services" *ngFor="let item of form.get('services').controls; let i = index;"
class="app-section-item">
<div [formGroupName]="i">
<h3 fxLayoutAlign="start">Service item„</h3>
<div fxLayout="row" fxLayout.xs="column">
<div fxFlex="90%" fxFlex.xs="100%" fxLayout="column">
<mat-form-field>
<input type="text" matInput placeholder="Order" name="order" formControlName="order">
...
Dodanie stepper’a by zrealizować wymaganie dot. podzielenia formularza na kroki dokłada kolejne tagi w pliku HTML. Wymagania dotyczące wyświetlania pól “price range from – price range to” udaje się zrealizować bez najmniejszego problemu z wykorzystaniem flex-layout.
Komponent po zaimplementowaniu wszystkich zgłoszonych zmian wygląda tak.
Można śmiało powiedzieć, że całość jest nieczytelna i że będzie problem z jego dalszą rozbudową i utrzymaniem. Oczywiście pokazana sytuacja jest trochę przerysowana, ponieważ można zrobić refactoring i wprowadzić pewne ulepszenia (wydzielenie mniejszych komponentów, np. per sekcja, stworzenie wrapper’ów na każde pole i w pewnym sensie duplikacji funkcjonalności, którą mamy dzięki Formly). Niemniej jednak, HTMLa raczej nie ubędzie, ale w efekcie powinniśmy otrzymać “czytelny dla ludzkiego oka” kod.
Angular Formly – upewnijmy się czy przedstawione wymagania da się zrealizować
Na tym etapie tworzenia formularza wiemy już, że dodanie obsługi nowego typu zlecenia, czyli rezerwacji usługi, to nie problem. Do utworzenia sekcji Services w której użytkownik ma mieć możliwość dodawania więcej niż jednego zlecania wykorzystujemy utworzony przez nas szablon Repeating section.
NgIf używany w “starym podejściu” do wyświetlania sekcji zgodnie z typem zlecenia, czyli albo Shoppings albo Services, zastępujemy opcją hide-fields.
Podzielenie formularza na kroki wymaga już odrobiny więcej pracy, ale i tu mamy w dokumentacji przykład multi-step-form, który zamierzamy wykorzystać.
Zostaje do wykonania ostatnie wymaganie dotyczące wyświetlania pól price range from – price range to. To zadanie wymaga szybkiego przypomnienia flexbox, gdyż zgodnie z dokumentacją advanced-layout-flex to właśnie w pliku CSS powinniśmy zdefiniować style naszego formularza.
Jak wygląda nasz kod po dodaniu wszystkich elementów?
W pliku HTML dodaliśmy kilka linijek by móc wykorzystać komponent stepper’a z Angular Material.
Znacząco wzrosła liczba linijek kody w pliku TS, ale mimo dodania kolejnej sekcji, podziału na kroki, atrybutów wskazujących na klasy, plik, naszym zdaniem, nie stracił na czytelności.
Komponent po dodaniu wszystkich wskazanych w wymaganiach elementów wygląda tak.
I znów w obu przypadkach efekt końcowy ten sam:
Wersja mobilna poniżej:
I po raz kolejny zaletami Angular Formly okazują się:
- przejrzystość – liczba linii kodu rośnie, czytelność kodu pozostaje na tym samym poziomie,
- dokumentacja jest czytelna, a przykłady przygotowane przez autorów działają,
- łatwość tworzenia Custom Templates, które wykorzystaliśmy przy polu Order type, zastępując “nudny” select przyciągającymi uwagę radio buttonami ukrytymi pod zdjęciami.
Dodatki
Sprawnie zrealizowane wymagania zachęciły nas do aktualizacji aplikacji o nowe funkcjonalności.
Wspierając się biblioteką ngx-translate i dokumentacją Angular Formly i18n dołożyliśmy możliwość zmiany języka. Wszystko poszło sprawnie, poza jednym tematem. Problemem okazały się globalnie (w pliku app.module.ts) definiowane komunikaty walidacyjne. Tłumaczenia ładowały się później, więc niemożliwe było skorzystanie z nich w tym miejscu.
Konieczne było stworzenie serwisu validations.loader.ts, w którym używając FormlyConfig definiujemy funkcje wywoływane podczas ładowania komunikatów. Po załadowaniu tłumaczeń inicjalizuje nasze komunikaty walidacyjne, dzięki czemu mogą być one odpowiednio tłumaczone i dynamicznie reagować na zmianę języka.
Wykonany został też drobny refactor kodu. W modelu: PriceRange,Service oraz Shopping dołożona została metoda formField, która następnie została użyta w pliku ts komponentu. Dzięki przeniesieniu definicji części formularza do klasy modelu unikamy duplikacji kodu i wynosimy odpowiedzialność za budowanie całego formularza z klasy komponentu. To klasa modelu (lub jakieś DTO) wie jak ma zbudować pola, którymi można ją wypełnić.
Komponent po dodaniu opisanych powyżej elementów wygląda tak.
Podsumowanie
Angular Formly to bez wątpienia ciekawa biblioteka, która może być przydatna w aplikacjach biznesowych, składających się w dużej mierze z rozbudowanych formularzy. Formly pozwala tworzyć formularze skupiając się na logice ich działania oraz modelu danych.
Definicja formularza w TypeScripcie pozwala w prostszy sposób budować dynamiczne formularze (np. na podstawie odpowiedzi z API), które są coraz częstszym wymaganiem biznesowym w tworzonych przez nas systemach.
Oczywiście należy mieć na uwadze, że podobne efekty można osiągnąć w inny sposób, np. tworząc swoją bibliotekę reużywalnych komponentów, jednak czy warto wymyślać koło na nowo?
W Software House Altkom Software & Consulting staramy się rozpoznać nową technologię czy też bibliotekę przed jej użyciem w komercyjnym projekcie. Taka ocena pozwala uniknąć eksperymentowania na projektach naszych Klientów. Pozwala skupić się na dostarczaniu wartości biznesowej, która w ostatecznym rozrachunku jest najważniejsza.
Agnieszka Chróścielewska, Web Developer
Robert Witkowski, Lead Software Engineer, ASC LAB