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.
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:
providing the shipping address
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.
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.
I also encourage you to familiarize yourself with the interesting use of ControlContainer, which has been presented by Netanel Basal on his blog.
Frequently asked questions
How do I split a multi-step form across different routes in Angular?
Put a [formGroup] directive on a <form> in the parent component and place <router-outlet> inside it, so each routed step renders within the same FormGroup. Each step then reads its part of the form through an injected ControlContainer, with the parent form holding nested groups such as address and creditCard. This keeps one form instance alive while the user navigates between routes like address and credit-card.
How do I inject ControlContainer to access a parent FormGroup in a child component?
Inject ControlContainer from @angular/forms in the child component's constructor; it resolves to the FormGroupDirective of the nearest ancestor element that has [formGroup]. Then read the nested control with this.controlContainer.control.get('creditCard') as FormGroup. Because the lookup is performed by the ElementInjector, the child must be rendered inside that form element, which is why <router-outlet> is placed within it.
When should I pass the form group from the parent instead of getting it via ControlContainer in a child component?
Selecting the group with this.controlContainer.control.get('creditCard') in the child is the simplest approach, but it forces every child to use the same control name across the application. Alternatively, bind [formGroup]="form[selectedStep]" on the parent form so the parent decides which group the routed child uses. This keeps child components reusable, since they no longer depend on a hardcoded control name.
How does ControlContainer get resolved to FormGroupDirective in Angular?
FormGroupDirective provides itself as ControlContainer through the formDirectiveProvider, which sets provide: ControlContainer and useExisting: forwardRef(() => FormGroupDirective). As a result, any component inside an element with [formGroup] can inject ControlContainer and receive the same directive instance. Angular's ElementInjector performs this resolution, following the DOM hierarchy of the template.