9. Dynamiczny formularz

Temat artykułu:

Dynamiczne wyświetlanie formularza w zależności od danych wejściowych

Co omówię:

– jak dynamicznie wyświetlać formularz w zależności od wprowadzanych danych

– jak stworzyć komponenty odpowiedzialne za obsługę klienta biznesowego oraz klienta prywatnego

Na początku popracujmy trochę nad wyglądem formularza. W app.component.scss dodaj następujący kod.

.container {
margin: 60px;
}

.card-container {
margin-top: 40px;
display: flex;
justify-content: space-between;
}

.card {
margin: 10px;
}

.info-card {
display: flex;
width: 33%;

&-form {
width: 60%;
}
}

.form-card {
width: 95%;
}

.form-container {
display: flex;
flex-direction: column;
align-items: center;
}

.form-input {
width: 50%;
}

.form-header {
display: flex;
justify-content: center;
}

oraz w app.componet.html

<div class="container">
<div class="card-container" *ngIf="!selectedCar; else carInfo">
<app-card *ngFor="let car of cars" class="card"
[identifier]="car.vin"
[title]="car.brand"
[subtitle]="car.model"
[photoSource]="car.photoSource"
[description]="'CENA: ' + car.price + ' ZŁ. VIN: ' + car.vin"
(carSelected)="carSelected($event)">
</app-card>
</div>
<ng-template #carInfo>
<div class="card-container">
<app-card class="info-card-form"
[identifier]="selectedCar.vin"
[title]="selectedCar.brand"
[subtitle]="selectedCar.model"
[photoSource]="selectedCar.photoSource"
[description]="'CENA: ' + selectedCar.price + ' ZŁ. VIN: ' + selectedCar.vin"
[isCarSelected]="!!selectedCar"
(returnToMainWindow)="resetCarSection()">
</app-card>

<mat-card class="form-card">
<mat-card-header>
<mat-card-title>Wprowadź dane klienta</mat-card-title>
</mat-card-header>
<mat-card-content>

<form class="form-container" [formGroup]="clientForm" (submit)="countDiscount()">
<mat-form-field class="form-input" appearance="outline">
<mat-label>Nazwa klienta</mat-label>
<input matInput formControlName="name"/>
</mat-form-field>

<mat-form-field class="form-input" appearance="outline">
<mat-label>Długość prowadzenia firmy ?</mat-label>
<input matInput formControlName="businessInMonths"/>
</mat-form-field>

<mat-form-field class="form-input" appearance="outline">
<mat-label>Długość posiadanego prawa jazdy ?</mat-label>
<input matInput formControlName="licenseInMonths"/>
</mat-form-field>

<mat-checkbox formControlName="isUnder25">Kierowca poniżej 25 roku ?</mat-checkbox>

<button type="submit" mat-raised-button>
Oblicz zniżkę
</button>
</form>
</mat-card-content>
</mat-card>

</div>
</ng-template>
</div>

Po dodaniu stylowania formularz powinien prezentować się znacznie lepiej.

W chwili obecnej mamy już w miarę wyglądający formularz oraz możliwość wyboru danego samochodu. To czego brakuje to możliwość obsłużenia klienta biznesowego oraz klienta prywatnego. W obu przypadkach formularz powinien posiadać inne pola do wypełnienia oraz różne strategie wyliczania zniżki. W tej lekcji dokończymy jeszcze temat formularzy  przy a w następnej podepniemy strategie do wyliczania zniżki wykorzystując w pewnym stopniu kod z artykułu o strategii.

Przejdź do form.service.ts i dodaj dwie metody. Jedna buduje formularz dla klienta detalicznego a druga dla klienta biznesowego.

import {Injectable} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';

@Injectable({
providedIn: 'root'
})
export class FormService {
constructor(private formBuilder: FormBuilder) {
}

public createPrivateClientFormGroup(): FormGroup {
return this.formBuilder.group({
clientName: ['', Validators.required],
clientLastName: ['', Validators.required],
isUnder25: [''],
licenseInMonths: ['', [Validators.required, Validators.pattern('^[0-9]*$')]],
});
}

public createBusinessClientFormGroup(): FormGroup {
return this.formBuilder.group({
companyName: ['', Validators.required],
nip: ['', [Validators.required, Validators.pattern('^[0-9]*$')]],
businessInMonths: ['', [Validators.required, Validators.pattern('^[0-9]*$')]],
});
}
}

Następnie w katalogu form utwórz dwa komponenty, business-client-form oraz private-client-form.

ng g c business-client-form oraz ng g c private-client-form.

Dodaj następujący kod w business-client-form.component.ts

import {Component, OnInit} from '@angular/core';
import {FormGroup} from '@angular/forms';
import {FormService} from '../form.service';

@Component({
selector: 'app-business-client-form',
templateUrl: './business-client-form.component.html',
styleUrls: ['./business-client-form.component.scss']
})
export class BusinessClientFormComponent implements OnInit {
public businessClientForm: FormGroup;

constructor(private formService: FormService) {
}

ngOnInit(): void {
this.businessClientForm = this.formService.createBusinessClientFormGroup();
}

public countDiscount() {
console.log(this.businessClientForm.controls);
}
}

