Angular 4+, Empower Dynamic Form, Part 1: A Simple One

Tom Liu
11 min readJul 29, 2018

Here you are. Since you are here, I assume you know some about Angular, typescript, javascript, html etc. So, let’s start right from the Angular dynamic form.

According to the description from this page, dynamic form is superb, and can do all that static form is capable of. It probably could, but to make a content & format rich dynamic form could take much more time. So, I think dynamic forms are more suitable for forms that are medium complicate. When designed properly, dynamic form can greatly simplify the form generation and form content/layout modification. I can assure you that you will enjoy its power when the dynamic form is properly built to where it fits.

Now, let’s start from the very beginning. Once upon a time, there were the Garden of Eden. We want to build a page to register the residents of Garden of Eden, so we have a Angular project to create…

Create a project:

$ ng new playground
$ cd playground

After the project is created, you will find the file structure is generated under playground directory.

  • node_modules has all the node modues (eh… seems quite obvious)
  • src has all your source codes (not yet, since this is an brand new baby)
  • package.json has the package dependency configuration. It has a follower package-lock.json, which will change together with package.json when you have new package installed through npm command. These two files update automatically if you install new packages or update existing packages to a new version. You could modify them manually, but I prefer to have them managed by npm automatically.
  • tsconfig.json & tslint.json you can play with these two files for typescript configurations.

Install Angular flex layout & Angular material:

$ npm i --save @angular/flex-layout
$ npm i --save @angular/material
$ npm i --save @angular/cdk
$ npm i --save hammerjs

Then, import the hammerjs in app.component.ts, which is located under src/app:

import 'hammerjs/hammer';

To use the build-in theme of Angular material, add the following line to style.css, which is located under src/app/ directory:

@import '~@angular/material/prebuilt-themes/indigo-pink.css';

About Angular flex layout & Angular material, please google it for more details.

Import material design and flex layout modules in app.modules.ts:

