This article was written against RC 1 of Angular 2, which was the current release at the time of writing. A series of planned renames will land with RC 2, most of which can be applied through simple find-and-replace. The full list of those renames appears in the summary of this design document.

When an application has to present many forms that share a similar shape, a form generator can cut down the repetitive work considerably. Such a generator also opens the door for server-driven UI, where the backend dictates which controls appear on screen.

Angular's imperative forms model makes the implementation of such a form generator fairly straightforward, as described in the official documentation. To push this concept further, you can involve the DynamicComponentLoader, which allows components to be inserted into the tree at runtime. That way, a form description can reference controls that the application has never seen at compile time.

This article walks through one such implementation. The central assumption is that every dynamically injected control implements the ControlValueAccessor interface, so it integrates smoothly with Angular's forms machinery. The complete code sample is available on GitHub.

With this approach, a component contributes metadata that describes the form it wants to use. For simplicity, that metadata is split into two pieces: a ControlGroup that drives the imperative forms API, and an array called elements that carries additional details about each control. Both are bundled into a single object named formMetaData:

@Component({
    selector: 'flight-search',  
    template: require('./flight-search.component.html'),
    directives: [DynamicFormComponent]
})
export class FlightSearchImpComponent {

    public filter: ControlGroup;
    public formMetaData;

    constructor(
        private flugService: FlugService,
        private fb: FormBuilder) {

            this.filter = fb.group({
               from: [
                    'Graz',
                    Validators.compose([
                        Validators.required, 
                        Validators.minLength(3),
                        Validators.maxLength(50),
                        OrtValidator.validateWithParams(['Graz', 'Wien', 'Hamburg']),
                        Validators.pattern("[a-zA-Z0-9]+")
                    ]),
                    Validators.composeAsync([
                        OrtAsyncValidator.validateAsync
                    ])
               ],
               to: ['Hamburg'],
               date: ['2016-05-01']
            });

            var elements = [
                { fieldName: 'from', label: 'From' },
                { fieldName: 'to', label: 'To' },
                { fieldName: 'date', label: 'Datum', controlName: 'date-control' }
            ];

            this.formMetaData = {
                controlGroup: this.filter,
                elements: elements  
            };
    }

    [...]
}

For each field, the property controlName declares which control should handle that field. In the sample, this mechanism tells the form to use the date-control component for the property date.

The metadata object is handed over to a dynamic-form component, whose implementation is the subject of the next section.

<dynamic-form [formMetaData]="formMetaData">
</dynamic-form>  

The DynamicFormComponent

Inside the dynamic-form component, the incoming metadata is consumed in the template to build the form:

import { Component, Input } from '@angular/core';

@Component({
    selector: 'dynamic-form',
    template: require('./dynamic-form.component.html')    
})
export class DynamicFormComponent {

    @Input() formMetaData;

}

The form itself is tied to the ControlGroup through ngFormModel, while the loop over elements produces one control per entry. Each rendered control is linked to a specific Control inside the group via the ngControl attribute, which receives the corresponding control's name.

In the default case, a plain input element is rendered. But when an entry uses the property controlName set to date-control, the template switches to a date-control element instead — a small custom control meant for editing date values. Its implementation is described in the linked post.

<form [ngFormModel]="formMetaData.controlGroup">

    <h2>Form Generator with dynamic Components</h2>

    <div *ngFor="let entry of formMetaData.elements" class="form-group">

        <div *ngIf="!entry.controlName && !entry.control">
            <label>{{entry.label}}</label>
            <input [ngControl]="entry.fieldName" class="form-control">
        </div>

        <!-- Issue: Template has to know all Controls here -->
        <div *ngIf="entry.controlName == 'date-control'">
            <label>{{entry.label}}</label>
            <date-control [ngControl]="entry.fieldName"></date-control>
        </div>

    </div>

    <ng-content></ng-content>

</form>

This setup works, but it carries a notable drawback: the DynamicFormComponent needs explicit knowledge of every control and a dedicated branch for each one. To loosen that coupling, the next step introduces runtime loading of controls.

To bring dynamic loading into play, the corresponding entry inside the elements array holds a direct reference to the control's class. In a real-world scenario, that class reference could also be fetched from a server on demand, using something like System.import.

var elements = [
    { fieldName: 'from', label: 'From' },
    { fieldName: 'to', label: 'To' },
    { fieldName: 'date', label: 'Datum', control: DateControlComponent }
    //                                              ^
    //                                              |
    //                  Component to use -----------+
];