oraz w private-client-form.component.ts

import {Component, OnInit} from '@angular/core';
import {FormGroup} from '@angular/forms';
import {FormService} from '../form.service';

@Component({
selector: 'app-private-client-form',
templateUrl: './private-client-form.component.html',
styleUrls: ['./private-client-form.component.scss']
})
export class PrivateClientFormComponent implements OnInit {

public privateClientForm: FormGroup;

constructor(private formService: FormService) {
}

ngOnInit(): void {
this.privateClientForm = this.formService.createPrivateClientFormGroup();
}

public countDiscount() {
console.log(this.privateClientForm.controls);
}
}

Przejdź do business-client-form.component.html i połącz formularz z komponentu z szablonem

<form class="form-container" [formGroup]="businessClientForm" (submit)="countDiscount()">
<mat-form-field class="form-input" appearance="outline">
<mat-label>Nazwa firmy</mat-label>
<input matInput formControlName="companyName"/>
</mat-form-field>

<mat-form-field class="form-input" appearance="outline">
<mat-label>NIP</mat-label>
<input matInput formControlName="nip"/>
</mat-form-field>

<mat-form-field class="form-input" appearance="outline">
<mat-label>Długość prowadzenia firmy ?</mat-label>
<input matInput formControlName="businessInMonths" placeholder="w miesiącach"/>
</mat-form-field>

<button type="submit" mat-raised-button>
Oblicz zniżkę
</button>
</form>

oraz w private-business-form.component.html dla przypadku klienta detalicznego.

<form class="form-container" [formGroup]="privateClientForm" (submit)="countDiscount()">
<mat-form-field class="form-input" appearance="outline">
<mat-label>Imię klienta</mat-label>
<input matInput formControlName="clientName"/>
</mat-form-field>

<mat-form-field class="form-input" appearance="outline">
<mat-label>Nazwisko klienta</mat-label>
<input matInput formControlName="clientLastName"/>
</mat-form-field>

<mat-form-field class="form-input" appearance="outline">
<mat-label>Długość posiadanego prawa jazdy ?</mat-label>
<input matInput formControlName="licenseInMonths" placeholder="w miesiącach"/>
</mat-form-field>

<mat-checkbox formControlName="isUnder25">Kierowca poniżej 25 roku ?</mat-checkbox>

<button type="submit" mat-raised-button>
Oblicz zniżkę
</button>
</form>

Dzięki temu zabiegowi mamy odseparowaną obsługę dwóch przypadków biznesowych.

Cssy w obu przypadkach będą takie same dlatego można wydzielić jeden plik scss dla dwóch komponentów. Usuń pliki business-client-form.component.scss oraz private-client-form.component.scss i stwórz jeden plik scss o nazwie client-form.component.scss na poziomie katalogu form. 

.form-container {
display: flex;
flex-direction: column;
align-items: center;
}

.form-input {
width: 50%;
}

W obu komponentach dodaj w stylesUrls namiar na wspólny plik scss.

import {Component, OnInit} from '@angular/core';
import {FormGroup} from '@angular/forms';
import {FormService} from '../form.service';

@Component({
selector: 'app-business-client-form',
templateUrl: './business-client-form.component.html',
styleUrls: ['../client-form.component.scss']
})
export class BusinessClientFormComponent implements OnInit {
public businessClientForm: FormGroup;

constructor(private formService: FormService) {
}

ngOnInit(): void {
this.businessClientForm = this.formService.createBusinessClientFormGroup();
}

public countDiscount() {
console.log(this.businessClientForm.controls);
}
}

Mamy już dwa komponenty gotowe obsłużyć każdy z przypadków biznesowych. Musimy teraz przygotować  główny komponent na obsługę tych przypadków. To co będziemy chcieli teraz uzyskać to możliwość wyboru czy dany klient jest klientem biznesowym czy prywatnym. W pliku card.component.ts dodamy drugi przycisk o nazwie „FIRMA” oraz przycisk „WYBIERZ” zmienimy na przycisk „PRYWATNIE”.

Przejdź do pliku card.component.html i dodaj poniższy kod.

<mat-card class="card">
<mat-card-header>
<mat-card-title>{{title}}</mat-card-title>
<mat-card-subtitle>{{subtitle}}</mat-card-subtitle>
</mat-card-header>
<img mat-card-image src="{{photoSource}}">
<mat-card-content>
<p>{{description}}</p>
</mat-card-content>
<mat-card-actions>
<ng-container *ngIf="!isCharacterSelected">
<button mat-raised-button color="warn" (click)="selectedCar(ClientType.BUSINESS)">FIRMA
</button>
<button mat-raised-button color="primary" (click)="selectedCar(ClientType.PRIVATE)">PRYWATNIE
</button>
</ng-container>
<button *ngIf="isCharacterSelected" mat-raised-button color="primary"
(click)="backToMainWindow()">WRÓĆ
</button>
</mat-card-actions>
</mat-card>

Pewnie zauważyłeś, że zmieniła się metoda selectedCar w tym momencie oczekuje ona parametru typu ClientType. Jest to prosty enum który posiada dwie wartości.

