w3resource

Dynamic Forms

When building forms, it is not usually a problem to build handcrafted forms when building one or a few forms. But when the number of forms needed in a project increases, this approach quickly becomes costly and time-consuming, especially when they change frequently to meet rapidly changing business and regulatory requirements. It most cases it is economical to create forms dynamically, based on the metadata that describes a business object model.

Bootstrap

Start by creating an NgModule called AppModule.

We will use the reactive form to implement dynamic forms.

Reactive forms belong to a different NgModule called ReactiveFormsModule, so in order to access any reactive forms directives, you have to import ReactiveFormsModule from the @angular/forms library.

Bootstrap the AppModule in main.ts.

App.module.ts

TypeScript Code:

import { BrowserModule }                from '@angular/platform-browser';
import { ReactiveFormsModule }          from '@angular/forms';
import { NgModule }                     from '@angular/core';

import { AppComponent }                 from './app.component';
import { DynamicFormComponent }         from './dynamic-form.component';
import { DynamicFormQuestionComponent } from './dynamic-form-question.component';

@NgModule({
  imports: [ BrowserModule, ReactiveFormsModule ],
  declarations: [ AppComponent, DynamicFormComponent, DynamicFormQuestionComponent ],
  bootstrap: [ AppComponent ]
})
export class AppModule {
  constructor() {
  }
}

Live Demo:

It is just a code snippet explaining a particular concept and may not have any output

See the Pen app.module.ts by w3resource (@w3resource) on CodePen.


Main.ts

TypeScript Code:

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule);

Live Demo:

It is just a code snippet explaining a particular concept and may not have any output

See the Pen main.ts by w3resource (@w3resource) on CodePen.


Question model

The next step is to define an object model that can describe all scenarios needed by the form functionality. The hero application process involves a form with a lot of questions. The question is the most fundamental object in the model.

The following QuestionBase is a fundamental question class.

Question-base.ts

TypeScript Code:

export class QuestionBase<T> {
  value: T;
  key: string;
  label: string;
  required: boolean;
  order: number;
  controlType: string;

  constructor(options: {
      value?: T,
      key?: string,
      label?: string,
      required?: boolean,
      order?: number,
      controlType?: string
    } = {}) {
    this.value = options.value;
    this.key = options.key || '';
    this.label = options.label || '';
    this.required = !!options.required;
    this.order = options.order === undefined ? 1 : options.order;
    this.controlType = options.controlType || '';
  }
}

Live Demo:

It is just a code snippet explaining a particular concept and may not have any output

See the Pen question-base.ts by w3resource (@w3resource) on CodePen.


From this base, you can derive two new classes in TextboxQuestion and DropdownQuestion that represent textbox and dropdown questions. The idea is that the form will be bound to specific question types and render the appropriate controls dynamically.

TextboxQuestion supports multiple HTML5 types such as text, email, and URL via the type property.

Question-textbox.ts

TypeScript Code:

import { QuestionBase } from './question-base';

export class TextboxQuestion extends QuestionBase<string> {
  controlType = 'textbox';
  type: string;

  constructor(options: {} = {}) {
    super(options);
    this.type = options['type'] || '';
  }
}

Live Demo:

It is just a code snippet explaining a particular concept and may not have any output

See the Pen question-textbox.ts by w3resource (@w3resource) on CodePen.


DropdownQuestion presents a list of choices in a select box.

Question-dropdown.ts

TypeScript Code:

import { QuestionBase } from './question-base';

export class DropdownQuestion extends QuestionBase<string> {
  controlType = 'dropdown';
  options: {key: string, value: string}[] = [];

  constructor(options: {} = {}) {
    super(options);
    this.options = options['options'] || [];
  }
}

Live Demo:

It is just a code snippet explaining a particular concept and may not have any output

See the Pen question-dropdown.ts by w3resource (@w3resource) on CodePen.


Next is QuestionControlService, a simple service for transforming the questions into a FormGroup. In a nutshell, the form group consumes the metadata from the question model and allows you to specify default values and validation rules.

Question-control.service.ts

TypeScript Code:

import { Injectable }   from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';

import { QuestionBase } from './question-base';

@Injectable()
export class QuestionControlService {
  constructor() { }

  toFormGroup(questions: QuestionBase<any>[] ) {
    let group: any = {};

    questions.forEach(question => {
      group[question.key] = question.required ? new FormControl(question.value || '', Validators.required)
                                              : new FormControl(question.value || '');
    });
    return new FormGroup(group);
  }
}

Live Demo:

It is just a code snippet explaining a particular concept and may not have any output

See the Pen question-control.service.ts by w3resource (@w3resource) on CodePen.


Question form components

Now that you have defined the complete model you are ready to create components to represent the dynamic form.

DynamicFormComponent is the entry point and the main container for the form.

Dynamic-form-component.html

HTML Code:

<div>
  <form (ngSubmit)="onSubmit()" [formGroup]="form">

    <div *ngFor="let question of questions" class="form-row">
      <app-question [question]="question" [form]="form"></app-question>
    </div>

    <div class="form-row">
      <button type="submit" [disabled]="!form.valid">Save</button>
    </div>
  </form>

  <div *ngIf="payLoad" class="form-row">
    <strong>Saved the following values</strong><br>{{payLoad}}
  </div>
