Angular - sterowanie kontrolkami, walidacja i tworzenie formularzy z użyciem dyrektywy ngModel

Stronę tą wyświetlono już: 15 razy

Wstęp

Komunikacja z użytkownikiem jest jedną z podstawowych i ważniejszych funkcjonalności praktycznie każdej aplikacji. Toteż nie zdziwi fakt, że kontrolki oraz formularze mają tutaj decydujące znaczenie. Oto bowiem kontrolki umożliwiają użytkownikowi sterowanie ustawieniami aplikacji, wprowadzaniem do nich danych a i w efekcie końcowym przetwarzanie tych danych przez aplikację, przesyłanie czy też pobieranie i wyświetlanie do edycji. Tak więc na tej stronie postaram się przybliżyć nieco sposób obsługi komunikacji z użytkownikiem zarówno za pomocą pojedynczych kontrolek jak i całej serii zgrupowanej w formularzu. Omówię tutaj też sposób walidacji takich kontrolek.

Metod obsługi kontrolek jest w Angular-ze jest kilka. Na tej stronie omówię te związane z użyciem dyrektywy ngModel, która to w łatwy sposób umożliwia bindowanie zmiennych z kontrolkami.

Użytkownik - pierwszy kontakt czyli pojedyncze kontrolki z dyrektywą ngModel

Jeżeli w danym widoku potrzebujesz utworzyć jedną lub dwie kontrolki, a nawet i całą ich serię, która to będzie sterowała zachowaniem twojego widoku, wtedy to dyrektywa ngModel zdaje się być najwygodniejszą w użyciu. Oto bowiem wystarczy utworzyć sobie kontrolkę i zbindować ją w jakże przebiegły sposób:

Listing 1
  1. <select [(ngModel)]="country" (selectionChanged)="countryChanged()">
  2. <option *ngFor="let countryOption of countries" [value]="countryOption">countryOption<option>
  3. </select>

oraz w kodzie komponentu napisać:

Listing 2
  1. country: string = 'Polska';
  2. countries: string[] = ['Polska', 'Francja', 'Włochy', 'Wielka Brytania', 'USA - the greatest country on the world'];
  3. countryChanged() {
  4. console.log('SELECTION CHANGED', this.country);
  5. }

aby po chwili, lub co najwyżej dwóch móc cieszyć się z dwukierunkowego bindowania danych oraz pozyskiwania informacji o ich zmianie za pomocą metody podpiętej pod zdarzenie selectionChanged.

Nieco trudniej jest z tworzeniem własnych walidatorów, albowiem aby utworzyć swój własny upragniony walidator konieczne jest stworzenie własnej dyrektywy. Standardowe HTML-owe walidatory w dużej części przypadków sprawdzają się wyśmienicie. Oto przykład użycia takich właśnie walidatorów:

Listing 3
  1. <input #country="ngModel" [(ngModel)]="countrySel" minlength="5" maxlength="20" required>
  2. <div>
  3. <span *ngIf="selCountrys.hasError('minlength')">Wprowadzony ciąg znaków nie może być krótszy od 5</span>
  4. <span *ngIf="selCountrys.hasError('maxlength')">Wprowadzony ciąg znaków nie może być dłuższy od 20</span>
  5. <span *ngIf="selCountrys.hasError('required')">To pole jest wymagane</span>
  6. </div>

Z powyższych walidatorów maxlength nie spowoduje wyświetlenia komunikatu błędu, albowiem ten walidator po prostu blokuje możliwość wpisywania tekstu dłuższego niż upragnione 20 znaków.

Wszystko fajnie, wszystko pięknie. Tylko jak zrobić ten swój własny upragniony walidator? Odpowiedź jest prosta: w przypadku dyrektywy ngModel koniecznej jest stworzenie własnej dyrektywy implementującej interfejs Validator:

Listing 4
  1. @Directive({
  2. selector: '[appForbiddenName]',
  3. providers: [{ provide: NG_VALIDATORS, useExisting: ForbiddenValidatorCountryDirective, multi: true }]
  4. })
  5. export class ForbiddenValidatorCountryDirective implements Validator {
  6. @Input('appForbiddenCountry') forbiddenCountry: string;
  7. validate(control: AbstractControl): { [key: string]: any } | null {
  8. const nameRe: RegExp = new RegExp(this.forbiddenCountry, 'i');
  9. const forbidden = nameRe.test(control.value);
  10. return this.frobiddenCountry
  11. ?
  12. ( forbidden ? { forbiddenCountry: { value: control.value } } : null)
  13. :
  14. null;
  15. }
  16. }

Dyrektywa powinna być dodana do deklaracji w app.module.ts w przeciwnym przypadku nie będzie działała poprawnie. Użycie tego walidatora będzie wyglądało tak:

