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

Autor podstrony: Krzysztof Zajączkowski

Stronę tą wyświetlono już: 2725 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ą:

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

Zaś w kodzie HTML:

<mat-form-field appearance="outline"> <mat-label>Kraje</mat-label> <mat-select [formControl]="selectCountry"> <mat-option *ngFor="let country of countries" [value]="country.id"> {{country.name}} </mat-option> </mat-select> </mat-form-field>

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

export class MyValidators { static frobiddenCountry(forbiddenCountry: string): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const nameRe: RegExp = new RegExp(forbiddenCountry, 'i'); const forbidden = nameRe.test(control.value); return forbiddenCountry ? (forbidden ? { forbiddenCountry: true } : null) : null; }; } }

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

myCountry: FormControl; ngOnInit() { this.myCountry = new FormControl('Polska', MyValidators.frobiddenCountry('Rosja')); }

i w kodzie HTML:

<mat-form-field appearance="outline"> <mat-label>Kraj</mat-label> <input matInput [formControl]="myCountry"> <mat-error> <span *ngIf="myCountry.hasError('forbiddenCountry')">Rosja nigdy! Rosja nigdy!</span> </mat-error> </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:

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

W kodzie HTML komponentu:

<form [formGroup]="formGroup"> <mat-form-field appearance="outline"> <mat-label>Kraj</mat-label> <input matInput formControlName="yourCountry"> <mat-error> <div *ngIf="formGroup.get('yourCountry').hasError('required')">To pole jest wymagane</div> <div *ngIf="formGroup.get('yourCountry').hasError('minlength')">Długość ciągu znaków krótsza od 5</div> <div *ngIf="formGroup.get('yourCountry').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div> </mat-error> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Imię</mat-label> <input matInput formControlName="firstName"> <mat-error> <div *ngIf="formGroup.get('firstName').hasError('required')">To pole jest wymagane</div> <div *ngIf="formGroup.get('firstName').hasError('minlength')">Długość ciągu znaków krótsza od 5</div> <div *ngIf="formGroup.get('firstName').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div> </mat-error> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Nazwisko</mat-label> <input matInput formControlName="lastName"> <mat-error> <div *ngIf="formGroup.get('lastName').hasError('required')">To pole jest wymagane</div> <div *ngIf="formGroup.get('lastName').hasError('minlength')">Długość ciągu znaków krótsza od 5</div> <div *ngIf="formGroup.get('lastName').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div> </mat-error> </mat-form-field> <button mat-button type="submit" [disabled]="formGroup.invalid">Wyślij</button> </form>

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

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:

<form [formGroup]="formGroup"> <mat-form-field appearance="outline"> <mat-label>Kraj</mat-label> <input matInput formControlName="yourCountry"> <mat-error> <div *ngIf="formGroup.get('yourCountry').hasError('required')">To pole jest wymagane</div> <div *ngIf="formGroup.get('yourCountry').hasError('minlength')">Długość ciągu znaków krótsza od 5</div> <div *ngIf="formGroup.get('yourCountry').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div> </mat-error> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Imię</mat-label> <input matInput formControlName="firstName"> <mat-error> <div *ngIf="formGroup.get('firstName').hasError('required')">To pole jest wymagane</div> <div *ngIf="formGroup.get('firstName').hasError('minlength')">Długość ciągu znaków krótsza od 5</div> <div *ngIf="formGroup.get('firstName').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div> </mat-error> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Nazwisko</mat-label> <input matInput formControlName="lastName"> <mat-error> <div *ngIf="formGroup.get('lastName').hasError('required')">To pole jest wymagane</div> <div *ngIf="formGroup.get('lastName').hasError('minlength')">Długość ciągu znaków krótsza od 5</div> <div *ngIf="formGroup.get('lastName').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div> </mat-error> </mat-form-field> <div formGroupName="address"> <mat-form-field appearance="outline"> <mat-label>Ulica</mat-label> <input matInput formControlName="street"> <mat-error> <div *ngIf="formGroup.controls['address'].get('street').hasError('required')">To pole jest wymagane</div> <div *ngIf="formGroup.controls['address'].get('street').hasError('minlength')">Długość ciągu znaków krótsza od 5</div> <div *ngIf="formGroup.controls['address'].get('street').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div> </mat-error> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Numer domu</mat-label> <input matInput type="number" formControlName="house"> <mat-error> <div *ngIf="formGroup.controls['address'].get('house').hasError('required')">To pole jest wymagane</div> <div *ngIf="formGroup.controls['address'].get('house').hasError('minlength')">Numer domu nie może mniejszy niż 1</div> </mat-error> </mat-form-field> </div> <button mat-button type="submit" [disabled]="formGroup.invalid">Wyślij</button> </form>

I kod komponentu:

formGroup: FormGroup; addressGroup: FormGroup; ngOnInit() { this.addressGroup = this.formBuilder.group({ street: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(20)]], house: [1, [Validators.required, Validators.min(1)]] }); this.addressGroup.patchValue( {street: 'Zadupie Wielkie', house: 10 }); this.formGroup = this.formBuilder.group({ yourCountry: ['Polska', [Validators.required]], firstName: ['', [Validators.required, Validators.maxLength(20), Validators.minLength(5)]], lastName: ['', [Validators.required, Validators.maxLength(20), Validators.minLength(5)]], address: this.addressGroup }); this.formGroup.patchValue({ yourCountry: 'Polska', firstName: 'Grzegorz', lastName: 'Brzęczyszczykiewicz' }); console.log(this.formGroup.getRawValue()); }

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:

table: FormArray; students = [{ studentName: 'Grzegorz', studentSurname: 'Brzęczyszczykiwicz' }, { studentName: 'Marian', studentSurname: 'Pietrucha' }]; ngOnInit() { this.addressGroup = this.formBuilder.group({ street: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(20)]], house: [1, [Validators.required, Validators.min(1)]] }); this.addressGroup.patchValue({ street: 'Zadupie Wielkie', house: 10 }); this.table = this.formBuilder.array([ this.formBuilder.group( { studentName: ['', Validators.required], studentSurname: ['', Validators.required] }) ]); this.formGroup = this.formBuilder.group({ yourCountry: ['Polska', [Validators.required]], firstName: ['', [Validators.required, Validators.maxLength(20), Validators.minLength(5)]], lastName: ['', [Validators.required, Validators.maxLength(20), Validators.minLength(5)]], address: this.addressGroup, table: this.table } ); this.students.forEach((student) => { this.table.push(this.formBuilder.group(student)); }); this.formGroup.patchValue({ yourCountry: 'Polska', firstName: 'Grzegorz', lastName: 'Brzęczyszczykiewicz' }); console.log(this.formGroup.getRawValue()); this.selectCountry = new FormControl(2, Validators.required); this.myCountry = new FormControl('Polska', MyValidators.frobiddenCountry('Rosja')); }

Zaś w kodzie HTML:

<mat-form-field appearance="outline"> <mat-label>Kraje</mat-label> <mat-select [formControl]="selectCountry"> <mat-option *ngFor="let country of countries" [value]="country.id"> {{country.name}} </mat-option> </mat-select> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Kraj</mat-label> <input matInput [formControl]="myCountry"> <mat-error> <div *ngIf="myCountry.hasError('forbiddenCountry')">Rosja nigdy! Rosja nigdy!</div> </mat-error> </mat-form-field> <form [formGroup]="formGroup"> <mat-form-field appearance="outline"> <mat-label>Kraj</mat-label> <input matInput formControlName="yourCountry"> <mat-error> <div *ngIf="formGroup.get('yourCountry').hasError('required')">To pole jest wymagane</div> <div *ngIf="formGroup.get('yourCountry').hasError('minlength')">Długość ciągu znaków krótsza od 5</div> <div *ngIf="formGroup.get('yourCountry').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div> </mat-error> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Imię</mat-label> <input matInput formControlName="firstName"> <mat-error> <div *ngIf="formGroup.get('firstName').hasError('required')">To pole jest wymagane</div> <div *ngIf="formGroup.get('firstName').hasError('minlength')">Długość ciągu znaków krótsza od 5</div> <div *ngIf="formGroup.get('firstName').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div> </mat-error> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Nazwisko</mat-label> <input matInput formControlName="lastName"> <mat-error> <div *ngIf="formGroup.get('lastName').hasError('required')">To pole jest wymagane</div> <div *ngIf="formGroup.get('lastName').hasError('minlength')">Długość ciągu znaków krótsza od 5</div> <div *ngIf="formGroup.get('lastName').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div> </mat-error> </mat-form-field> <div formGroupName="address"> <mat-form-field appearance="outline"> <mat-label>Ulica</mat-label> <input matInput formControlName="street"> <mat-error> <div *ngIf="formGroup.controls['address'].get('street').hasError('required')">To pole jest wymagane</div> <div *ngIf="formGroup.controls['address'].get('street').hasError('minlength')">Długość ciągu znaków krótsza od 5 </div> <div *ngIf="formGroup.controls['address'].get('street').hasError('maxlength')">Długość ciągu znaków dłuższa od 20</div> </mat-error> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Numer domu</mat-label> <input matInput type="number" formControlName="house"> <mat-error> <div *ngIf="formGroup.controls['address'].get('house').hasError('required')">To pole jest wymagane</div> <div *ngIf="formGroup.controls['address'].get('house').hasError('minlength')">Numer domu nie może mniejszy niż 1 </div> </mat-error> </mat-form-field> </div> <div formArrayName="table" *ngFor="let student of formGroup.controls['table'].controls; let i = index;"> <div [formGroupName]="i"> <mat-form-field appearance="outline"> <mat-label>Imię studenta</mat-label> <input matInput formControlName="studentName"> <mat-error> <div *ngIf="formGroup.controls['address'].get('house').hasError('required')">To pole jest wymagane</div> </mat-error> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Nazwisko studenta</mat-label> <input matInput formControlName="studentSurname"> <mat-error> <div *ngIf="formGroup.controls['address'].get('house').hasError('required')">To pole jest wymagane</div> </mat-error> </mat-form-field> <button *ngIf="i === 0" mat-icon-button (click)="add()"><mat-icon>add_circle</mat-icon></button> </div> </div> <button mat-button type="submit" [disabled]="formGroup.invalid">Wyślij</button> </form>
Angular - widok dynamicznie rozszerzalnego formularza
Rys. 1
Angular - widok dynamicznie rozszerzalnego formularza
Propozycje książek