Dynamic Form Builder using Angular 5 and Bootstrap 4 (Reactive Forms)
Published on March 11, 2018
A while back I wrote a post on how to build dynamic form using Angular 2. In that post, we have implemented it by using Angular Template driven model. I have been working on another dynamic form builder where I have implemented it in reactive style.
Let’s see how can we do it.
I will use the same example which I have used in that old post. Our goal here is to generate a form which will have bootstrap 4 CSS for forms.
[
{
'name': 'email',
'label': 'Email'
},
{
'name': 'first_name',
'label': 'First Name',
'multi' : true
},
{
'type': 'radio',
'name': 'radio',
'label': 'Radio',
'opts': [
{ label: 'Option 1', key:'opt_1'},
{ label: 'Option 2', key: 'opt_2'}
]
},
{ 'type': 'check',
'name': 'opt',
'opts': [
{'key':'opt_1', label: 'Option 1',value:false},
{'key':'opt_2', label: 'Option 2',value:false}
]
},
{
'type': 'select',
'name': 'select_1',
'label': 'Select',
'opts': [
{ label: 'Option 1', key:'opt_1'},
{ label: 'Option 2', key: 'opt_2'}
]
}
]
I skipping the part where we create new Angular
Application using angular-cli
. I hope you have already had your application created. If not just go this document.
First Let’s create our from control atoms TextBox
,DropDown
, Radio
,CheckBox
and FileUpload
** TextBox **
import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
// text,email,tel,textarea,password,
@Component({
selector: 'textbox',
template: `
<div [formGroup]="form">
<input *ngIf="!field.multiline" [attr.type]="field.type" class="form-control" [id]="field.name" [name]="field.name" [formControlName]="field.name">
<textarea *ngIf="field.multiline" [class.is-invalid]="isDirty && !isValid" [formControlName]="field.name" [id]="field.name"
rows="9" class="form-control" [placeholder]="field.placeholder"></textarea>
</div>
`
})
export class TextBoxComponent {
@Input() field:any = {};
@Input() form:FormGroup;
get isValid() { return this.form.controls[this.field.name].valid; }
get isDirty() { return this.form.controls[this.field.name].dirty; }
constructor() {
}
}
This is basic field control. It allows all html 5 input types. text
,email
,tel
,password
and etc.
We can use this same component for textarea
field by having multiline
property.
We can get if a field is valid or invalid by using the getter isValid
. By using isDirty
we can find if the field is touched at least once.
** Dropdown Select**
import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
selector: 'dropdown',
template: `
<div [formGroup]="form">
<select class="form-control" [id]="field.name" [formControlName]="field.name" [class.is-invalid]="isDirty && !isValid">
<option *ngFor="let opt of field.options" [value]="opt.key">{{opt.label}}</option>
</select>
</div>
`
})
export class DropDownComponent {
@Input() field:any = {};
@Input() form:FormGroup;
get isValid() { return this.form.controls[this.field.name].valid; }
get isDirty() { return this.form.controls[this.field.name].dirty; }
}
This same as textbox
field control only new thing here we have options
.
** Radio **
import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
selector: 'radio',
template: `
<div [formGroup]="form">
<div class="form-check" *ngFor="let opt of field.options">
<input class="form-check-input" type="radio" [value]="opt.key" >
<label class="form-check-label">
{{opt.label}}
</label>
</div>
</div>
`
})
export class RadioComponent {
@Input() field:any = {};
@Input() form:FormGroup;
}
This is also same as textbox
and dropdown
.
Checkbox
import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
selector: 'checkbox',
template: `
<div [formGroup]="form">
<div [formGroupName]="field.name" >
<div *ngFor="let opt of field.options" class="form-check form-check">
<label class="form-check-label">
<input [formControlName]="opt.key" class="form-check-input" type="checkbox" id="inlineCheckbox1" value="option1" />
{{opt.label}}</label>
</div>
</div>
</div>
`
})
export class CheckBoxComponent {
@Input() field:any = {};
@Input() form:FormGroup;
get isValid() { return this.form.controls[this.field.name].valid; }
get isDirty() { return this.form.controls[this.field.name].dirty; }
}
In Angular forms checkbox
are trickey. There are different way to solve it but here We choose to have sub FormGroup
. to create nested object with values true
or false
.
That means when we submit the form we will get the result something like
'hobbies': {
'games': true
'cooking': false
}
If we use FormArray
we could get
'hobbies': [true, false]
** File Upload**
import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
selector: 'file',
template: `
<div [formGroup]="form">
<div *ngIf="!field.value" class="drop-container""
(dropped)="field.onUpload($event)">
<p class="m-0">
Drag a file here or
<label class="upload-button">
<input type="file" multiple="" (change)="field.onUpload($event.target.files)"> browse
</label>
to upload.
</p>
</div>
<div *ngIf="field.value">
<!-- <button type="button" class="btn btn-primary">Change</button> -->
<div class="card">
<img class="card-img-top" [src]="field.value">
</div>
</div>
</div>
`,
styles:[
`
.drop-container {
background: #fff;
border-radius: 6px;
height: 150px;
width: 100%;
box-shadow: 1px 2px 20px hsla(0,0%,4%,.1);
display: flex;
align-items: center;
justify-content: center;
border: 2px dashed #c0c4c7;
}
p {
font-size: 16px;
font-weight: 400;
color: #c0c4c7;
}
.upload-button {
display: inline-block;
border: none;
outline: none;
cursor: pointer;
color: #5754a3;
}
.upload-button input {
display: none;
}
`
]
})
export class FileComponent {
@Input() field:any = {};
@Input() form:FormGroup;
get isValid() { return this.form.controls[this.field.name].valid; }
get isDirty() { return this.form.controls[this.field.name].dirty; }
}
Here instead of using default html file uploader, We have custom file uploader. This will allow field drag and drop also. For file control we have to supply onUpload
property to deal with uploaded files.
Ok, We are done with our atoms. Now we need a way to pick one of this atom based on field json
field type. To do that let’s use *ngSwitch
directive. Let’s call it as Field Builder
Component.
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'field-builder',
template: `
<div class="form-group row" [formGroup]="form">
<label class="col-md-3 form-control-label" [attr.for]="field.label">
{{field.label}}
<strong class="text-danger" *ngIf="field.required">*</strong>
</label>
<div class="col-md-9" [ngSwitch]="field.type">
<textbox *ngSwitchCase="'text'" [field]="field" [form]="form"></textbox>
<dropdown *ngSwitchCase="'dropdown'" [field]="field" [form]="form"></dropdown>
<file *ngSwitchCase="'file'" [field]="field" [form]="form"></file>
<checkbox *ngSwitchCase="'checkbox'" [field]="field" [form]="form"></checkbox>
<radio *ngSwitchCase="'radio'" [field]="field" [form]="form"></radio>
<date *ngSwitchCase="'date'" [field]="field" [form]="form"></date>
<div class="alert alert-danger my-1 p-2 fadeInDown animated" *ngIf="!isValid && isDirty">{{field.label}} is required</div>
</div>
</div>
`
})
export class FieldBuilderComponent {
@Input() field:any;
@Input() form:any;
get isValid() { return this.form.controls[this.field.name].valid; }
get isDirty() { return this.form.controls[this.field.name].dirty; }
}
This will pick field controls based on field type.
Dynamic Form Builder
component is the only component that we are going to export on this module. We will use this component in outer module to implement the dynamic form builder.
Dynamic Form Builder Component
import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
@Component({
selector: 'dynamic-form-builder',
template:`
<form (ngSubmit)="onSubmit.emit(this.form.value)" [formGroup]="form" class="form-horizontal">
<div *ngFor="let field of fields">
<field-builder [field]="field" [form]="form"></field-builder>
</div>
<div class="form-row"></div>
<div class="form-group row">
<div class="col-md-3"></div>
<div class="col-md-9">
<button type="submit" [disabled]="!form.valid" class="btn btn-primary">Save</button>
<strong >Saved all values</strong>
</div>
</div>
</form>
`,
})
export class DynamicFormBuilderComponent implements OnInit {
@Output() onSubmit = new EventEmitter();
@Input() fields: any[] = [];
form: FormGroup;
constructor() { }
ngOnInit() {
let fieldsCtrls = {};
for (let f of this.fields) {
if (f.type != 'checkbox') {
fieldsCtrls[f.name] = new FormControl(f.value || '', Validators.required)
} else {
let opts = {};
for (let opt of f.options) {
opts[opt.key] = new FormControl(opt.value);
}
fieldsCtrls[f.name] = new FormGroup(opts)
}
}
this.form = new FormGroup(fieldsCtrls);
}
}
This is very important that we have to implement the FormControl
and FormGroup
based on our field json structure.
Here, we loop through all the fields and create the FormGroup
model out of it.
** dymanic-form-builder.module **
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
// components
import { DynamicFormBuilderComponent } from './dynamic-form-builder.component';
import { FieldBuilderComponent } from './field-builder/field-builder.component';
import { TextBoxComponent } from './atoms/textbox';
import { DropDownComponent } from './atoms/dropdown';
import { FileComponent } from './atoms/file';
import { CheckBoxComponent } from './atoms/checkbox';
import { RadioComponent } from './atoms/radio';
import { DateComponent } from './atoms/date';
@NgModule({
imports: [
CommonModule,
ReactiveFormsModule,
NgbModule.forRoot()
],
declarations: [
DynamicFormBuilderComponent,
FieldBuilderComponent,
TextBoxComponent,
DropDownComponent,
CheckBoxComponent,
FileComponent,
RadioComponent,
DateComponent
],
exports: [DynamicFormBuilderComponent],
providers: []
})
export class DynamicFormBuilderModule { }
This is our DynamicFormsBuilderModule
which bundles all components. Here we export only DynamicFormBuilderComponent
.
To use this module import it in main app module.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
// dynamic form builder
import { DynamicFormBuilderModule } from './dynamic-form-builder/dynamic-form-builder.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [ BrowserModule, ReactiveFormsModule , DynamicFormBuilderModule],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
app.component
import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
@Component({
selector: 'my-app',
template: `
<dynamic-form-builder [fields]="fields"></dynamic-form-builder>
`,
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
public fields:any[] = [
{
type: 'text',
name: 'firstName',
label: 'First Name',
value: '',
required : true,
},
{
type: 'text',
name: 'lastName',
label: 'Last Name',
value: '',
required : true,
},
{
type: 'text',
name: 'email',
label: 'Email',
value: '',
required : true,
},
{
type: 'radio',
name: 'gender',
label: 'Gender',
value: '',
required : true,
options: [
{ key:'m', label: 'Male'},
]
},
{
type: 'dropdown',
name: 'county',
label: 'Country',
value: 'in',
required : true,
options: [
{ value:'in', label: 'India'},
{ value:'usa', label: 'USA'},
]
},
{
type: 'checkbox',
name: 'hobbies',
label: 'Hobbies',
value: '',
required : true,
options: [
{ key:'cooking', label: 'Cooking'},
{ key:'jumping', label: 'Jumpping'},
]
}
];
}
That’s is we are done. Change the Fields json based on your requirements.