Don’t forget to add ReactiveFormsModule, BrowserAnimationsModule

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { FormElementComponent } from './form-element/form-element.component';
import { FormComponent } from './form/form.component';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {
MatAutocompleteModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatCheckboxModule,
MatChipsModule,
MatDatepickerModule,
MatDialogModule,
MatExpansionModule,
MatFormFieldModule,
MatGridListModule,
MatIconModule,
MatInputModule,
MatListModule,
MatMenuModule,
MatNativeDateModule,
MatPaginatorModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatRippleModule,
MatSelectModule,
MatSidenavModule,
MatSliderModule,
MatSlideToggleModule,
MatSnackBarModule,
MatSortModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
MatStepperModule,
} from '@angular/material';
@NgModule({
declarations: [
AppComponent,
FormElementComponent,
FormComponent
],
imports: [
BrowserModule,
FlexLayoutModule,
FormsModule,
ReactiveFormsModule,
BrowserAnimationsModule,
MatAutocompleteModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatCheckboxModule,
MatChipsModule,
MatDatepickerModule,
MatDialogModule,
MatExpansionModule,
MatFormFieldModule,
MatGridListModule,
MatIconModule,
MatInputModule,
MatListModule,
MatMenuModule,
MatNativeDateModule,
MatPaginatorModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatRippleModule,
MatSelectModule,
MatSidenavModule,
MatSliderModule,
MatSlideToggleModule,
MatSnackBarModule,
MatSortModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
MatStepperModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

MVC

I guess you had heard the rumors about the legendary MVC, and how glorious it is. In fact, since MVC was born last century, it has been the golden rule for GUI design. There are numerous articles about MVC.

Model

Of course we will use MVC pattern too. To start with, we need model as the data carrier. First, we create a src/app/model directory, and will put all the model classes into this directory.

Enum type.ts is used to define the types:

export enum Type {
MyLady = 'My Lady', MyMan = 'My Man'
}

It is obvious, isn’t it? In fact, intuition and clarity in coding is indeed important.

Interface MyModelParam.ts is defined to define the model paramters:

export interface MyModelParam {
name: string;
type?: string;
}

Model class MyModel.ts is created to represent the model:

import { MyModelParam } from './myModelParam';
import { Type } from './type';
export class MyModel {
name: string;
type: string;

constructor(param : MyModelParam) {
this.name = param.name || '';
this.type = param.type || Type.MyLady.toString();
}
}

Most of the fields in the interface and corresponding class are optional (marked by the question mark) so that we don’t have to specify all the parameters when using the interface.

Using interface in the class constructor together with the optional fields makes class creation easy. This way, we can refer to the parameter names in the constructor without worrying about wrong parameter order, and we don’t need to provide all the parameters in the constructor, e.g. we could do:

let model: MyModel = new MyModel({name: 'Adam', type: Type.MyMan});

How about let’s call it Easy-Construction-by-Interface-and-Optional-Fields pattern ;-).

That is all for our model. In our MVC, the model serves as the data carrier among the GUI elements, GUI controllers, and backend REST API.

Form configuration elements

When we configure a dynamic form, we need the elements such as text filed (or input), drop-down list (or select), radio button, nested group of elements (more details coming) etc., you name it. To start with, I will add a class for each of the elements. These element classes will be used to represent the form elements. It contains the information that is needed when we want to create the forms dynamically. Now, let’s add the element classes.

Wait a second, where do we want to put these classes? How about inside form-element component? But what & where is the form-element component? hmm…, it is a good question. We need to generate form-element components. To create component, under playground directory, run following CLI command:

$ ng g component form-element

This will generate component form-element under src/app/form-element/. It has following files:

  • form-element.component.css: Cascading Style Sheets file for the HTML page
  • form-element.component.html: the HTML page
  • form-element.component.spec.ts: I don’t know, and I don’t care.
  • form-element.component.ts: The typescript class that backs the HTML page

Guess what, all the element classes that we are going to add in this section will be under src/app/form-element/ directory too.

The generated components is just a skeleton for now. We will add flesh and blood to the component soon enough.

Now, where were we? We were trying to add some form elements. Let’s use Easy-Construction-by-Interface-and-Optional-Fields pattern for the element classes. Isn’t it fun?

To apply Easy-Construction-by-Interface-and-Optional-Fields pattern, we use element parameter interface elementParam.ts to represent the element parameters:

export interface ElementParam {
name: string;
displayName?: string;
elementType?: string;
value?: any;
required?: boolean;
order?: number;
layout?: any;
options?: string[];
}

For the sake of reusing and interfacing, a super class formElement is created as the parent of all the specific elements. Parent element class formElement.ts uses the ElementParam in its constructor, which also initializes the class fields to default.

import { ElementParam } from './elementParam';
import { FormControl, AbstractControl } from '@angular/forms';
export class FormElement {
value: any;
name: string;
displayName: string;
required: boolean;
order: number;
elementType: string;
layout: any;
constructor(element: ElementParam) {
this.value = element.value;
this.name = element.name || '';
this.displayName = element.displayName || '';
this.required = element.required;
this.order = element.order || 1;
this.elementType = element.elementType || '';
this.layout = element.layout || {float: 'left', width: '100%' };
}
toFormControl(): AbstractControl {
return new FormControl(this.value);
}
}

A text filed element class textField.ts extends the FormElement:

import { FormElement } from './formElement';export class TextField extends FormElement {  elementType = 'textField';  constructor(element: ElementParam) {
super(element);
}
}

A drop-down list element class dropDown.ts also extends the FormElement:

import { ElementParam } from './elementParam';
import { FormElement } from './formElement';
export class DropDown extends FormElement {
elementType = 'dropDown';
options: string[];
constructor(element: ElementParam) {
super(element);
this.options = element.options || [];
}
}

We could add more element if you like, but I am not sure if you like it or not, so I will just use these 2 elements in this example.

Form view & control element

As aforementioned, the generated form-element component skeleton is still waiting for its flesh and blood. form-element component is the basic form view & control element. To refresh our memory a little bit, the skeleton has:

  • A HTML page form-element.component.html that carries out the HTML elements, and
  • The component class form-element.component.ts to back the HTML page.

The HTML page form-element.component.html contains the HTML tags, and serves as view template. The backing class form-element.component.ts has the variables and functions to support the HTML page, and servers as function implementation of the control unit. FormGroup is the control unit of Angular GUI. It interacts with the model & HTML tags & the backing class. When we add all these up, we get the famous MVC framework.

After adding the following HTML codes to form-element.component.html, we make sure that the form HTML element, which is configured by the formElement element, is displayed properly.

<div [formGroup]="group">  <div [ngSwitch]="element.elementType">    <mat-form-field *ngSwitchCase="'textField'" appearance="outline"        [ngStyle]="{'float': element.layout.float, 'font-size.px': element.layout.fontSize}">      <mat-label>{{element.displayName}}</mat-label>      <input id="{{element.name}}" matInput    
[formControlName]="element.name"/>
</mat-form-field> <mat-form-field *ngSwitchCase="'dropDown'" appearance="outline" [ngStyle]="{'float': element.layout.float, 'font-size.px': element.layout.fontSize}"> <mat-label>{{element.displayName}}</mat-label> <mat-select id="{{element.name}}" [formControlName]="element.name"> <mat-option *ngFor="let opt of element['options']" [value]="opt">{{ opt }}</mat-option> </mat-select> </mat-form-field> </div></div>

How beautiful the format of the HTML page is presented. Think it as an fine art of abstract painting, which hardly makes any sense at the first glimpse (In fact, abstract painting never makes any sense to me).

The following component class form-element.component.ts backs the above HTML page:

import { FormElement } from './formElement';
import { Component, OnInit, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
selector: 'app-form-element',
templateUrl: './form-element.component.html',
styleUrls: ['./form-element.component.css']
})
export class FormElementComponent implements OnInit {
@Input() element: FormElement;
@Input() group: FormGroup;
constructor() {}
ngOnInit() {}
}

Allow me to do some explanation for the HTML page and the backing class.

  • This component’s selector is app-form-element as indicated in form-element.component.ts, meaning, in the HTML page that uses this component, <app-form-element></app-form-element> summons the present of this component.
  • The HTML page form-element.component.html is backed by field variable element and group of the class form-element.component.ts.
  • These two variables element and group are marked as @input , which allows these two fields being used as component input in the HTML page that uses this component, such as: <app-form-element [element]=”elemt” [group]=”form”></app-form-element>
  • All the elements in the same form are bound to the same formGroup. The component HTML page uses [formGroup]="group" to tell us that the element belongs to a formGroup, whose name is group. The group is an variable of type FormGroup, which is defined in component class form-element.component.ts.
  • The elementhere is a FormElement, if you scroll up to the Form configuration elements section, you will find that we have TextFieldand DropDown elements defined.
  • The HTML element is selected by [ngSwitch]="element.elementType" . *ngSwitchCase="'textField'" tell us, when the elementType is 'textField' , the mat-form-field with input label & element is displayed, *ngSwitchCase="'dropDown'" tell us, when the elementType is dropDown , the mat-form-field with mat-select label & element is displayed.
  • [ngStyle] is used to control the display style of the elements.

That is pretty much all about the form-element component.

Form component

We have form-element in hands. Now let’s use it to build a dynamic form. We need a form component. To generate a form component, run following CLI under playground directory:

ng g component form

This generate a form component under src/app/form/.

We use form.component.html to build the form HTML view:

<form (ngSubmit)="submit()" [formGroup]="form" >  <div style="width: 40%; margin: auto;">    <div *ngFor="let elemt of elements" 
[ngStyle]="{'float': elemt.layout.float, 'width': elemt.layout.width}">
<app-form-element [element]="elemt" [group]="form"></app-form-element> </div> <button mat-button type="submit" class="btn-success">Submit</button> </div></form>

In form.component.ts, we have the variables that is used by the HTML page. We create the form control in createForm method by using formFactory.

import { FormElement } from '../form-element/formElement';
import { MyModel } from '../model/myModel';
import { FormFactory } from './formFactory';
import { Component, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
selector: 'app-form',
templateUrl: './form.component.html',
styleUrls: ['./form.component.css']
})
export class FormComponent implements OnInit {

elements: FormElement[];
form: FormGroup;
formFactory: FormFactory;
model: MyModel;
constructor() {}ngOnInit() {}public createForm(model: MyModel) {
this.formFactory = new FormFactory();
this.form = this.formFactory.createForm(model);
this.elements = this.formFactory.elements;
}

public submit() {
this.model = this.form.value;
console.log('Model: ' + JSON.stringify(this.model));
//do your submitting job to backend API
}
}

