Angular - sterowanie kontrolkami, walidacja oraz tworzenie formularzy z wykorzystaniem FormControl i FormGroup z modułu ReactiveForms

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

Wstęp

Omówiłem już na wcześniejszej stronie jak tworzyć wiązania dwukierunkowe z kontrolkami HTML za pomocą dyrektywy ngModel. Na tej stronie opowiem co nieco o tym jak zrobić to samo za pomocą klasy FormControl i FormGroup. Przy wykorzystaniu tych klas nie działa bindowanie dwukierunkowe związane z dyrektywą ngModel a ustawianie i pobieranie danych z kontrolki wygląda inaczej. W tym przypadku również i podpinanie walidatorów będzie realizowane z poziomu obiektu klasy a ich tworzenie uprości się do stworzenia klasy z statyczną funkcją, która podpięta do obiektu klasy FormControl będzie walidowała daną kontrolkę.

Klasa FormControl i komunikacja z pojedynczą kontrolką

W celu zrealizowania komunikacji dwukierunkowej z kontrolką za pomocą obiektu klasy FormControl konieczne jest utworzenie takiego obiektu wewnątrz klasy komponentu i podpięcie jego do kontrolki w kodzie HTML co też i czynię z najdzikszą rozkoszą:

Listing 1
  1. selectControl: FormControl;
  2. countries: any[] = [
  3. { id: 0, name: 'Polska' },
  4. { id: 1, name: 'Francja' },
  5. { id: 2, name: 'Belgia' },
  6. { id: 3, name: 'Bułgaria' },
  7. ];
  8. ngOnInit(): void {
  9. this.selectCountry = new FormControl(2, Validators.required);
  10. }

Zaś w kodzie HTML:

Listing 2
  1. <mat-form-field appearance="outline">
  2. <mat-label>Kraje</mat-label>
  3. <mat-select [formControl]="selectCountry">
  4. <mat-option *ngFor="let country of countries" [value]="country.id">
  5. {{country.name}}
  6. </mat-option>
  7. </mat-select>
  8. </mat-form-field>

Stworzenie własnego walidatora dla klasy FormControl będzie wyglądało następująco:

Listing 3
  1. export class MyValidators {
  2. static frobiddenCountry(forbiddenCountry: string): ValidatorFn {
  3. return (control: AbstractControl): ValidationErrors | null => {
  4. const nameRe: RegExp = new RegExp(forbiddenCountry, 'i');
  5. const forbidden = nameRe.test(control.value);
  6. return forbiddenCountry
  7. ?
  8. (forbidden ? { forbiddenCountry: true } : null)
  9. :
  10. null;
  11. };
  12. }
  13. }

Użycie takiego walidatora jest dziecinnie proste, pod warunkiem, że korzystasz z klasy FormControl:

Listing 4
  1. myCountry: FormControl;
  2. ngOnInit() {
  3. this.myCountry = new FormControl('Polska', MyValidators.frobiddenCountry('Rosja'));
  4. }

i w kodzie HTML:

Listing 5
  1. <mat-form-field appearance="outline">
  2. <mat-label>Kraj</mat-label>
  3. <input matInput [formControl]="myCountry">
  4. <mat-error>
  5. <span *ngIf="myCountry.hasError('forbiddenCountry')">Rosja nigdy! Rosja nigdy!</span>
  6. </mat-error>
  7. </mat-form-field>

Tworzenie i walidacja formularzy z wykorzystaniem FormGroup

Klasa FormGroup umożliwia stworzenie obiektu grupy kontrolek formularza, do których dane zwrócone przez serwis mogą zostać w bardzo łatwy sposób wstawione jak i odczytane. Oto przykład, jak może wyglądać stworzenie grupy kontrolek i przypisanie im wartości zawartych w interfejsie:

Listing 6
  1. formGroup: FormGroup;
  2. ngOnInit() {
  3. this.formGroup = this.formBuilder.group({
  4. yourCountry: ['Polska', [Validators.required]],
  5. firstName: ['', [Validators.required, Validators.maxLength(20), Validators.minLength(5)]],
  6. lastName: ['', [Validators.required, Validators.maxLength(20), Validators.minLength(5)]],
  7. });
  8. this.formGroup.patchValue({ yourCountry: 'Polska', firstName: 'Grzegorz', lastName: 'Brzęczyszczykiewicz' });
  9. this.addressGroup.patchValue( {street: 'Zadupie Wielkie', house: 10 });
  10. }

W kodzie HTML komponentu:

