05 Jun 2020
5 min

Nested forms with ControlContainer

Are you creating a nested form? Do you want individual form steps to appear on different routing paths? If so, then this post will come in handy. Experience the power of the ControlContainer!

The article is based on Jennifer Wadell’s speech at ngConf in 2020, of which we were a partner. Unfortunately, the speech has not yet been made available on the official ng-Conf YouTube channel. As soon as it appears, it will be attached to this article.

ControlContainer

As we can read in the documentation“ControlContainer is a base class for directives that contain multiple registered instances of NgControl“. For example, FormGroup, is a type of directive. Looking at the source code, we will notice that it provides itself as a ControlContainer.

export const  formDirectiveProvider:  any  =  {
provide:  ControlContainer,
useExisting:  forwardRef(()  =>  FormGroupDirective)
};
 
@Directive({
selector:  '[formGroup]',
 providers: [formDirectiveProvider],
host: {'(submit)':  'onSubmit($event)',  '(reset)':  'onReset()'},
exportAs:  'ngForm'
})

After applying the previously mentioned directive to any DOM element, we will be able to inject the ControlContainer (in this case being an instance of FormGroupDirective) inside this element and its child component. This is all the result of the resolution of dependencies by  ElementInjector.

Implementation

The use of ControlContainer will be presented in the example of a form for order placement. It consists of two steps:

  1. providing the shipping address
  2. providing credit card information for payment

Each step is on a different routing path.

Implementation starts with the parent component, inside which the instance of our form will be kept.

@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: ['']
     })
   });
 }
}

This initiates the form, which will then be passed to the [formGroup] directive superimposed on the form element in the view. Inside <form> in place of <router-outlet>  the appropriate step of the form will be displayed depending on the path on which we are located.

Then, to the child component, which is a representative of one of the steps, the ControlContainer will be injected, through which we can access the [formGroup] directive from the parent. The rest is only a formality. From the obtained instance of  FormGroupDirective,  we download the form field we are interested in and associate it with our view.

@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;
 }
}

Depending on your preferred way of implementing and reusing components, you can use ControlContainer in two ways:

  • by selecting the form field you are interested in, on the child component level:
this.form = this.controlContainer.control.get('creditCard') as FormGroup;

However, this solution obliges you to name the downloaded field in the child component the same throughout the application.

  • transmit directly to the child’s field, in which it will be used. Here, from the parent level, we control which form field our child will access:
<form  [formGroup]="form[selectedStep]">
< router-outlet></router-outlet>
</> form
< router-outlet> form

Where selectedStep, in this case, creditCard or address depending on the path on which we are located.

In this simple way, we have implemented multi-step forms, on different routes.

Source code for a complete solution: https://stackblitz.com/edit/angular-love-ccZ

I also encourage you to familiarize yourself with the interesting use of ControlContainer, which has been presented by Netanel Basal on his blog.

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.