Introduction
In large-scale applications, multi-step forms are common — especially in complex workflows like company registration. However, managing many steps can lead to unnecessary complexity and a poor user experience.
In my current project, our registration process spanned 7–10 steps, with each step built as a standalone Angular component containing a template-driven form. To improve the experience, we decided to reduce the process to just 3 main steps — without rewriting the existing form components from scratch.
This post explains how I achieved this using Angular’s ControlContainer and dynamic component loading with ComponentFactoryResolver.
The Challenge
- Original setup:
- 7–10 Angular components, each containing a template-driven form.
- Navigation between components was based on previous form values.
- Goal: Merge multiple pages into fewer steps (from 7–10 → 3) to improve UX.
- Constraints:
- Minimal code changes in existing components.
- Preserve form validation and data binding.
The Solution
1. Step Factory Component
I created a Step Factory Component that dynamically loads existing form components at runtime, based on the step configuration.
import { Component, ComponentFactoryResolver, ViewChild, ViewContainerRef, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-step-factory',
template: `<ng-template #container></ng-template>`
})
export class StepFactoryComponent implements OnInit {
@ViewChild('container', { read: ViewContainerRef, static: true }) container!: ViewContainerRef;
@Input() component!: any;
constructor(private cfr: ComponentFactoryResolver) {}
ngOnInit() {
const factory = this.cfr.resolveComponentFactory(this.component);
this.container.createComponent(factory);
}
}
This component takes a step component class (e.g., CompanyDetailsComponent, ContactInfoComponent) and renders it dynamically.
2. Using ControlContainer for Form Context
When embedding a form component inside a parent form, the child’s inputs won’t automatically register with the parent form’s NgForm.
ControlContainer lets us reuse existing template-driven form components without breaking validation or submission.
parent-form.component.html
<form #parentForm="ngForm" (ngSubmit)="onSubmit(parentForm)">
<app-company-details></app-company-details>
<app-contact-info></app-contact-info>
<button type="submit">Submit</button>
</form>
parent-form.component.ts
import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
@Component({
selector: 'app-parent-form',
templateUrl: './parent-form.component.html'
})
export class ParentFormComponent {
onSubmit(parentForm: NgForm) {
console.log('Entire form value:', parentForm.value);
console.log('Is form valid?', parentForm.valid);
// Access child form fields directly
console.log('Company Name:', parentForm.value.companyName);
console.log('Tax Code:', parentForm.value.taxCode);
console.log('Contact Email:', parentForm.value.contactEmail);
}
}
company-details.component.ts
import { Component } from '@angular/core';
import { ControlContainer, NgForm } from '@angular/forms';
@Component({
selector: 'app-company-details',
templateUrl: './company-details.component.html',
// This line makes the child form fields part of the parent NgForm
viewProviders: [{ provide: ControlContainer, useExisting: NgForm }]
})
export class CompanyDetailsComponent {
companyName = '';
taxCode = '';
}
company-details.component.html
<div>
<label>Company Name:</label>
<input name="companyName" [(ngModel)]="companyName" required />
<label>Tax Code:</label>
<input name="taxCode" [(ngModel)]="taxCode" required />
</div>
Here’s what this does:
- viewProviders tells Angular: “Use the parent’s NgForm for this component’s form controls.”
- No extra form tag in the child template — it just declares fields, but they’re registered with the parent form.
- This allows validation, dirty state, and submission to be handled at the parent level.
Benefits of This Approach
- Minimal Code Changes: Existing form components remain mostly untouched.
- Reusable Components: Dynamic loading means the same form components can be reused in different steps or workflows.
- Preserved Validation: ControlContainer ensures that nested forms work as part of the parent form.
- Flexible Step Management: You can easily adjust step order or grouping via configuration.
Final Thoughts
If you have a large, multi-step form in Angular and need to restructure it without rewriting everything, combining ControlContainer with dynamic component loading is a powerful pattern. It allows you to keep your existing form logic while making your registration flow more user-friendly and maintainable.
This technique works especially well for template-driven forms and long workflows in enterprise applications.
For new projects, consider Reactive Forms for more control over form state and structure, but if you’re working with existing template-driven forms, this approach can save you significant refactoring time.