Listing 5
  1. <input #country="ngModel" [(ngModel)]="countrySel" minlength="5" maxlength="20" appForbiddenCountry="Rosja" required>
  2. <div>
  3. <span *ngIf="selCountrys.hasError('minlength')">Wprowadzony ciąg znaków nie może być krótszy od 5</span>
  4. <span *ngIf="selCountrys.hasError('maxlength')">Wprowadzony ciąg znaków nie może być dłuższy od 20</span>
  5. <span *ngIf="selCountrys.hasError('appForbiddenCountry')">Co Rosja! Rosja nigdy, Rosja nigdy!</span>
  6. <span *ngIf="selCountrys.hasError('required')">To pole jest wymagane</span>
  7. </div>

Po wpisaniu frazy, która będzie zawierała w swoim wnętrzu słowo Rosja (nawet pisane małą literą) zwrócony zostanie błąd 'appForbiddenCountry'. Dlaczegóż się uwziąłem na Rosję? Nie uwziąłem się, po prostu większość ludzi w Polsce ma alergię na ten kraj.

Walidacja formularzy

Jeżeli przyszło ci do głowy na myśl, że fajnie by było uzyskać ogólny kod błędu czy walidacji twojego cudownego formularza to z pewnością oczy swe skierujesz ku rozwiązaniu związanym z użyciem znacznika form. Albowiem za prawdę powiadam wam, dzięki zgrupowaniu kontrolek wewnątrz formularza można łatwo sprawdzić, czy cały formularz jest poprawnie wypełniony i umożliwić zablokowanie źle wypełnionego formularza. Niestety aby dyrektywa ngModel działała poprawnie konieczne jest w takim przypadku przypisanie każdej kontrolce atrybutu nazwy name albowiem jeśli tego nie zrobisz to nie będzie działało poprawnie bindowanie.

Oto przykład takiego formularza:

Listing 6
  1. <form #myForm="ngForm">
  2. <mat-form-field appearance="outline">
  3. <mat-label>Kraj</mat-label>
  4. <input matInput name="country" #country="ngModel" [(ngModel)]="countrySel" appForbiddenName="Rosja"
  5. minlength="5" maxlength="20" required touched="true">
  6. <mat-error>
  7. <span *ngIf="country.hasError('forbiddenName')">zabroniony kraj!</span>
  8. <span *ngIf="country.hasError('minlength')">Wprowadzony ciąg znaków nie może być krótszy od 5</span>
  9. <span *ngIf="country.hasError('maxlength')">Wprowadzony ciąg znaków nie może być dłuższy od 20</span>
  10. <span *ngIf="country.hasError('required')">To pole jest wymagane</span>
  11. </mat-error>
  12. </mat-form-field>
  13. <mat-form-field appearance="outline">
  14. <mat-label>Imię</mat-label>
  15. <input matInput name="firstName" #firstname="ngModel" [(ngModel)]="firstName" minlength="5" maxlength="20"
  16. required>
  17. <mat-error>
  18. <span *ngIf="firstname.hasError('minlength')">Wprowadzony ciąg znaków nie może być krótszy od 5</span>
  19. <span *ngIf="firstname.hasError('maxlength')">Wprowadzony ciąg znaków nie może być dłuższy od 20</span>
  20. <span *ngIf="firstname.hasError('required')">To pole jest wymagane</span>
  21. </mat-error>
  22. </mat-form-field>
  23. <mat-form-field appearance="outline">
  24. <mat-label>Nazwisko</mat-label>
  25. <input matInput name="lastName" #lastname="ngModel" [(ngModel)]="lastName" minlength="5" maxlength="20"
  26. required>
  27. <mat-error>
  28. <span *ngIf="lastname.hasError('minlength')">Wprowadzony ciąg znaków nie może być krótszy od 5</span>
  29. <span *ngIf="lastname.hasError('maxlength')">Wprowadzony ciąg znaków nie może być dłuższy od 20</span>
  30. <span *ngIf="lastname.hasError('required')">To pole jest wymagane</span>
  31. </mat-error>
  32. </mat-form-field>
  33. <button mat-button type="submit" (click)="submit()" [disabled]="myForm.invalid">Wyślij</button>
  34. </form>

Jak widać dzięki fragmentowi kodu:

Listing 7
  1. #myForm="ngForm"

Uzyskuję dostęp do obiektu formularza, który to z kolei posiada właściwość invalid. Tę właściwość wykorzystałem w jakże przebiegły sposób do zablokowania przycisku Wyślij, gdy formularz nie jest poprawnie wypełniony. Użyłem tutaj również nieco ładniejszych Angular-owych kontrolek z modułu Material. Jeżeli chcesz skorzystać z tego modułu koniecznie musisz go najpierw zainstalować:

npm install @angular/material

a następnie dodać do projektu:

ng add @angular/material --save

Ostatnią rzeczą, jaką należy zrobić to zaimportować odpowiednie moduły w pliku app.module.ts:

Listing 8
  1. FormsModule,
  2. BrowserAnimationsModule,
  3. ReactiveFormsModule,
  4. MatInputModule,
  5. MatButtonModule,
  6. MatIconModule,
  7. MatTableModule,
  8. MatFormFieldModule,

do importów.

Komentarze