Angular - sterowanie kontrolkami, walidacja oraz tworzenie formularzy z wykorzystaniem FormControl i FormGroup z modułu ReactiveForms
Stronę tą wyświetlono już: 1655 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>


Tytuł:
Angular. Profesjonalne techniki programowania. Wydanie IV
Autor:
Adam Freeman

Tytuł:
Angular. Programowanie z użyciem języka TypeScript. Wydanie II
Autor:
Yakov Fain, Anton Moiseev

Tytuł:
ASP.NET Core, Angular i Bootstrap. Kompletny przybornik front-end developera
Autor:
Simone Chiaretta

Tytuł:
Angular instalacja i działanie. Nauka krok po kroku
Autor:
Shyam Seshadri

Tytuł:
Angular w akcji
Autor:
Jeremy Wilken

Tytuł:
ASP.NET Core 2 i Angular 5. Przewodnik dla Full-Stack Web Developera
Autor:
Valerio De Sanctis

Tytuł:
Angular. Profesjonalne techniki programowania. Wydanie II
Autor:
Adam Freeman

Tytuł:
Angular 2. Programowanie z użyciem języka TypeScript
Autor:
Yakov Fain, Anton Moiseev

Tytuł:
Angular 2. Tworzenie interaktywnych aplikacji internetowych
Autor:
Gion Kunz