Listing 7
  1. <form [formGroup]="formGroup">
  2. <mat-form-field appearance="outline">
  3. <mat-label>Kraj</mat-label>
  4. <input matInput formControlName="yourCountry">
  5. <mat-error>
  6. <div *ngIf="formGroup.get('yourCountry').hasError('required')">To pole jest wymagane</div>
  7. <div *ngIf="formGroup.get('yourCountry').hasError('minlength')">Długość ciągu znaków krótsza od 5</div>
  8. <div *ngIf="formGroup.get('yourCountry').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div>
  9. </mat-error>
  10. </mat-form-field>
  11. <mat-form-field appearance="outline">
  12. <mat-label>Imię</mat-label>
  13. <input matInput formControlName="firstName">
  14. <mat-error>
  15. <div *ngIf="formGroup.get('firstName').hasError('required')">To pole jest wymagane</div>
  16. <div *ngIf="formGroup.get('firstName').hasError('minlength')">Długość ciągu znaków krótsza od 5</div>
  17. <div *ngIf="formGroup.get('firstName').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div>
  18. </mat-error>
  19. </mat-form-field>
  20. <mat-form-field appearance="outline">
  21. <mat-label>Nazwisko</mat-label>
  22. <input matInput formControlName="lastName">
  23. <mat-error>
  24. <div *ngIf="formGroup.get('lastName').hasError('required')">To pole jest wymagane</div>
  25. <div *ngIf="formGroup.get('lastName').hasError('minlength')">Długość ciągu znaków krótsza od 5</div>
  26. <div *ngIf="formGroup.get('lastName').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div>
  27. </mat-error>
  28. </mat-form-field>
  29. <button mat-button type="submit" [disabled]="formGroup.invalid">Wyślij</button>
  30. </form>

Pobranie surowych danych z formularza również jest proste, albowiem wystarczy zrobić coś takiego:

Listing 8
  1. console.log(this.formGroup.getRawValue());

by w konsoli przeglądarki zobaczyć coś takiego:

{
  "yourCountry": "Polska",
  "firstName": "",
  "lastName": ""
}

Zagnieżdżanie obiektów w formularzu

W jednym formularzu można tak na prawdę zagnieździć więcej niż jeden obiekt zgrupowany w podobiekcie formularza. Oto przykład kodu HTML:

Listing 9
  1. <form [formGroup]="formGroup">
  2. <mat-form-field appearance="outline">
  3. <mat-label>Kraj</mat-label>
  4. <input matInput formControlName="yourCountry">
  5. <mat-error>
  6. <div *ngIf="formGroup.get('yourCountry').hasError('required')">To pole jest wymagane</div>
  7. <div *ngIf="formGroup.get('yourCountry').hasError('minlength')">Długość ciągu znaków krótsza od 5</div>
  8. <div *ngIf="formGroup.get('yourCountry').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div>
  9. </mat-error>
  10. </mat-form-field>
  11. <mat-form-field appearance="outline">
  12. <mat-label>Imię</mat-label>
  13. <input matInput formControlName="firstName">
  14. <mat-error>
  15. <div *ngIf="formGroup.get('firstName').hasError('required')">To pole jest wymagane</div>
  16. <div *ngIf="formGroup.get('firstName').hasError('minlength')">Długość ciągu znaków krótsza od 5</div>
  17. <div *ngIf="formGroup.get('firstName').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div>
  18. </mat-error>
  19. </mat-form-field>
  20. <mat-form-field appearance="outline">
  21. <mat-label>Nazwisko</mat-label>
  22. <input matInput formControlName="lastName">
  23. <mat-error>
  24. <div *ngIf="formGroup.get('lastName').hasError('required')">To pole jest wymagane</div>
  25. <div *ngIf="formGroup.get('lastName').hasError('minlength')">Długość ciągu znaków krótsza od 5</div>
  26. <div *ngIf="formGroup.get('lastName').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div>
  27. </mat-error>
  28. </mat-form-field>
  29. <div formGroupName="address">
  30. <mat-form-field appearance="outline">
  31. <mat-label>Ulica</mat-label>
  32. <input matInput formControlName="street">
  33. <mat-error>
  34. <div *ngIf="formGroup.controls['address'].get('street').hasError('required')">To pole jest wymagane</div>
  35. <div *ngIf="formGroup.controls['address'].get('street').hasError('minlength')">Długość ciągu znaków krótsza od 5</div>
  36. <div *ngIf="formGroup.controls['address'].get('street').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div>
  37. </mat-error>
  38. </mat-form-field>
  39. <mat-form-field appearance="outline">
  40. <mat-label>Numer domu</mat-label>
  41. <input matInput type="number" formControlName="house">
  42. <mat-error>
  43. <div *ngIf="formGroup.controls['address'].get('house').hasError('required')">To pole jest wymagane</div>
  44. <div *ngIf="formGroup.controls['address'].get('house').hasError('minlength')">Numer domu nie może mniejszy niż 1</div>
  45. </mat-error>
  46. </mat-form-field>
  47. </div>
  48. <button mat-button type="submit" [disabled]="formGroup.invalid">Wyślij</button>
  49. </form>

