Techumber
Home Blog Work

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.

Demo

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.