NashTech Blog

Simplifying Multi-Step Forms in Angular Using ControlContainer and Dynamic Components

Table of Contents

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.

Picture of San Nguyen

San Nguyen

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top