Angular 4+, Empower Dynamic Form, Part 2: Make It Better

We had a simple yet good start on dynamic form in the part one of Empower Dynamic Form. We made it work. Now, we need to make it better (much better). I reckon CSS is either too simple or too boring for us. Imagine that you spend hours playing on CSS, and when the others looking at the page with your favorite design, they might say it is pretty beautiful or ugly depending on their mood. Believe me, it is fun to have a GUI review meeting…

So, I decided not to add the CSS to give you the chance to comment on it. The agenda for us is to make the form structurally enriched. In another word, make the form more powerful to handle more complicated GUI models.

Quite often, we have a group of HTML elements that we want to demarcate. We want to display/handle them in a way that it looks like the elements are from the same clan.

Let’s re-organize the model to include two groups:

  • Previous, we have Type “My Lady” and “My Man”. Now, let’s add a group Sex to indicate how attractive the residents are. Group Sex has two DropDown elements: Type “My Lady” and “My Man”, and Sexy “Yes!” and “Oh, Yeah!”
  • A new group Address is added to record where the residents live. The group has one TextField element Street and one DropDown element Location.

To tailer our model to include the new groups, we add two new classes under src/app/model.

address.ts to represent the Address group:

import { MyModelParam } from './myModelParam';export class Address {
street: string;
location: string;
constructor(param : MyModelParam) {
this.street = param.street || 'guess...';
this.location = param.location || 'Heaven';
}
}

sex.ts to represent the Sex group:

import { MyModelParam } from './myModelParam';
import { Type } from './type';
export class Sex {
type: string;
sexy: string;
constructor(param : MyModelParam) {
this.type = param.type || Type.MyLady.toString();
this.sexy = param.sexy || 'Yes!';
}
}

Modify myModelParam.ts to include the new parameters sexy, street, and location for new groups (The bold sections are the parts changed from part 1 codes):

import { Type } from './type';export interface MyModelParam {
name?: string; //<-optional now
type?: string;
sexy?: string;
street?: string;
location?: string

}

Modify myModel.ts to include the new groups, Sex and Address:

import { Address } from './address';
import { MyModelParam } from './myModelParam';
import { Sex } from './sex';
import { Type } from './type';
export class MyModel {
name: string;
sex: Sex;
address: Address;
constructor(param : MyModelParam) {
this.name = param.name || '';
this.sex = new Sex({type: param.type, sexy: param.sexy});
this.address = new Address({street: param.street, location: param.location});

}
}

As you might already know, in Angular, we use FormGroup to group the set of elements as the GUI control. In this section, we are going to look at how to configure the group elements, and how to convert our group element configuration to the FormGroup.

In part one, we used the FormElement to represent/configure the elements on the HTML page. For example, a TextField represents a input field, DropDown represents a select field. Similarly, to configure a group of elements, we introduce a new sub-class of FormElement: Group, which will reside under src/app/group-element/. Wait a minute, why/how do we get the src/app/group-element/ directory?

In order to organize/display the group elements gracefully, we will need a new component, group-element, so that the group-element can reuse the previous form-element component and group-element component itself in a recursive manner.

To generate group-element:

ng g component group-element

This will generate the group-element component under src/app/group-element/.

Here we are. That is why/how we get the src/app/group-element/ directory. Our group.ts lives happily inside src/app/group-element/:

import { ElementParam } from '../form-element/elementParam';
import { FormElement } from '../form-element/formElement';
import { AbstractControl, FormGroup } from '@angular/forms';
export class Group extends FormElement {
elementType = 'group';
elements: FormElement[];
constructor(element: ElementParam) {
super(element);
this.elements = element.elements;
}
toFormControl(): AbstractControl {
return new FormGroup(this.toFormGroup(this.elements));
}
private toFormGroup(elements) {
let group: any = {};
elements.forEach((element: FormElement) => {
if (element.elementType === 'group') {
let subGrp: any = this.toFormGroup((element as Group).elements);
group[element.name] = new FormGroup(subGrp);
} else {
group[element.name] = element.toFormControl();
}
});

return group;
}
}

If you are familiar with design patterns, here is an example of Composite Pattern, where Group serves as the composite of its leaves and itself. If there are nested groups in elements, toFormGroup method recursively converts the group configuration to a map of FormGroup, and then toFormControl method converts everything in the group to the final FormGroup.

Note that we also added the elements field to ElementParam class to allow elements being configured:

import { Type } from '../model/type';
import { FormElement } from './formElement';
export interface ElementParam {
name: string;
displayName?: string;
elementType?: string;
value?: any;
required?: boolean;
order?: number;
//'elements' field represents a group of elements
elements?: FormElement[];

layout?: any;
options?: string[];
}

Now, with the Group class as a new form element for configuration, we can easily convert a form configuration with group elements to FormGroup. There is no change on toFormGroup method of FormFactory class (how nice it is!). We simply add the groups/elements configuration to elements of FormFactory as we desire. Here is a the FormFactory class with a new form configuration:

