05 cze 2020
5 min

Zagnieżdżone formularze z ControlContainer

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:

  1. podanie adresu wysyłki
  2. 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
Z
achęcam Was również do zapoznania się z ciekawym zastosowaniem ControlContainera, które na swoim blogu przedstawił Netanel Basal.

Podziel się artykułem

Zapisz się na nasz newsletter

Dołącz do community Angular.love i bądź na bieżąco z trendami.