Tworzysz zagnieżdżony formularz? Chcesz, żeby poszczególne kroki formularza występowały na różnych ścieżkach routingu? Jeśli tak, to ten wpis jest dla Ciebie ? Poznaj moc ControlContainer!
Artykuł napisano na podstawie wystąpienia Jennifer Wadella podczas tegorocznego ngConf, którego byliśmy partnerem. Niestety wystąpienie nie zostało jeszcze udostępnione na oficjalnym kanale YouTube ng-Conf. Jak tylko się pojawi, dołączymy je do artykułu.
ControlContainer
Jak możemy przeczytać w dokumentacji – “ControlContainer jest klasą bazową dla dyrektyw, które zawierają wiele zarejestrowanych instancji NgControl”. Tego typu dyrektywą jest np. FormGroup. Spoglądając w kod źródłowy, zauważymy, że providuje ona samą siebie jako ControlContainer.
export const formDirectiveProvider: any = {
provide: ControlContainer,
useExisting: forwardRef(() => FormGroupDirective)
};
@Directive({
selector: '[formGroup]',
providers: [formDirectiveProvider],
host: {'(submit)': 'onSubmit($event)', '(reset)': 'onReset()'},
exportAs: 'ngForm'
})
Po nałożeniu wspomnianej dyrektywy na dowolny DOM element, będziemy mogli wstrzyknąć ControlContainer (w tym wypadku będący instancją FormGroupDirective) wewnątrz tego elementu oraz jego childów. A wszystko za sprawą rozwiązywania zależności przez ElementInjector.
Implementacja
Użycie ControlContainer przedstawimy na przykładzie formularza do składania zamówień. Składa się on z dwóch kroków:
- podanie adresu wysyłki
- podanie karty kredytowej do zapłaty
Każdy z kroków znajduje się na innej ścieżce routingu.
Implementację, zaczniemy od parent komponentu, wewnątrz którego będziemy przetrzymywali instancję naszego formularza.
@Component({
selector: 'app-root',
template: `
<div class="container">
<form [formGroup]="form">
<router-outlet></router-outlet>
</form>
<button routerLink="address" type="button" mat-button>Step 1</button>
<button routerLink="credit-card" type="button" mat-button>Step 2</button>
</div>
`,
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'control-container';
form: FormGroup;
constructor(
private fb: FormBuilder,
) {
}
ngOnInit(): void {
this.initForm();
}
initForm(): void {
this.form = this.fb.group({
address: this.fb.group({
city: [''],
street: [''],
homeNumber: ['']
}),
creditCard: this.fb.group({
cardNumber: [''],
ccvNumber: [''],
expirationDate: ['']
})
});
}
}
Inicjuje on formularz, który następnie przekazujemy do dyrektywy [formGroup] nałożonej na element form w widoku. Wewnątrz <form> w miejsce <router-outlet> będzie wyświetlany odpowiedni krok formularza w zależności od ścieżki, na której się znajdujemy.
Następnie do child komponentu, będącego reprezentantem jednego z kroków, wstrzykujemy ControlContainer, poprzez który uzyskujemy dostęp do dyrektywy [formGroup] z parenta. Reszta jest już formalnością. Z uzyskanej instancji FormGroupDirective pobieramy interesującą nas pole formularza oraz wiążemy je z naszym widokiem.
@Component({
selector: 'app-credit-card-form',
template: `
<div [formGroup]="form" class="container">
<mat-form-field>
<mat-label>Card number</mat-label>
<input matInput placeholder="Card number" formControlName="cardNumber" required>
</mat-form-field>
<mat-form-field>
<mat-label>CCV number</mat-label>
<input matInput placeholder="CCV number" formControlName="ccvNumber" required>
</mat-form-field>
<mat-form-field>
<mat-label>Expiration date</mat-label>
<input matInput placeholder="Expiration date" formControlName="expirationDate" required>
</mat-form-field>
</div>
`,
styleUrls: ['./credit-card-form.component.css']
})
export class CreditCardFormComponent implements OnInit {
form: FormGroup;
constructor(private controlContainer: ControlContainer) {
}
ngOnInit(): void {
this.form = this.controlContainer.control.get('creditCard') as FormGroup;
}
}
W zależności, od preferowanego przez Ciebie sposobu implementacji i reużywania komponentów, możesz skorzystać z ControlContainera dwojako:
- wybierając interesujące Cię pole formularza na poziomie child komponentu:
this.form = this.controlContainer.control.get('creditCard') as FormGroup;
Aczkolwiek to rozwiązanie zobowiązuje, Cię do nazywania pobieranego pola w child komponencie jednakowo na przestrzeni całej aplikacji.
- przekazywać bezpośrednio do childa pole, którym ma on się posługiwać. Tutaj z poziomu parenta kontrolujemy to, do jakiego pola formularza nasz child uzyska dostęp:
<form [formGroup]="form[selectedStep]"> <router-outlet></router-outlet> </form>
Gdzie selectedStep w tym wypadku to – creditCard lub address w zależności od ścieżki na, której się znajdujemy.
Tym prostym sposobem, zaimplementowaliśmy multi step form, na różnych routach.
Kod źródłowy do kompletnego rozwiązania: https://stackblitz.com/edit/angular-love-cc
Zachęcam Was również do zapoznania się z ciekawym zastosowaniem ControlContainera, które na swoim blogu przedstawił Netanel Basal.