• 13. Juni 2016

Dynamic forms with Angular 2 and DynamicComponentLoader

This post refers to the RC 1 of Angular 2 which was the current version when it was written. For the RC 2 some renamings are planned. An adoption to it seems to be possible via find/replace. See the summary at the end of this design-document for a list of this renamings.

To simplify dealing with many similar forms, you can make use of form generators. This approach gives also a server-side use-case-control the possibility to influence the presented forms.

The imperative forms handling in Angular 2 makes it very easy to implement such a form generator. Information about this can be found in the documentation of angular. To make this approach more flexible, you can put the "DynamicComponentLoaders" into play. This allows a dynamic integration of components. Thus, an application can display controls, which are only mentioned in a form-description.

This article describes such an implementation. It assumes that every dynamically loaded control implements the interface ControlValueAccessor, which allows it to play together with Angular's mechanismns for forms-handling. The entire example can be found here.

Metadata for dynamic form

To generate a form using the approach described here, a component offers metadata for a form. For the sake of simplification, this metadata consists of two parts, namely a ControlGroup used by the imperative forms handling and an array of elements which contains additional data for the form-controls. It then stows these two pieces of information in an object with the name 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 define a control for a field, the component uses the property controlName. On this way, the considered example configures the usage of the component date-control for the property date.

This metadata is passed to the dynamic-form component. The implementation of this component can be found in the next section.

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

DynamicForm-Component

The component dynamic-form takes the passed metadata and uses it within it's template to render the form in question:

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

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

    @Input() formMetaData;

}

The form is bound to the ControlGroup using ngFormModel and the array elements is iterated. For each array-entry the sample renders a control which is bound to a Control within the mentioned ControlGroup. For this, the Attribute ngControl gets the name of the Control in question.

By default, it uses an ordinary input-element, but when there is a property controlName with the value date-control, it renders a date-control. This is a simple control that can be used to edit date-values. The implementation of it is 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 quite well, but has the disadvantage that the DynamicFormComponent must know all controls and respect them with an own branch. To lower this strong coupling, the following extensions use the possibility to load controls dynamically into the page.

Dynamically load controls

To enable loading components dynamically, the affected entry in the array elements gets a direct reference to the component-controller, which is just a class. An application could also dynamically fetch this control-controller from the server at run time, e. g. by using System.import.

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

The below presented component ControlWrapperComponent takes care of dynamic loading in this scenario. As its name suggests, it is a wrapper for the component which is to be loaded dynamically. It implements the lifecycle-hook OnInit and the ControlValueAccessor interface. The latter is necessary, to make it with work with Angular's forms handling. It takes the necessary metadata from the input-binding metadata.

In addition, it has properties for the dynamically loaded components (innerComponent), the ChangeDetector of this component (innerComponentChangeDetectorRef) and the current value (value). As described in this example, the constructor sets up the component as its own ValueAccessor.

The method writeValue is called by Angular to set the current value. If the dynamically loaded component already exists, it passes this value to it and then it triggers the ChangeDetector to update its view.

The methods registerOnChange and registerOnTouched take callbacks from Angular and stow them within the member-variables onChange and onTouched. With these callbacks the component informs Angular when the user changes the displayed value.

The lifecycle hook ngOnInit uses the injected DynamicComponentLoader to load the desired component. It's method loadAsRoot takes the reference to the component class, a CSS selector and an injector. The CSS selector determines where to place the component within the template. The injector determines what services the component can get via dependency injection. In the considered case the injector of the wrapper-component is used for this purpose.

After loading the component, the generated component-instance as well as it's ChangeDetector is put into variables. Then ngOnInit passes the current value (value) to the newly created component via writeValue. After that, it triggers it's ChangeDetector.

Then, the wrapper component registeres callbacks to keep track of changes. To do this, it passes lambda expressions to registerOnChanged and registerOnTouched. These lambda expressions delegate to Angular by calling onChange and onTouched.

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

            });
    }

}

Dynamic-Form-Component um dynamische Steuerelemente erweitern

The DynamicFormComponent can now use the wrapper-component:

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;

}

In addition, the template uses the wrapper-component for each field that refers to a component by the means of the property control. This contains the metadata including the control to dynamically load.

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

 

 
Hier können Sie eine Anfrage für eine unverbindliche Schulung ode Beratung bzw. einen Workshop erstellen.
 
Unverbindliche Anfrage
 
 

Schulungen

Angular Schulung: Strukturierte Einführung

Lernen Sie in dieser interaktiven Schulung anhand einer Beispielanwendung den Einsatz von Angular für Ihre erfolgreichen Projekte kennen. Sie durchdringen die Hintergründe und bauen von der ersten Minute an auf Best Practices auf.

Details

Advanced Angular: Enterprise-Anwendungen und Architektur

In dieser Schulung erfahren Sie alles für die Entwicklung großer Anwendungen mit Angular: Mono-Repos, Micro-Apps, State Management, Performance und mehr

Details

Angular: Strukturierte Einführung

Seit der Ankündigung von Angular (2+) fragen sich Entwicklungs-Teams, welche Migrationspfade für AngularJS-1.x-Anwendungen vorliegen werden. Das im Lieferumfang von Angular enthaltene ngUpgrade bietet eine Antwort darauf. Es erlaubt einen Parallelbetrieb von AngularJS 1.x und Angular (2+) und stellt somit die Grundlage für eine schrittweise Migration dar.

Details

Progressive Web-Apps mit Angular

Progressive Web Apps bieten den Komfort nativer Anwendungen, indem sie auf moderne Browser APIs, wie Service Worker, setzen. Sie sind installierbar sowie offlinefähig und nutzen Hintergrundprozesse für Datensynchronisation und Push-Notifications. Diese Schulung zeigt anhand eines durchgehenden Beispiels was sich genau hinter diesem neuen Konzept verbirgt, wie solche Anwendungen mit Angular entwickelt werden und wie Sie in Ihren Projekten von den dahinterstehenden Ideen profitieren.

Details

Reaktive Architekturen mit Angular und Redux

Dieses interaktive Seminar vermittelt, wie Sie reaktive Anwendungen mit Angular entwickeln können.

Details

TypeScript

TypeScript gibt Ihnen alle Möglichkeiten der neuen JavaScript-Standards und zusätzlich ein statisches Typsystem, das dabei hilft, Fehler möglichst früh zu erkennen. Außerdem ist TypeScript die Grundlage für Angular. In diesem interaktiven Seminar lernen Sie diese mächtige Sprache anhand einer Fallstudie kennen.

Details

Weitere Schulungen ...