In this article I will show how to use dynamic forms in Angular.
In situations where you need to support multiple forms with frequent changes, it's never desirable to rely on fixed markup forms. Instead it's much better to create data driven forms where the form is generated dynamically based on conventions in your model.
In the following example I will show how to use formGroup dynamically to render a simple form with different controls and validation.
The first step is to generate an object model that can describe all scenarios needed by the form functionality.
I have decided to create a base model to form a baseline for more specific question types:
export class QuestionBase<T>{
value: T;
key:string;
text:string;
required:boolean;
order:number;
controlType:string;
}
From the base I have derived new classes to represent Textbox and Dropdown questions.
The idea is that the form will be bound to the high level classes and render the appropriate controls. In the case of textbox it supports multiple html5 types like text, email, url etc.
import {QuestionBase} from './question-base';
export class DropDownQuestion extends QuestionBase<string>{
options = [];
controlType = 'dropdown';
constructor(){
super();
}
}
export class TextboxQuestion extends QuestionBase<string>{
type:string;
controlType = 'textbox';
constructor(){
super();
}
}
The next step is to define a top level model that will contain the list of questions that make up a survey.
import {FormGroup, Validators, FormControl} from '@angular/forms';
export class QuestionModel{
questions = [];
toGroup(){
let group:any = {};
this.questions.forEach((question) => {
if(question.required){
group[question.key] = new FormControl('', Validators.required);
}
else{
group[question.key] = new FormControl('');
}
});
return new FormGroup(group);
}
}
The model is very simple with just an array of questions and a method to convert the questions to a form group. The from group is needed to connect the questions to the form later.
In a scenario like this it's likely that your question definitions will come from an api, but for simplicity I have instantiated the model manually like so:
let question = new TextboxQuestion();
question.key = 'emailAddress';
question.text = 'Email';
question.required = false;
question.type = 'email';
question.order = 3;
this.questionModel.questions.push(question);
let ddQuestion = new DropDownQuestion();
ddQuestion.key = 'country';
ddQuestion.text = 'Country';
ddQuestion.options.push({key:'usa',value:'USA'});
ddQuestion.options.push({key:'germany',value:'Germany'});
ddQuestion.options.push({key:'canada',value:'Canada'});
ddQuestion.options.push({key:'australia',value:'Australia'});
ddQuestion.order = 4;
this.questionModel.questions.push(ddQuestion);
this.questionModel.questions.sort((a,b) => a.order - b.order);
There are more questions in my demo, but I am only including two here for brevity. As you can tell, the question models enable you to specify the type of control to render - in this case email textbox and dropdown.
Now that we have the model we can go ahead and define the form markup.
<div>
<form (ngSubmit)="onSubmit()" [formGroup]="form" >
<div *ngFor="let question of model.questions" class="form-row">
<div class="formHeading">{{question.text}}</div>
<div [ngSwitch]="question.controlType">
<div *ngSwitchCase="'textbox'"><input type="{{question.type}}" id="{{question.key}}" [formControlName]="question.key"></div>
<div *ngSwitchCase="'dropdown'">
<select [formControlName]="question.key">
<option *ngFor="let o of question.options" [value]="o.key">{{o.value}}</option>
</select>
</div>
</div>
<div class="errorMessage" *ngIf="!form.controls[question.key].valid">*required</div>
</div>
<div class="form-row">
<button type="submit" [disabled]="!form.valid">Save</button>
</div>
</form>
<div class="form-row">
<div *ngIf="payLoad"><strong>The form contains the following values</strong></div>
<div>
{{payLoad}}
</div>
</div>
</div>
The key to this is really the dynamic data binding of meta info used to render the form. In addition to control meta we are also adding validation dynamically. The validation requirements are added in the toGroup() method we discussed earlier in the main model.
The save button will be disabled until the form is in a valid state, but once clicked I am rendering out the current values of the form. This proves that any user input is bound back to the model.
Hopefully this will give you an idea of how to do dynamic forms in Angular 2.0. As always my code is available on Github and as a live demo.