Nested Angular Reactive Forms
How to create manageable Angular reactive forms in a variety of situations: creating nested and re-usable form components and forms across multiple routes using the ControlContainer class.
October 11, 2019
Dealing with forms is rarely simple anymore. Many SaaS products offer highly customizable setups managed by complex and dynamic forms. Knowing how to use Angulars ControlContainer will give you more control, heh, over managing your forms.
ControlContainer
The ControlContainer is a base class for form directives that contain multiple registered instances of NgControl. We can use the ControlContainer to access FormControls, FormGroups, and FormArrays and manage a main form chunked across components.
A common situation is having a group of form controls, like an “address” groups of fields like “street”, “city”, “zip” that you use repeatedly across your application. To use bind a Reactive FormControl to the DOM we need access to that FormControl - and can use the FormGroup directive to pass a main FormGroup instance to nested components.
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-span-form',
template: `
<form [formGroup]="sampleForm">
<div class="form-group">
<label for="name">First Name</label>
<input name="first_name" formControlName="first_name" />
</div>
<div class="form-group">
<label for="name">Last Name</label>
<input name="last_name" formControlName="last_name" />
</div>
<div class="form-group">
<label for="name">Email Address</label>
<input name="email" formControlName="email" />
</div>
<app-address></app-address>
</form>
`,
styleUrls: ['./span-form.component.less']
})
export class SpanFormComponent implements OnInit {
public sampleForm: FormGroup;
constructor(
private fb: FormBuilder
) { }
ngOnInit() {
this.sampleForm = this.fb.group({
user_name: ['', Validators.required],
first_name: ['',Validators.required],
last_name: ['',Validators.required],
email: ['',Validators.required],
street: ['',Validators.required],
city: ['',Validators.required],
state: ['',Validators.required],
zip: ['',Validators.required]
})
}
}
Inside our reusable Address component we can access the sampleForm
by injecting the ControlContainer class in our constructor - this will return the parent FormGroupDirective and allow us to access that control. From there we can use the formControlName
for our FormControls as expected.
import { Component, OnInit } from '@angular/core';
import { ControlContainer } from '@angular/forms';
@Component({
selector: 'app-address',
template: `
<form *ngIf="ogFormGroup" [formGroup]="ogFormGroup">
<h5>Address:</h5>
<div class="form-group">
<label for="name">Street Name</label>
<input formControlName="street" />
</div>
<div class="form-group">
<label for="name">City</label>
<input formControlName="city" />
</div>
<div class="form-group">
<label for="name">State</label>
<input formControlName="state" />
</div>
<div class="form-group">
<label for="name">Zip</label>
<input formControlName="zip" />
</div>
</form>
`,
styleUrls: ['./address.component.less']
})
export class AddressComponent implements OnInit {
public ogFormGroup;
constructor(public controlContainer: ControlContainer) {
}
ngOnInit() {
this.ogFormGroup = this.controlContainer.control;
}
}
We can use the same approach in other situations, for example a form that’s presented across multiple paths.
//parent component where form is initialized
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-span-form',
template: `
<button routerLink="step-1">Step 1</button>
<button routerLink="step-2">Step 2</button>
<button routerLink="step-3">Step 3</button>
<form [formGroup]="mainForm" class="form-ui">
<router-outlet></router-outlet>
</form>
`,
styleUrls: ['./span-form.component.less']
})
export class SpanFormComponent implements OnInit {
public mainForm: FormGroup;
constructor(
private fb: FormBuilder
) { }
ngOnInit() {
this.mainForm = this.fb.group({
user_name: ['', Validators.required],
first_name: ['',Validators.required],
last_name: ['',Validators.required],
email: ['',Validators.required],
address: this.fb.group({
street: ['',Validators.required],
city: ['',Validators.required],
state: ['',Validators.required],
zip: ['',Validators.required]
}),
favorite_color: ['',Validators.required],
favorite_food: ['',Validators.required],
favorite_season: ['',Validators.required],
favorite_episode: ['',Validators.required]
})
}
}
In the child component we access the parent formDirective through the ControlContainer
class and bind it to a new form directive in our component and include the formControls we want to present to the user.
// child component at nested route displaying part of main form:
import { Component, OnInit } from '@angular/core';
import { ControlContainer } from '@angular/forms';
@Component({
selector: 'app-step1',
template: `
<h3>Step One of Our Sign-in Process</h3>
<form [formGroup]="parentForm">
<div class="form-group">
<label for="name">Choose a User Name</label>
<input name="user_name" formControlName="user_name" />
</div>
</form>
<button [disabled]="!parentForm.controls.user_name.valid"
routerLink="/signup/step-2">Next</button>
`,
styleUrls: ['./step1.component.less']
})
export class Step1Component implements OnInit {
public parentForm;
constructor(private controlContainer: ControlContainer) {
}
ngOnInit() {
this.parentForm = this.controlContainer.control;
}
}
Code demo here: https://github.com/tehfedaykin/ControlContainerExample
Cheers from a dairy farm 🐮in New Zealand!