Stwórz katalog enums na poziomie katalogu app oraz dodaj w nim klasę client-type.enum.ts.

export enum ClientType {
PRIVATE = 'PRIVATE',
BUSINESS = 'BUSINESS'
}

Pozostaje jeszcze dostosować komponent card.component.ts

import { Component, EventEmitter, Input, Output } from '@angular/core';
import { OfferType } from '../interfaces/offer-type.interface';
import { ClientType } from '../enums/client-type.enum';

@Component({
selector: 'app-card',
templateUrl: './card.component.html',
styleUrls: ['./card.component.scss']
})
export class CardComponent {
@Input()
public identifier: string;
@Input()
public title: string;
@Input()
public subtitle: string;
@Input()
public photoSource: string;
@Input()
public description: string;
@Input()
public isCarSelected: boolean;
@Output()
public carSelected = new EventEmitter<OfferType>();
@Output()
public returnToMainWindow = new EventEmitter();
public ClientType = ClientType;

public selectedCar(clientType: ClientType): void {
this.carSelected.emit({clientType, carVin: this.identifier});
}

public backToMainWindow(): void {
this.returnToMainWindow.emit();
}
}

Ponownie musimy dostosować metodę selectedCar ponieważ przyjmuje ona inny typ niż poprzednio oraz EventEmitter carSelected wysła w tym momencie nie tylko nr vin samochodu ale także kontekst klienta.

Na poziomie katalogu app stwórz katalog interfaces i dodaj w nim plik offer-type.interface.ts 

import { ClientType } from '../enums/client-type.enum';

export interface OfferType {
clientType: ClientType;
carVin: string;
}

Przejdź do pliku app.component.ts i dostosuj obsługę EventEmittera carSelcted

import {Component, OnInit} from '@angular/core';
import {CarService} from './car/car.service';
import {CarModel} from './car/car.model';
import {FormGroup} from '@angular/forms';
import {OfferType} from './interfaces/offer-type.interface';
import {ClientType} from './enums/client-type.enum';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
selectedCar: CarModel;
cars: CarModel[] = [];
clientForm: FormGroup;
clientType: ClientType;
public ClientType = ClientType;

public ngOnInit(): void {
this.carService.fetchAvailableCars().subscribe(cars => {
this.cars = cars;
});
}

constructor(private carService: CarService) {
}

public carSelected(offerType: OfferType): void {
this.selectedCar = this.cars
.find(car => car.vin === offerType.carVin);
this.clientType = offerType.clientType;
}

public resetCarSection() {
this.selectedCar = null;
}
}

W metodzie carSelected przypisujemy w tym momencie wartość clientType, którą dostaliśmy z card.component, do lokalnej zmiennej this.clientType. Dzięki tej zmiennej będziemy wiedzieli którego komponentu form użyć, czy dla przypadku biznesowego czy dla przypadku detalicznego.

Spójrz na kod w app.component.html

<div class="container">
<div class="card-container" *ngIf="!selectedCar; else carInfo">
<app-card *ngFor="let car of cars" class="card"
[identifier]="car.vin"
[title]="car.brand"
[subtitle]="car.model"
[photoSource]="car.photoSource"
[description]="'CENA: ' + car.price + ' ZŁ. VIN: ' + car.vin"
(carSelected)="carSelected($event)">
</app-card>
</div>
<ng-template #carInfo>
<div class="card-container">
<app-card class="info-card-form"
[identifier]="selectedCar.vin"
[title]="selectedCar.brand"
[subtitle]="selectedCar.model"
[photoSource]="selectedCar.photoSource"
[description]="'CENA: ' + selectedCar.price + ' ZŁ. VIN: ' + selectedCar.vin"
[isCarSelected]="!!selectedCar"
(returnToMainWindow)="resetCarSection()">
</app-card>

<mat-card class="form-card">
<mat-card-header class="form-header">
<mat-card-title>
Klient
<ng-container *ngIf="clientType === ClientType.PRIVATE">prywatny</ng-container> <!--1-->
<ng-container *ngIf="clientType === ClientType.BUSINESS">biznesowy</ng-container> <!--1-->
wprowadź dane
</mat-card-title>
</mat-card-header>
<mat-card-content>
<app-private-client-form *ngIf="clientType === ClientType.PRIVATE"> <!--2-->
</app-private-client-form>
<app-business-client-form *ngIf="clientType === ClientType.BUSINESS"> <!--2-->
</app-business-client-form>

</mat-card-content>
</mat-card>

</div>
</ng-template>
</div>

1) w tym miejscu dodajemy sprawdzenie w jakim kontekscie klienta działamy czy biznesowym czy detalicznym. W zależności od kontekstu wyświetlamy odpowiednią informację w nagłówku formularza.

2) tutaj także sprawdzamy rodzaj klienta. W zależności od rodzaju wyświetlamy odpowiedni komponent z odpowiednio przygotowanym formularzem.

Mam nadzieję, że wszystko poszło dobrze i zobaczyć następujące ekrany.

Dla klienta biznesowego

oraz dla klienta prywatnego