14. Forms Dynamic Forms

QuestionsModule:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { ReactiveFormsModule } from '@angular/forms';

import { DynamicHostComponent } from './dynamic-host.component';
import { DynamicFormComponent } from './dynamic-form.component';
import { DynamicFormQuestionComponent } from './dynamic-form-question.component';

const routes: Routes = [
  { path: 'questions', component: DynamicHostComponent }
];

@NgModule({
  imports: [
    CommonModule,
    ReactiveFormsModule,
    RouterModule.forChild(routes)
  ],
  declarations: [
    DynamicHostComponent,
    DynamicFormComponent,
    DynamicFormQuestionComponent
  ]
})
export class QuestionsModule { }

QuestionService:

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: 'power',
                label: 'Technology Power',
                options: [
                  { key: 'dataBinding', value: 'Data Binding' },
                  { key: 'animations', value: 'Animations' },
                  { key: 'tooling', value: 'Tooling' },
                  { key: 'beautifulCSS', value: 'Beautiful CSS'}
                ],
                order: 3
              }),
              new TextboxQuestion({
                key: 'name',
                label: 'Name',
                value: 'Angular',
                required: true,
                order: 1
              }),
              new TextboxQuestion({
                key: 'url',
                label: 'URL',
                type: 'url',
                order: 2
              })
            ];
            return questions.sort((a, b) => a.order - b.order);
          }

}

QuestionControlService:

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

}

QuestionBase:

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

}

TextboxQuestion:

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

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

  constructor(options: {} = {}){
    super(options);
    this.type = options['type'] || '';
  }
}
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'] || [];
  }

}

DynamicFormQuestionComponent:

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

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

@Component({
  selector: 'app-dynamic-form-question',
  template: `
        <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>
  `
})
export class DynamicFormQuestionComponent {
            @Input() question: QuestionBase<any>;
            @Input() form: FormGroup;
            get isValid() { return this.form.controls[this.question.key].valid; }
}

DynamicFormComponent:

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',
  template: `
        <div>
            <form (ngSubmit)="onSubmit()" [formGroup]="form">
                    <div *ngFor="let question of questions" class="form-row">
                      <app-dynamic-form-question [question]="question" [form]="form">
                      </app-dynamic-form-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>
  `,
  providers: [ QuestionControlService ]
})
export class DynamicFormComponent {
      @Input() questions: QuestionBase<any>[] = [];

      form: FormGroup;
      payLoad = '';

      constructor(private questionControlService: QuestionControlService) { }

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

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

}

DynamicHostComponent:

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

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

@Component({
  selector: 'app-dynamic-host',
  template: `
        <div>
              <h2>Technology Power for Angular</h2>
              <app-dynamic-form [questions]="questions"></app-dynamic-form>
        </div>
  `,
  providers: [ QuestionService ]
})
export class DynamicHostComponent {
        questions: any[];

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

}

Let's have a look at the results in Chromium:

results matching ""

    No results matching ""