I kod komponentu:

Listing 10
  1. formGroup: FormGroup;
  2. addressGroup: FormGroup;
  3. ngOnInit() {
  4. this.addressGroup = this.formBuilder.group({
  5. street: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(20)]],
  6. house: [1, [Validators.required, Validators.min(1)]]
  7. });
  8. this.addressGroup.patchValue( {street: 'Zadupie Wielkie', house: 10 });
  9. this.formGroup = this.formBuilder.group({
  10. yourCountry: ['Polska', [Validators.required]],
  11. firstName: ['', [Validators.required, Validators.maxLength(20), Validators.minLength(5)]],
  12. lastName: ['', [Validators.required, Validators.maxLength(20), Validators.minLength(5)]],
  13. address: this.addressGroup
  14. });
  15. this.formGroup.patchValue({ yourCountry: 'Polska', firstName: 'Grzegorz', lastName: 'Brzęczyszczykiewicz' });
  16. console.log(this.formGroup.getRawValue());
  17. }

Wynik danych wyciągniętych z formularza i wyświetlonych w konsoli przeglądarki:

{
  "yourCountry": "Polska",
  "firstName": "Grzegorz",
  "lastName": "Brzęczyszczykiewicz",
  "address": {
    "street": "Zadupie Wielkie",
    "house": 10
  }
}

Dynamicznie rozszerzalny formularz

A co jeśli chciałbym stworzyć formularz, który będzie umożliwiał dynamiczne dodawanie np. nowego rekordu danych? Czy da się coś takiego zrobić? Da się, albowiem FormBuilder ma opcję tworzenia tablicy przechowójący z kolei obiekty klasy AbstractControl. Tak się jakoś dziwnie składa, że po tej abstrakcyjnej klasie dziedziczy nie co innego ale klasa FormGroup. A oto i przebiegły sposób, w jaki można to wykorzystać do stworzenia prawdziwie rozszerzalnego formularza:

Listing 11
  1. table: FormArray;
  2. students = [{ studentName: 'Grzegorz', studentSurname: 'Brzęczyszczykiwicz' },
  3. { studentName: 'Marian', studentSurname: 'Pietrucha' }];
  4. ngOnInit() {
  5. this.addressGroup = this.formBuilder.group({
  6. street: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(20)]],
  7. house: [1, [Validators.required, Validators.min(1)]]
  8. });
  9. this.addressGroup.patchValue({ street: 'Zadupie Wielkie', house: 10 });
  10. this.table = this.formBuilder.array([
  11. this.formBuilder.group(
  12. {
  13. studentName: ['', Validators.required],
  14. studentSurname: ['', Validators.required]
  15. })
  16. ]);
  17. this.formGroup = this.formBuilder.group({
  18. yourCountry: ['Polska', [Validators.required]],
  19. firstName: ['', [Validators.required, Validators.maxLength(20), Validators.minLength(5)]],
  20. lastName: ['', [Validators.required, Validators.maxLength(20), Validators.minLength(5)]],
  21. address: this.addressGroup,
  22. table: this.table
  23. }
  24. );
  25. this.students.forEach((student) => { this.table.push(this.formBuilder.group(student)); });
  26. this.formGroup.patchValue({ yourCountry: 'Polska', firstName: 'Grzegorz', lastName: 'Brzęczyszczykiewicz' });
  27. console.log(this.formGroup.getRawValue());
  28. this.selectCountry = new FormControl(2, Validators.required);
  29. this.myCountry = new FormControl('Polska', MyValidators.frobiddenCountry('Rosja'));
  30. }

Zaś w kodzie HTML:

Listing 12
  1. <mat-form-field appearance="outline">
  2. <mat-label>Kraje</mat-label>
  3. <mat-select [formControl]="selectCountry">
  4. <mat-option *ngFor="let country of countries" [value]="country.id">
  5. {{country.name}}
  6. </mat-option>
  7. </mat-select>
  8. </mat-form-field>
  9. <mat-form-field appearance="outline">
  10. <mat-label>Kraj</mat-label>
  11. <input matInput [formControl]="myCountry">
  12. <mat-error>
  13. <div *ngIf="myCountry.hasError('forbiddenCountry')">Rosja nigdy! Rosja nigdy!</div>
  14. </mat-error>
  15. </mat-form-field>
  16. <form [formGroup]="formGroup">
  17. <mat-form-field appearance="outline">
  18. <mat-label>Kraj</mat-label>
  19. <input matInput formControlName="yourCountry">
  20. <mat-error>
  21. <div *ngIf="formGroup.get('yourCountry').hasError('required')">To pole jest wymagane</div>
  22. <div *ngIf="formGroup.get('yourCountry').hasError('minlength')">Długość ciągu znaków krótsza od 5</div>
  23. <div *ngIf="formGroup.get('yourCountry').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div>
  24. </mat-error>
  25. </mat-form-field>
  26. <mat-form-field appearance="outline">
  27. <mat-label>Imię</mat-label>
  28. <input matInput formControlName="firstName">
  29. <mat-error>
  30. <div *ngIf="formGroup.get('firstName').hasError('required')">To pole jest wymagane</div>
  31. <div *ngIf="formGroup.get('firstName').hasError('minlength')">Długość ciągu znaków krótsza od 5</div>
  32. <div *ngIf="formGroup.get('firstName').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div>
  33. </mat-error>
  34. </mat-form-field>
  35. <mat-form-field appearance="outline">
  36. <mat-label>Nazwisko</mat-label>
  37. <input matInput formControlName="lastName">
  38. <mat-error>
  39. <div *ngIf="formGroup.get('lastName').hasError('required')">To pole jest wymagane</div>
  40. <div *ngIf="formGroup.get('lastName').hasError('minlength')">Długość ciągu znaków krótsza od 5</div>
  41. <div *ngIf="formGroup.get('lastName').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div>
  42. </mat-error>
  43. </mat-form-field>
  44. <div formGroupName="address">
  45. <mat-form-field appearance="outline">
  46. <mat-label>Ulica</mat-label>
  47. <input matInput formControlName="street">
  48. <mat-error>
  49. <div *ngIf="formGroup.controls['address'].get('street').hasError('required')">To pole jest wymagane</div>
  50. <div *ngIf="formGroup.controls['address'].get('street').hasError('minlength')">Długość ciągu znaków krótsza od 5
  51. </div>
  52. <div *ngIf="formGroup.controls['address'].get('street').hasError('maxlength')">Długość ciągu znaków dłuższa od
  53. 20</div>
  54. </mat-error>
  55. </mat-form-field>
  56. <mat-form-field appearance="outline">
  57. <mat-label>Numer domu</mat-label>
  58. <input matInput type="number" formControlName="house">
  59. <mat-error>
  60. <div *ngIf="formGroup.controls['address'].get('house').hasError('required')">To pole jest wymagane</div>
  61. <div *ngIf="formGroup.controls['address'].get('house').hasError('minlength')">Numer domu nie może mniejszy niż 1
  62. </div>
  63. </mat-error>
  64. </mat-form-field>
  65. </div>
  66. <div formArrayName="table" *ngFor="let student of formGroup.controls['table'].controls; let i = index;">
  67. <div [formGroupName]="i">
  68. <mat-form-field appearance="outline">
  69. <mat-label>Imię studenta</mat-label>
  70. <input matInput formControlName="studentName">
  71. <mat-error>
  72. <div *ngIf="formGroup.controls['address'].get('house').hasError('required')">To pole jest wymagane</div>
  73. </mat-error>
  74. </mat-form-field>
  75. <mat-form-field appearance="outline">
  76. <mat-label>Nazwisko studenta</mat-label>
  77. <input matInput formControlName="studentSurname">
  78. <mat-error>
  79. <div *ngIf="formGroup.controls['address'].get('house').hasError('required')">To pole jest wymagane</div>
  80. </mat-error>
  81. </mat-form-field>
  82. <button *ngIf="i === 0" mat-icon-button (click)="add()"><mat-icon>add_circle</mat-icon></button>
  83. </div>
  84. </div>
  85. <button mat-button type="submit" [disabled]="formGroup.invalid">Wyślij</button>
  86. </form>
Angular - widok dynamicznie rozszerzalnego formularza
Rys. 1
Angular - widok dynamicznie rozszerzalnego formularza

Komentarze