</div>

TypeScript Code:

import { Component, Input, OnInit }  from '@angular/core';
import { FormGroup }                 from '@angular/forms';

import { QuestionBase }              from './question-base';
import { QuestionControlService }    from './question-control.service';

@Component({
  selector: 'app-dynamic-form',
  templateUrl: './dynamic-form.component.html',
  providers: [ QuestionControlService ]
})
export class DynamicFormComponent implements OnInit {

  @Input() questions: QuestionBase<any>[] = [];
  form: FormGroup;
  payLoad = '';

  constructor(private qcs: QuestionControlService) {  }

  ngOnInit() {
    this.form = this.qcs.toFormGroup(this.questions);
  }

  onSubmit() {
    this.payLoad = JSON.stringify(this.form.value);
  }
}

Live Demo:

It is just a code snippet explaining a particular concept and may not have any output

See the Pen form-component.html by w3resource (@w3resource) on CodePen.


It presents a list of questions, each bound to a <app-question> component element. The <app-question> tag matches the DynamicFormQuestionComponent, the component responsible for rendering the details of each individual question based on values in the data-bound question object.

HTML Code:

<div [formGroup]="form">
  <label [attr.for]="question.key">{{question.label}}</label>

  <div [ngSwitch]="question.controlType">

    <input *ngSwitchCase="'textbox'" [formControlName]="question.key"
            [id]="question.key" [type]="question.type">

    <select [id]="question.key" *ngSwitchCase="'dropdown'" [formControlName]="question.key">
      <option *ngFor="let opt of question.options" [value]="opt.key">{{opt.value}}</option>
    </select>

  </div> 

  <div class="errorMessage" *ngIf="!isValid">{{question.label}} is required</div>
</div>

TypeScript Code:

import { Component, Input } from '@angular/core';
import { FormGroup }        from '@angular/forms';

import { QuestionBase }     from './question-base';

@Component({
  selector: 'app-question',
  templateUrl: './dynamic-form-question.component.html'
})
export class DynamicFormQuestionComponent {
  @Input() question: QuestionBase<any>;
  @Input() form: FormGroup;
  get isValid() { return this.form.controls[this.question.key].valid; }
}

Live Demo:

It is just a code snippet explaining a particular concept and may not have any output

See the Pen form-component.ts by w3resource (@w3resource) on CodePen.


Notice this component can present any type of question in your model. You only have two types of questions at this point but you can imagine many more. The ngSwitch determines which type of question to display.

In both components, you're relying on Angular's formGroup to connect the template HTML to the underlying control objects, populated from the question model with display and validation rules.

formControlName and formGroup are directives defined in ReactiveFormsModule. The templates can access these directives directly since you imported ReactiveFormsModule from AppModule.

Questionnaire data

DynamicFormComponent expects the list of questions in the form of an array bound to @Input() questions.

The set of questions you've defined for the job application is returned from the QuestionService. In a real app, you'd retrieve these questions from storage.

The key point is that you control the hero job application questions entirely through the objects returned from QuestionService. Questionnaire maintenance is a simple matter of adding, updating, and removing objects from the questions array.

TypeScript Code:

import { Injectable }       from '@angular/core';

import { DropdownQuestion } from './question-dropdown';
import { QuestionBase }     from './question-base';
import { TextboxQuestion }  from './question-textbox';

@Injectable()
export class QuestionService {

  // TODO: get from a remote source of question metadata
  // TODO: make asynchronous
  getQuestions() {

    let questions: QuestionBase<any>[] = [

      new DropdownQuestion({
        key: 'brave',
        label: 'Bravery Rating',
        options: [
          {key: 'solid',  value: 'Solid'},
          {key: 'great',  value: 'Great'},
          {key: 'good',   value: 'Good'},
          {key: 'unproven', value: 'Unproven'}
        ],
        order: 3
      }),

      new TextboxQuestion({
        key: 'firstName',
        label: 'First name',
        value: 'Bombasto',
        required: true,
        order: 1
      }),

      new TextboxQuestion({
        key: 'emailAddress',
        label: 'Email',
        type: 'email',
        order: 2
      })
    ];

    return questions.sort((a, b) => a.order - b.order);
  }
}

Live Demo:

It is just a code snippet explaining a particular concept and may not have any output

See the Pen service.ts by w3resource (@w3resource) on CodePen.


Finally, display an instance of the form in the AppComponent shell.

TypeScript Code:

import { Component }       from '@angular/core';

import { QuestionService } from './question.service';

@Component({
  selector: 'app-root',
  template: `
    <div>
      <h2>Job Application for Heroes</h2>
      <app-dynamic-form [questions]="questions"></app-dynamic-form>
    </div>
  `,
  providers:  [QuestionService]
})
export class AppComponent {
  questions: any[];

  constructor(service: QuestionService) {
    this.questions = service.getQuestions();
  }
}

Live Demo:

It is just a code snippet explaining a particular concept and may not have any output

See the Pen app.component.ts by w3resource (@w3resource) on CodePen.


Previous: Form Validation
Next: Observables



Share this Tutorial / Exercise on : Facebook and Twitter