This article refers to RC 1 of Angular 2, which was the current version at the time of writing. A few name changes are planned for RC 2. An adjustment will likely be possible with search and replace. See the summary at the end of this design document.

Form generators are a practical way to handle many similar forms with less effort. This approach also allows server-side use-case control to dynamically influence how forms are structured.

The imperative forms handling in Angular 2 makes implementing such a form generator quite straightforward. Details can be found in the Angular documentation. To make this approach more flexible, the DynamicComponentLoader can be brought into play. It allows components to be embedded dynamically, meaning an application can load controls that are merely referenced in a form description.

This article describes such an implementation. It assumes that the controls being loaded implement the ControlValueAccessor interface and therefore work with Angular 2's forms handling. The complete example is available here.

Metadata for the dynamic form

To generate a form with the approach described here, a component first provides metadata that describes the form. For the sake of simplicity, this metadata consists of two parts: a ControlGroup used by the imperative forms handling and an elements array containing additional data about the controls to be displayed. These two pieces of information are bundled into an 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  
            };
    }

    [...]
}

To specify a control for a particular field, the component stores its name in the controlName property. In this way, the example under discussion designates the date-control component.

This metadata is then passed on to the dynamic-form component. The implementation of that component is presented in the next section.

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

Dynamic-Form-Component

The dynamic-form component simply takes in the metadata and uses it within its template to render 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 binds to the ControlGroup in the metadata and then iterates over the elements array. For each entry, it renders a control and connects it to the Control inside the ControlGroup via the ngControl attribute. By default, a standard input element is used. However, if controlName points to date-control, the template renders a date-control. This is a simple custom control described here.

<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 approach works reasonably well, but it has a drawback: the DynamicFormComponent must know about every control and embed each one through its own branch. To remove this tight coupling, the following extensions use the ability to load controls dynamically into the page.

Loading controls dynamically

To enable dynamic loading of components, the relevant entry in the elements array gets a reference to the component. This is the class that implements the component. The example could also load it from the server at runtime, for instance using System.import.

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

In this scenario, the dynamic loading is handled by the ControlWrapperComponent presented below. As the name suggests, it serves as a wrapper for the component to be loaded dynamically. It implements the OnInit lifecycle hook as well as the ControlValueAccessor interface. The latter is necessary so that it works with Angular's forms handling. The necessary metadata is passed in through the metadata property.

In addition, it has properties for the dynamically loaded component (innerComponent), the change detector of that component (innerComponentChangeDetectorRef), and the currently represented value (value). As described in the article on custom form components, the constructor sets the component up as its own ValueAccessor.

The writeValue method, which Angular calls to set the value, stores the new value in value. If the dynamically loaded component already exists, the method also forwards this value to it and then triggers its ChangeDetector so the component can refresh its view.

The registerOnChange and registerOnTouched methods receive callbacks from Angular and store them in the members onChange and onTouched. Through these callbacks, the component later informs Angular when the user changes the displayed value.

The ngOnInit lifecycle hook uses the DynamicComponentLoader injected into the constructor to load the desired component into the wrapper's template. To do this, its loadAsRoot method takes a reference to the component class, a CSS selector, and an injector. The CSS selector determines where in the template the component should be placed. In this case, it is the span element with the id control. The injector determines which services the component can obtain via dependency injection. Here, the wrapper component's injector is used.

After loading the component, ngOnInit gets a reference to the created component instance as well as its ChangeDetector. It then passes the current value (value) to that instance via the writeValue method and subsequently triggers its ChangeDetector.

Afterwards, the wrapper component registers itself with the dynamically loaded component to stay informed about changes to the current value. To this end, it passes a lambda expression to each of the registerOnChanged and registerOnTouched methods. These lambda expressions delegate to Angular by invoking the callbacks onChange and onTouched that Angular provided. The new value received from the change event is stored by the wrapper in the value property.

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();
                })

            });
    }

}

Extending the Dynamic-Form-Component with dynamic controls

To allow the DynamicFormComponent to make use of the wrapper component, the application registers it among its directives:

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;

}

Additionally, the template uses the wrapper component for each field that references a control through the control property. This wrapper receives the metadata from which the control to be used becomes evident.

<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>