Here are some details:

  • In HTML file, we use the app-form-element component to represent one of the form elements. For different element types, it has corresponding element presentation.
  • app-form-elementhas 2 input parameters [element]="elemt"and [group]="form". Where, when the form has multiple elements, elemt is one of the elements. group is used to assign a FormGroup form for the form element.
  • When the form has multiple elements, *ngFor="let elemt of elements" is used to iterate the elements .
  • elements is the class field of form.component.ts . The elements field is an array of FormElement. elements is populated after the createForm method is called.
  • form is the FormGroup that is associated with the HTML form, which is populated when the createForm method is called.
  • [ngStyle] is used to control the display style of the elements.
  • The selector of form component is app-form.

Following is the formFactory.ts class. It serves as the dynamic form configurer. In createForm method, we use the form elements TextField & DropDown to create the form control, i.e. FormGroup. When the FormGroup is created, the byproduct is the element array, which is used in form.component.ts class as class field elements to support the display of HTML elements.

import { DropDown } from '../form-element/dropDown';
import { FormElement } from '../form-element/formElement';
import { TextField } from '../form-element/textField';
import { MyModel } from '../model/myModel';
import { Type } from '../model/type';
import { FormGroup, AbstractControl, FormControl } from '@angular/forms';
export class FormFactory { elements: FormElement[]; private toFormGroup(elements: FormElement[]): AbstractControl {
let group: any = {};
elements.forEach((element: FormElement) => {
group[element.name] = element.toFormControl();
});

return new FormGroup(group);
}
public createForm(model: MyModel): FormGroup {
console.debug('creating form...');
this.elements = [
new TextField({
name: 'name',
displayName: 'Name',
value: model.name,
layout: {float: 'left', width: '80%', fontSize: '16' }
}),
new DropDown({
name: 'type',
displayName: 'Type',
value: model.type,
options: [Type.MyLady.toString(), Type.MyMan.toString()],
layout: {float: 'left', width: '80%', fontSize: '16' }
}),
];
let ret: FormGroup = this.toFormGroup(this.elements) as FormGroup;
return ret;
}
}
  • value field in the form element is used to assign the initial value of the element from model.
  • layout configures the element look-and-feel, which is used by [ngStyle] in the HTML template form-element.component.html.
  • toFormGroup method converts the form element configuration to the FormGroup.

