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.
1 2 3 4 5 6 7 8 9 10 11 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
@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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
@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:
1 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:
123 <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.
No dobre, dobre… dzięki za posta.
Jak to się ma do Reactive Forms?
Cześć Emil,
O jakie konkretnie kwestie związane z Reactive Forms pytasz? Przykładowa implementacja przedstawiona na stackblitz działa w oparciu o Reactive Forms.
Pozdrawiam.
Jak Ja żałuję, że tego wcześniej nie znałem. Taski zajęły by zdecydowanie mniej czasu:)
Można jeszcze inaczej korzystać z podformularzy: https://stackblitz.com/edit/workshop-angular-sub-form
Świetne rozwiązanie, to wystąpienie to jest chyba to: https://www.youtube.com/watch?v=Ovpm8qZYvQY