Angular Dynamic Forms with Smart UI for Angular

Overview

The following guide shows how to create Angular Dynamic Forms using Smart components.

The Angular application that we are going to create will use Smart Angular TextBoxComponent, ButtonComponent, PasswordTextBoxComponent and DropDownListComponent. The purpose of the application is to demonstrate how to create dynamic forms with questions for a job application of a local business. The idea is that dynamic forms can be used to quickly create different types of form controls.

Project Configuration

  1. Create a new Angular project with the following command:
    ng new angular-dynamic-forms
    
  2. Create the question-base.ts - from inside the new folder create a TS file containing the QuestionBase class that will contain the needed properties for the questions. Here's the content of the file:
    export class QuestionBase {
      value: T;
      key: string;
      label: string;
      password: string;
      required: boolean;
      order: number;
      controlType: string;
      type: string;
      options: {key: string, value: string}[];
    
      constructor(options: {
          value?: T,
          key?: string,
          label?: string,
          password?: string,
          required?: boolean,
          order?: number,
          controlType?: string,
          type?: string
        } = {}) {
        this.value = options.value;
        this.key = options.key || '';
        this.label = options.label || '';
        this.password = options.password || '';
        this.required = !!options.required;
        this.order = options.order === undefined ? 1 : options.order;
        this.controlType = options.controlType || '';
        this.type = options.type || '';
      }
    }
    

    The questions will be created as instances of this class and will pass the options for the question controls during initialization.

  3. Create questions in separate TS files in the same directory:
    • question-dropdown.ts - contains the class definiton of the DropDownQuestion:
      import { QuestionBase } from './question-base';
      
      export class DropdownQuestion extends QuestionBase {
        controlType = 'dropdown';
        options: {key: string, value: string}[] = [];
      
        constructor(options: {} = {}) {
          super(options);
          this.options = options['options'] || [];
        }
      }
      

      We defined an additional controlType property of the class that is going to be used for the forms to determine the type of control to display.

    • question-textbox.ts - contains the class definition for the TextBoxQuestion:
      import { QuestionBase } from './question-base';
      
      export class TextboxQuestion extends QuestionBase {
        controlType: string;
        type: string;
      
        constructor(options: {} = {}) {
          super(options);
          this.type = options['type'] || '';
          this.controlType = options['controlType'] || 'textbox';
        }
      }
      
  4. Create a serivce called question.service.ts that will be used to fetch the questions:
    import { Injectable }       from '@angular/core';
    
    import { DropdownQuestion } from './question-dropdown';
    import { QuestionBase }     from './question-base';
    import { TextboxQuestion }  from './question-textbox';
    import { of } from 'rxjs';
    
    @Injectable()
    export class QuestionService {
    
      // TODO: get from a remote source of question metadata
      getQuestions() {
    
        let questions: QuestionBase[] = [
    
          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: 4
          }),
    
          new TextboxQuestion({
            key: 'firstName',
            label: 'First name',
            value: 'Bombasto',
            required: true,
            order: 1
          }),
    
          new TextboxQuestion({
            key: 'emailAddress',
            label: 'Email',
            type: 'email',
            required: true,
            order: 2
          }),
    
          new TextboxQuestion({
            key: 'password',
            label: 'Password',
            type: 'password',
            controlType: 'password',
            required: true,
            order: 3
          })
        ];
    
        return of(questions.sort((a, b) => a.order - b.order));
      }
    }
    

    Four messages are created and the getQuestions method will return them as an Observable.

  5. Create a question-control.service.ts - this service will create the actual FormGroup and will popuplate it with questions. Here's the content of the file:
    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<string>[] ) {
        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);
      }
    }
    

    The toFormGroup method will create the FormGroup. It accepts an array of questions.

    If a question has it's required property applied then a FormGroup's Validator will be used.

  6. Create a new DynamicFormComponent via the following command line:
    ng generate component dynamic-form
    

    A new folder will be created with two configuration files:

    • dynamic-form.component.html - the HTML page for the component. It will contain a single form with a submit button. Here's the content of the file:
      <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">
              <smart-button type="submit" [disabled]="!form.valid">Save</smart-button>
            </div>
          </form>
        
          <div *ngIf="payLoad" class="form-row">
            <strong>Saved the following values</strong><br>{{payLoad}}
          </div>
        </div>
      

      Notice the app-question component that is nested inside the form. That's the component for the questions that will be created next.

      All questions will be created thanks to the *ngFor Angular attribute.

    • dynamic-form.component.ts - initializes the FormGroup and defines a onSubmit method that is called when the Form is submitted.
      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[] = [];
        form: FormGroup;
        payLoad = '';
      
        constructor(private qcs: QuestionControlService) {  }
      
        ngOnInit() {
          this.form = this.qcs.toFormGroup(this.questions);
        }
      
        onSubmit() {
          this.payLoad = JSON.stringify(this.form.getRawValue());
        }
      }
      

      Remember to declare the QuestionControlService as a provider for the DynamicFormComponent. It can also be declared inside the app.module.ts but since we are going to use it inside this component only we will include it here.

  7. Create a new DynamicFormQuestionComponent via the following command line:
    ng generate component dynamic-form-question
    

    A new folder will be created with the following configuration files:

    • dynamic-form-question.component.html - the HTML page that will display the question. All controls that will be used to display a question are placed inside this file and will be created depending on the question.controlType. Here's the code:
      <div [formGroup]="form">
          <div [ngSwitch]="question.controlType" class="control-container">
              <smart-text-box *ngSwitchCase="'textbox'" [label]="question.label" [formControlName]="question.key"
                  [id]="question.key"></smart-text-box>
      
              <smart-password-text-box *ngSwitchCase="'password'" [label]="question.label" [formControlName]="question.key"
                  [id]="question.key" [showPasswordIcon]="true" [showPasswordStrength]="true"></smart-password-text-box>
      
              <smart-drop-down-list [id]="question.key" *ngSwitchCase="'dropdown'" [label]="question.label"
                  [formControlName]="question.key">
                  <option *ngFor="let opt of question.options" [value]="opt.key">{{opt.value}}</option>
              </smart-drop-down-list>
              <div class="errorMessage" *ngIf="!isValid">{{question.label}} is required</div>
          </div>
      </div>
      

      An additional DIV element is used to display Validation messages when the control for the question is required.

    • dynamic-form-question.component.ts - the TS definition of the component which contains the isValid check used to determine whether the validation message is visible or not:
      import { Component, Input } from '@angular/core';
      import { FormGroup }        from '@angular/forms';
      
      import { QuestionBase }     from './../question-base';
      
      @Component({
        selector: 'app-question',
        styleUrls: ['./dynamic-form-question.component.css'],
        templateUrl: './dynamic-form-question.component.html'
      })
      export class DynamicFormQuestionComponent {
        @Input() question: QuestionBase<string>;
        @Input() form: FormGroup;
        get isValid() { return this.form.controls[this.question.key].valid; }
      }
      

      The @Input directive is necessary in order to use the metadata inside the HTML where the binding expression is used.

  8. Configure app.module.ts - in order to use the question services and the components we have to declare them insinde the main application module file:
    import { BrowserModule } from '@angular/platform-browser';
    import { ReactiveFormsModule } from '@angular/forms';
    import { NgModule } from '@angular/core';
    
    import { TextBoxModule } from 'smart-webcomponents-angular/textbox';
    import { PasswordTextBoxModule } from 'smart-webcomponents-angular/passwordtextbox';
    import { DropDownListModule } from 'smart-webcomponents-angular/dropdownlist';
    import { ButtonModule } from 'smart-webcomponents-angular/button';
    
    import { AppComponent } from './app.component';
    import { DynamicFormComponent } from './dynamic-form/dynamic-form.component';
    import { DynamicFormQuestionComponent } from './dynamic-form-question/dynamic-form-question.component';
    
    @NgModule({
      imports: [BrowserModule, ReactiveFormsModule, TextBoxModule, PasswordTextBoxModule, DropDownListModule, ButtonModule],
      declarations: [AppComponent, DynamicFormComponent, DynamicFormQuestionComponent, DynamicFormQuestionComponent],
      bootstrap: [AppComponent]
    })
    export class AppModule {
      constructor() {
      }
    }
    

    Here we also include the Modules for the PasswordTextBoxComponent, TextBoxComponent, ButtonComponent and DropDownListComponent.

  9. Configure app.component.ts - finally we need to configure the main application component to use the DynamicFormComponent. Here's the content of the file:
    import { Component }       from '@angular/core';
    
    import { QuestionService } from './question.service';
    import { QuestionBase }    from './question-base';
    import { Observable }      from 'rxjs';
    
    @Component({
      selector: 'app-root',
      template: `
        <div>
          <h2>Job Application for Heroes</h2>
          <app-dynamic-form [questions]="questions$ | async"></app-dynamic-form>
        </div>
      `,
      providers:  [QuestionService]
    })
    export class AppComponent {
      questions$: Observable<QuestionBase<any>[]>;
    
      constructor(service: QuestionService) {
        this.questions$ = service.getQuestions();
      }
    }
    

    The QuestionService is included here as a provider in order to use it to fetch all questions and pass them to the DynamicFormComponent.

  10. Build and Run the Application - build the application with the following command line:
    ng build --prod
    

    A live version of the code presented in this tutorial can be found in the demo Angular Dynamic Forms.

    Here is what the application looks like when launched.

    Live Demo: Dynamic Forms