import { DropDown } from '../form-element/dropDown';
import { FormElement } from '../form-element/formElement';
import { TextField } from '../form-element/textField';
import { Group } from '../group-element/group';
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 Group({
name: 'form',
layout: {width: '80%', fontSize: '12', fx: 'column' },
elements: [
new TextField({
name: 'name',
displayName: 'Name',
value: model.name,
layout: {float: 'left', width: '80%', fontSize: '16' }
}),
new Group({
name: 'sex',
displayName: 'Sex',
layout: {float: 'right', width: '80%', labelFontSize: '14', fx: 'column', fxAlign: 'start', lableFontWeight: 'bold' },
elements: [
new DropDown({
name: 'type',
displayName: 'Type',
value: model.sex.type,
options: [Type.MyLady.toString(), Type.MyMan.toString()],
layout: {float: 'left', width: '80%',fontSize: '16'}
}),
new DropDown({
name: 'sexy',
displayName: 'Sexy',
value: model.sex.sexy,
options: ['Yes!', 'Oh, Yeah!'],
layout: {float: 'left', width: '80%',fontSize: '16'}
}),
],
}),
new Group({
name: 'address',
displayName: 'Address',
layout: {float: 'right', width: '80%', labelFontSize: '14', fx: 'column', fxAlign: 'start', lableFontWeight: 'bold' },
elements: [
new TextField({
name: 'street',
displayName: 'Street',
value: model.address.street,
layout: {float: 'left', width: '80%', fontSize: '16' }
}),
new DropDown({
name: 'location',
displayName: 'Location',
value: model.address.location,
options: ['Heaven', 'Under the tree', 'On the green grass', 'Among the beautiful flowers', 'Down to earth'],
layout: {float: 'left', width: '80%', fontSize: '16' }
}),
],
})
]
})
];
let ret: FormGroup = this.toFormGroup(this.elements) as FormGroup;
return ret;
}
}

As you see, the only part that is changed in FormFactory class is how the elements is constructed. We added two groups in it. You could play with the elements configuration, and the output form will be changed magically.

Here is how the page looks like:

When the form is submitted, it generates the model as:

{
"form":{
"name":"Eve",
"sex":{
"type":"My Lady",
"sexy":"Yes!"
},
"address":{
"street":"guess...",
"location":"Heaven"
}
}
}

Let’s say Adam like apples, and he plants apple trees (That is right. Adam’s apple…). He lives under the tree. When registering Adam, we have:

When the form is submitted, it generates the model as:

{
"form":{
"name":"Adam",
"sex":{
"type":"My Man",
"sexy":"Oh, Yeah!"
},
"address":{
"street":"801 Who-knows",
"location":"Under the tree"
}
}
}

Amazing, right? Hold on, it is not over yet. If we change the elements configuration in FormFactory class, for Group sex and address, we change layout from fx: 'column' to fx: 'row'. We have:

Fantastic, right? Hold on, it is not done yet. We just have the model and configuration covered so far. How about the group-element component? How about the form component? That is right, we are totally not done yet… Following section will bring you the magics.

The group-element component is used to render the groups, which in turn renders the elements inside the group.

group-element component’s magics comes from two files:

  • group-element.component.html the HTML template for group elements
  • group-element.component.ts the class that supports the template

In group-element.component.html, we have:

<div [formGroup]="group" style="border:0px;">  <mat-label fxLayoutAlign="{{element.layout.fxAlign}}" 
[ngStyle]="{'float': element.layout.float,
'font-size.px': element.layout.labelFontSize,
'font-weight': element.layout.lableFontWeight}">
{{element.displayName}}
</mat-label>
<!-- That is right. This is the plain dumb br here! I believe you could do better...-->
<br>
<mat-card class="card" fxLayout="{{element.layout.fx}}" fxLayoutGap="{{element.layout.fxGap}}"> <div *ngFor="let elemt of element.elements"> <div [ngSwitch]="elemt.elementType" class='panel-body' style="border:0px;"> <!-- This app-form-element allows the form elements in this group displayed -->
<app-form-element *ngSwitchDefault [group]="group" [element]="elemt" ></app-form-element>
<!-- This app-group-element allows the nested groups displayed -->
<app-group-element *ngSwitchCase="'group'" [group]="group.get(elemt.name)" [element]="elemt"></app-group-element>
</div>
</div>
</mat-card>
</div><br>
  • *ngFor iterates the elements in the group element element.
  • app-form-element renders the elements inside the group.
  • app-group-element renders the nested groups if there is any.
  • Note [formGroup]="group" , wheregroup represents the FormGroup of the group element being rendered.
  • Similarly, note [group]="group.get(elemt.name)", where [group] is the input of nested app-group-element for FormGroup. The group.get(elemt.name) retrieves the nested FormGroup for the nested group. (Remember in Group class, when we converted the configuration to FormGroup, we used the element name of Group as the nested group name.)

In group-element.component.ts, we have:

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

@Input() element: FormElement;
@Input() group: FormGroup;
constructor() { } ngOnInit() { }
}

We also need a little bit change in the template page that assembles the elements for the form, which is in form.component.html:

<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}">
<div [ngSwitch]="elemt.elementType" class='panel-body' style="border:0px;"> <app-form-element *ngSwitchDefault [group]="form" [element]="elemt" ></app-form-element> <app-group-element *ngSwitchCase="'group'" [group]="form.get(elemt.name)" [element]="elemt"> </app-group-element>

</div>

</div>
<button mat-button type="submit">Submit</button> </div>
</form>
  • The bold section is the part changed from part 1 code. According to the element type elemt.elementType, this template renders the app-form-element or app-group-element accordingly.
  • The [group] input of app-group-element is used to denote the FormGroup that is associated with the Group element. The FormGroup for this app-group-element is retrieved by form.get(elemt.name).

I think that is all for the magics of the group elements. I hope you enjoy it.

Hold on, we are not done yet. Of course, we have Part 3 coming, stay tuned!

Life is beautiful…