Now, we have the form assembled. We need a final touch to have it displayed. Let’s use app.component.html & app.component.ts to do the final touch.

Modify the generated code in app.component.html:

<div style="text-align:center">  <h3>    Welcome to {{ title }}!  </h3></div><app-form></app-form>

where, app-form is the form component above.

Modify app.component.ts class, where form: FormComponentis the form component above.

import { FormComponent } from './form/form.component';
import { MyModel } from './model/myModel';
import { Type } from './model/type';
import { Component, ViewChild, OnInit } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'The Garden of Eden Registration';
@ViewChild(FormComponent) form: FormComponent;
ngOnInit() {
//To edit existing form, you could retrieve the
//model from backend REST API, and use it to create form here.
this.form.createForm(new MyModel({name: 'Eve', type: Type.MyLady}));
}
}

Here is how it looks like:

We spend so much time just to come out with such a simple form? I dare you to modify the formFactory.ts, see how simple and easy it becomes to add GUI components, to change the layout. I challenge you to add more CSS attribute in the layout of the form elements inside formFactory.ts, and use them in [ngStyle] of form-element.component.html. I believe after you played with the form creation & modification, you would appreciate what we have done. It just makes everything works like a charm!

But, that is too simple, isn’t it? We made it work first. Of course, we will make it better. Check out Part 2: Make It Better!

--

--