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

  • 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.
import { MyModelParam } from './myModelParam';export class Address {
street: string;
location: string;
constructor(param : MyModelParam) {
this.street = param.street || 'guess...';
this.location = param.location || 'Heaven';
}
}
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!';
}
}
import { Type } from './type';export interface MyModelParam {
name?: string; //<-optional now
type?: string;
sexy?: string;
street?: string;
location?: string

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

}
}
ng g component 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;
}
}
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[];
}
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;
}
}
{
"form":{
"name":"Eve",
"sex":{
"type":"My Lady",
"sexy":"Yes!"
},
"address":{
"street":"guess...",
"location":"Heaven"
}
}
}
{
"form":{
"name":"Adam",
"sex":{
"type":"My Man",
"sexy":"Oh, Yeah!"
},
"address":{
"street":"801 Who-knows",
"location":"Under the tree"
}
}
}
  • group-element.component.html the HTML template for group elements
  • group-element.component.ts the class that supports the template
<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.)
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() { }
}
<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).

--

--

--

Life is beautiful…

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Coding Interview for web developers

Phaser 3 Tricks: Make Circle Image.

Form Validation Made Easy.

AIRDROP : JOIN NO

Why Did We Choose Web Components?

DRY node.js server code (without Express).

Visualize Drone Photos Location and Orientation in deck.gl

HTML, Web Performance, DOM.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Tom Liu

Tom Liu

Life is beautiful…

More from Medium

Read configuration properties from JSON file and load it before Angular application is initialized

Using Angular services in Storybook

How to add Mat-Stepper in Angular