In this design, the ControlWrapperComponent takes responsibility for the dynamic injection. As the name suggests, it acts as a shell around the component that gets loaded at runtime. It implements both the OnInit lifecycle hook and the ControlValueAccessor interface; the latter is what makes it compatible with Angular's forms system. Necessary configuration arrives through the input binding named metadata.

The wrapper exposes a few internal properties: innerComponent holds the reference to the dynamically created component, innerComponentChangeDetectorRef points to that component's change detector, and value stores the current value. Following the pattern shown in the referenced example, the constructor registers the wrapper itself as a ValueAccessor.

Whenever Angular needs to push a new value into the control, it invokes writeValue. If the inner component has already been created, the wrapper delegates the value to it and then triggers the inner change detector so the view reflects the update.

The callbacks supplied by Angular are captured in registerOnChange and registerOnTouched, and stashed away in the member variables onChange and onTouched. Later, when the user edits the displayed value, the wrapper uses these callbacks to notify Angular of the change.

The ngOnInit hook makes use of the injected DynamicComponentLoader to create the desired component. Its loadAsRoot method expects the component class, a CSS selector, and an injector. The selector decides where inside the template the new component should be placed; the injector controls which services are available to the component through dependency injection. Here, the wrapper's own injector is reused for that purpose.

Once the component is loaded, both its instance and its ChangeDetector are stored in the fields mentioned above. The current value (value) is then forwarded through writeValue to the new component, followed by a change detection run.

To stay informed about user interactions, the wrapper registers a pair of lambda expressions with registerOnChanged and registerOnTouched. These lambdas simply forward to Angular by invoking onChange and onTouched respectively.

import { Component, Input, OnInit, DynamicComponentLoader, Injector, ChangeDetectorRef } from '@angular/core';
import {ControlValueAccessor, NgControl } from '@angular/common';

@Component({
    selector: 'control-wrapper',
    template: '<span id="control"></span>'
})
export class ControlWrapperComponent 
                    implements OnInit, ControlValueAccessor {

    @Input() metadata;

    innerComponent: any;
    innerComponentChangeDetectorRef: ChangeDetectorRef;
    value: any;

    constructor(
        private c: NgControl, 
        private dcl: DynamicComponentLoader, 
        private injector: Injector) {

        c.valueAccessor = this;
    }

    writeValue(value: any) {
        this.value = value;
        if (this.innerComponent) {
            this.innerComponent.writeValue(value);
            this.innerComponentChangeDetectorRef.detectChanges();
        }
    }

    onChange = (_) => {};
    onTouched = () => {};
    registerOnChange(fn): void { this.onChange = fn; }
    registerOnTouched(fn): void { this.onTouched = fn; }

    ngOnInit() {

        this.dcl.loadAsRoot(this.metadata.control, '#control', this.injector)

            .then(compRef => {
                this.innerComponent                  = compRef.instance;
                this.innerComponentChangeDetectorRef = compRef.changeDetectorRef;

                this.innerComponent.writeValue(this.value);
                compRef.changeDetectorRef.detectChanges();

                this.innerComponent.registerOnChange((value) => {
                    this.value = value;
                    this.onChange(value); 
                });
                this.innerComponent.registerOnTouched(() => {
                    this.onTouched();
                })

            });
    }

}

With the wrapper in place, the DynamicFormComponent can rely on it without needing to know the concrete controls:

import { Component, Input } from '@angular/core';
import { ControlWrapperComponent} from '../control-wrapper/control-wrapper.component';

@Component({
    selector: 'dynamic-form',
    template: require('./dynamic-form.component.html'),
    directives: [ControlWrapperComponent]    
})
export class DynamicFormComponent {

    @Input() formMetaData;

}

The template also references the wrapper component for every field that declares a component through the control property. That property carries the metadata, including the control that ought to be loaded at runtime.

<form [ngFormModel]="formMetaData.controlGroup">

    <h2>Form Generator with dynamic Components</h2>

    <div *ngFor="let entry of formMetaData.elements" class="form-group">

        <div *ngIf="!entry.controlName && !entry.control">
            <label>{{entry.label}}</label>
            <input [ngControl]="entry.fieldName" class="form-control">
        </div>

        <div *ngIf="entry.control">
            <label>{{entry.label}}</label>
            <control-wrapper [metadata]="entry" [ngControl]="entry.fieldName"></control-wrapper>
        </div>

    </div>

    <ng-content></ng-content>

</form>