Most times when dealing with forms we may come across requirements where we need to create angular dynamic forms based on user inputs. A simple scenario of the mentioned condition would be a question-answer form, which should populate questions based on user selection.
Let’s take a look at how we can implement a angular dynamic form based on user selection from the different topics available.
We will build a quiz form where users can select their favorite topic from a dropdown list. Based on their selection, the question related to that topic will come up. The template uses bootstrap 4 for styling and The content for the question will be hardcoded in this example. However, in a real scenario, you can fetch the questions from a server using an API request.
Enable reactive forms for your project
We need to import the ReactiveForms module inside the app.module.ts file to give the application access to reactive forms directives.
app.module.ts
import { NgModule } from ‘@angular/core’;
import { BrowserModule } from ‘@angular/platform-browser’;
import { ReactiveFormsModule } from ‘@angular/forms’;
import { AppRoutingModule } from ‘./app-routing.module’;
import { AppComponent } from ‘./app.component’;
@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
ReactiveFormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Create a form object model
We need an object model that can describe all scenarios of the input field required by a angular dynamic form. The data model must represent a question. The example includes DynamicFormQuestionComponent which defines a question as the fundamental object in the model.
We need a base class for questions and answer which can be late useful to define form controls.
question-base.ts
export class QuestionBase<T> {
value: T | undefined;
key: string;
label: string;
required: boolean;
order: number;
controlType: string;
type: string;
options: { key: string; value: string; id?: string; label?: string }[];
constructor(
options: {
value?: T;
key?: string;
label?: string;
required?: boolean;
order?: number;
controlType?: string;
type?: string;
options?: { key: string; value: string; id?: string; label?: 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 || ”;
this.type = options.type || ”;
this.options = options.options || [];
}
}
Create control class
From the base class, we need to create three new classes, TextboxQuestion, DropdownQuestion, and RadioQuestion. We will instantiate these question types in order to render the controls dynamically.
Create class files as follows;
question-textbox.ts
import { QuestionBase } from ‘./question-base’;
export class TextBoxQuestion extends QuestionBase<string> {
override controlType = ‘textbox’;
}
question-dropdown.ts
import { QuestionBase } from ‘./question-base’;
export class DropdownQuestion extends QuestionBase<string> {
override controlType = ‘dropdown’;
}
question-radio.ts
import { QuestionBase } from ‘./question-base’;
export class RadioQuestion extends QuestionBase<string> {
override controlType = ‘radio’;
}
Compose form group using service
Let’s create a service that will provide a grouped set of input controls, based on form models. The angular dynamic form uses this service to get the required form group instance which includes all the form controls. The service class will use the metadata from the question model to serve a FormGroup instance.
question-control.service.ts
import { Injectable } from ‘@angular/core’;
import { FormControl, FormGroup, Validators } from ‘@angular/forms’;
import { QuestionBase } from ‘./question-base’;
@Injectable()
export class QuestionControlService {
toFormGroup(questions: QuestionBase<string>[]) {
const group: any = {};
questions.forEach((question) => {
group[question.key] = question.required
? new FormControl(question.value || ”, Validators.required)
: new FormControl(question.value || ”);
});
return new FormGroup(group);
}
}
Compose dynamic form contents
Each question is represented in the form component’s template by a <app-question> tag, which matches an instance of DynamicFormQuestionComponent.
Based on the question object the DynamicFormQuestionComponent will render the details of the individual question. The HTML template is connected to the underlying control object using the [FormGroup] directive.
dynamic-form-question.component.html
<div [formGroup]=“form“>
<div [ngSwitch]=“question.controlType“>
<div *ngSwitchCase=“‘textbox'” class=“d-flex flex-column mb-3”>
<label [attr.for]=“question.key“>{{question.label}}</label>
<input [formControlName]=“question.key“ [id]=“question.key“ [type]=“question.type“>
</div>
<div *ngSwitchCase=“‘dropdown'” class=“d-flex flex-column mb-3”>
<label [attr.for]=“question.key“>{{question.label}}</label>
<select [id]=“question.key“ [formControlName]=“question.key“>
<option *ngFor=“let opt of question.options“ [value]=“opt.key“>{{opt.value}}</option>
</select>
</div>
<div *ngSwitchCase=“‘radio'”>
<div class=“mb-2”>{{question.label}}</div>
<div class=“form-group ml-2”>
<div *ngFor=“let opt of question.options“>
<input [id]=“opt.id“ type=“radio” [value]=“opt.value“
[attr.name]=“question.key“ [name]=“question.key“
[formControlName]=“question.key“>
<label [attr.for]=“opt.id“ class=“ml-2”>{{opt.value}}</label>
</div>
</div>
</div>
</div>
</div>
We are using ngSwitch for rendering questions based on the types. Right now in the example, we are having three question types but we can imagine more.
dynamic-form-question.component.ts
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<string>;
@Input() form!: FormGroup;
get isValid() { return this.form.controls[this.question.key].valid; }
}
The isValid get method takes care of the validity of the controls. If the field is invalid, the submit button will disable.
Supply data
We need another service for supplying the required question data. Let’s name it QuestionService.
Right now we are hardcoding the data, but in the real scenario, you can fetch it from the server using an API request. This service will return dynamic data based on different options selected by the user.
In this example, there are three types of questions for the quiz (football, cricket, and world politics). Based on the selected option the service will serve a specific set of questions as an array. Demo questions related to the topics will add up for the sake of the example.
Question.service.ts
import { Injectable } from ‘@angular/core’;
import { QuestionBase } from ‘./question-base’;
import { of } from ‘rxjs’;
import { RadioQuestion } from ‘./question-radio’;
import { TextBoxQuestion } from ‘./question-textbox’;
import { DropdownQuestion } from ‘./question-dropdown’;
@Injectable()
export class QuestionService {
// TODO: get from a remote source of question metadata
getQuestions(type?: string) {
let questions: QuestionBase<string>[] = [
new TextBoxQuestion({
key: ‘name’,
label: ‘Your Name?’,
type: ‘text’,
required: true,
order: 1
}),
new TextBoxQuestion({
key: ’email’,
label: ‘Your Email?’,
type: ’email’,
required: true,
order: 2
}),
new DropdownQuestion({
key: ‘level’,
label: ‘Expert level?’,
type: ‘dropdown’,
options: [
{id: ‘1’, key: ‘beginner’, value: ‘beginner’, label: ‘Beginner’},
{id: ‘2’, key: ‘intermediate’, value: ‘intermediate’, label: ‘Intermediate’},
{id: ‘2’, key: ‘expert’, value: ‘expert’, label: ‘Expert’},
],
order: 3,
})
];
if(type === ‘world-politics’){
questions = [
…questions,
new RadioQuestion({
key: ‘Chief court of the UN’,
label: ‘Name of the chief court of the UN?’,
type: ‘radio’,
required: true,
options: [
{id: ‘1’, key: ‘a’,
value: ‘Permanent Court of International Justice (PCIJ)’,
label: ‘Permanent Court of International Justice (PCIJ)’
},
{id: ‘2’, key: ‘b’, value: ‘International Criminal Court’,
label: ‘International Criminal Court (ICC)’
},
{id: ‘3’, key: ‘c’, value: ‘European Court of Justice (ECJ)’,
label: ‘European Court of Justice (ECJ)’},
{id: ‘4’, key: ‘d’, value: ‘International Court of Justice’,
label: ‘International Court of Justice (ICJ)’},
{id: ‘5’, key: ‘e’, value: ‘None of the above’,
label: ‘none of the above’}
],
order: 5
}),
new RadioQuestion({
key: ‘Current UN secretary general’,
label: ‘Current UN secretary general?’,
type: ‘radio’,
required: true,
options: [
{id: ‘6’, key: ‘a’, value: ‘António Guterres’, label: ‘antónio guterres’},
{id: ‘7’, key: ‘b’, value: ‘Kofi Annan’, label: ‘kofi annan’},
{id: ‘8’, key: ‘c’, value: ‘Ban Ki-moon’, label: ‘ban ki-moon’},
{id: ‘9’, key: ‘d’, value: ‘Javier Perez de Cuellar’,
label: ‘javier perez de cuellar’},
{id: ’10’, key: ‘e’, value: ‘None of the above’,
label: ‘none of the above’}
],
order: 4
}),
];
};
if(type === ‘football’){
questions = [
…questions,
new RadioQuestion({
key: ‘How high is a full-size goal post?’,
label: ‘How high is a full-size goal post?’,
type: ‘radio’,
required: true,
options: [
{id: ‘1’, key: ‘a’, value: ‘5 feet’, label: ‘size of goal post’},
{id: ‘2’, key: ‘b’, value: ‘6 feet’, label: ‘size of goal post’},
{id: ‘3’, key: ‘c’, value: ‘7 feet’, label: ‘size of goal post’},
{id: ‘4’, key: ‘d’, value: ‘8 feet’, label: ‘size of goal post’}
],
order: 5
}),
new RadioQuestion({
key: ‘How long is a game of professional football?’,
label: ‘How long is a game of professional football?’,
type: ‘radio’,
required: true,
options: [
{id: ‘5’, key: ‘a’, value: ’60 minutes’,
label: ‘professional football time in minutes’},
{id: ‘6’, key: ‘b’, value: ’70 minutes’,
label: ‘professional football time in minutes’},
{id: ‘7’, key: ‘c’, value: ’80 minutes’,
label: ‘professional football time in minutes’},
{id: ‘8’, key: ‘d’, value: ’90 minutes’,
label: ‘professional football time in minutes’}
],
order: 4
})
]
};
if(type === ‘cricket’){
questions = [
…questions,
new RadioQuestion({
key: ‘The distance between the popping crease and the bowling crease is?’,
label: ‘The distance between the popping crease and the bowling crease is?’,
type: ‘radio’,
required: true,
options: [
{id: ‘1’, key: ‘a’, value: ‘4 feet’, label: ‘size of crease’},
{id: ‘2’, key: ‘b’, value: ‘6 feet’, label: ‘size of crease’},
{id: ‘3’, key: ‘c’, value: ‘7 feet’, label: ‘size of crease’},
{id: ‘4’, key: ‘d’, value: ‘8 feet’, label: ‘size of crease’}
],
order: 4
}),
new RadioQuestion({
key: ‘Who won the maximum sixes award for the IPL 2008 season?’,
label: ‘Who won the maximum sixes award for the IPL 2008 season?’,
type: ‘radio’,
required: true,
options: [
{id: ‘5’, key: ‘a’, value: ‘Sachin Tendulkar’, label: ‘IPL 2008’},
{id: ‘6’, key: ‘b’, value: ‘Adam Gilchrist’, label: ‘IPL 2008’},
{id: ‘7’, key: ‘c’, value: ‘Sanath Jayasuriya’, label: ‘IPL 2008’},
{id: ‘8’, key: ‘d’, value: ‘AB de Villiers’, label: ‘IPL 2008’}
],
order: 6
}),
new RadioQuestion({
key: ‘Who was ‘the Man of the match’ in the 1975 World Cup final?’,
label: ‘Who was ‘the Man of the match’ in the 1975 World Cup final?’,
type: ‘radio’,
required: true,
options: [
{id: ‘9’, key: ‘a’, value: ‘Geoffrey Boycott’, label: ‘1975 World Cup final’},
{id: ’10’, key: ‘b’, value: ‘Clive Lloyd’, label: ‘1975 World Cup final’},
{id: ’11’, key: ‘c’, value: ‘Kapil Dev’, label: ‘1975 World Cup final’},
{id: ’12’, key: ‘d’, value: ‘Martin Crowe’, label: ‘1975 World Cup final’}
],
order: 5
}),
];
}
return of(questions.sort((a, b) => a.order – b.order));
}
}
Create a dynamic form template
The DynamicFormComponent will act as a main container for the question form. A *ngFor directive with <app-question /> selector is used for rendering each question.
dynamic-form.component.html
<div *ngIf=“questions“ class=“ml-2”>
<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” class=“btn btn-success” [disabled]=“!form.valid“>Save</button>
</div>
</form>
<div *ngIf=“payLoad“ class=“form-row mt-4”>
<strong>Saved the following values</strong><br>{{payLoad}}
</div>
</div>
dynamic-form.component.ts
import { Component, Input, OnChanges, OnInit, SimpleChanges } 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, OnChanges {
@Input() questions: QuestionBase<string>[] | null = [];
form!: FormGroup;
payLoad = ”;
constructor(private qcs: QuestionControlService) {}
ngOnInit() {}
ngOnChanges(): void {
this.form = this.qcs.toFormGroup(this.questions as QuestionBase<string>[]);
}
onSubmit() {
this.payLoad = JSON.stringify(this.form.getRawValue());
}
}
Display the form
To display the form, the question array returned from the QuestionService is passed to the DynamicFormComponent from app-compnent.html. The dropdown from which users can select different question topics will also get add up in the app component HTML file. Based on the selection, a different question array will get back from the service.
App.component.html
<div class=“container w-50 mt-5 mb-5 pb-3 pt-3” style=“background: aliceblue;”>
<h2 class=“mb-2”>Quiz for Heroes</h2>
<select class=“form-select mb-3” #type (change)=“onChange()“ aria-label=“Default select example”>
<option selected>Open this select menu</option>
<option value=“football”>Football</option>
<option value=“cricket”>Cricket</option>
<option value=“world-politics”>World Politics</option>
</select>
<app-dynamic-form *ngIf=“questions“ [questions]=“questions“></app-dynamic-form>
</div>
app.component.ts
import { Component, ElementRef, OnInit, ViewChild } from ‘@angular/core’;
import { Observable } from ‘rxjs’;
import { QuestionService } from ‘./question.service’;
import { QuestionBase } from ‘./question-base’;
@Component({
selector: ‘app-root’,
templateUrl: ‘./app.component.html’,
styleUrls: [‘./app.component.scss’],
providers: [QuestionService]
})
export class AppComponent implements OnInit{
questions$!: Observable<QuestionBase<any>[]>;
questions!: QuestionBase<any>[];
@ViewChild(‘type’) type!: ElementRef;
constructor(private _service: QuestionService) {}
ngOnInit() {}
onChange = () => {
this.questions$ = this._service.getQuestions(this.type.nativeElement.value);
this.questions$.subscribe((questions: QuestionBase<string>[]) => {
this.questions = questions;
});
}
}
questions$ observable ensures data flow from the service on request. The question$ is assigned inside the onChange lifecycle hook to ensure its execution for each dropdown selection.
The angular dynamic form which renders questions based on different topics selected is completed now.
Let’s view it by running an ng serve command.
Are you looking forward to a hire a professional Angular Development Company?
If yes, then contact us. Perfomatix is one of the top AngularJS development company. We provide angular development services in developing highly scalable software solutions.
To know how we helped our clients from diverse industries, then check out our success stories section.