Creating Dynamic Forms with Dependent Controls and Angular Implementation






4.80/5 (7 votes)
How to dynamically render form controls that depend on each other's values using json template
Introduction
In this article, we are going to propose a solution demonstrating how to dynamically render form controls that depend on each other's values using json template.
My solution is based on having an abstract form control with a centric form container as each control will push a notification to the parent form when needed in order to notify its dependants.
The class diagram above shows:
- a form consists of controls that implement the
IFormControl
Interface - a form control has an instance of
ControlData
for rendering options - each control should have a reference to a method inside the form that will be used to receive a message from such a control and broadcast that message to its dependants.
Basic Scenario
- User edits form input
- Input control check if it has dependants
- If yes:
- send message to the form with params (control name and dependants)
- Form handles the call and Iterates over all controls then forward the message to which match a dependant name.
Implementation
We will implement the solution in Angular framework with typescript language that has interface datatype which apparently eases the implementation and makes it straightforward.
Step 1
Create interface for data.
export interface IControlData {
controlName: string;
controlType: string;
placeholder?: string;
dependents?: string[];
order?: number;
value?: any;
DependentKey?: any;
options?: Array<{
optionName: string;
value: string;
dependentKey?: any;
}>;
validators?: {
required?: boolean;
minlength?: number;
maxlength?: number;
};
}
In the above code, we created all the properties needed to create form controls.
Example
controlName
: to hold the name of eachFormControl
Dependents
: Array of depending Controls on thisFormControl
that have to be notified at certain events according to the business logic
Sample Data
export const FormData = [
{
controlName: 'Name',
controlType: 'text',
valueType: 'text',
dependents: ['Gender'],
placeholder: 'Enter name'
},
{
controlName: 'Type',
placeholder: 'Select Type',
controlType: 'radio',
dependents: ['Email'],
options: [{
optionName: 'Type A',
value: 'A'
}, {
optionName: 'Type B',
value: 'B'
}]
},
{
controlName: 'Gender',
placeholder: 'Select gender',
controlType: 'select',
dependents: ['Age', 'Books'],
options: [{
optionName: 'Male',
value: 'male'
}, {
optionName: 'Female',
value: 'female'
}],
validators: {
required: true
}
]
Step 2
Create the interface that will be implemented by form controls components.
export interface IControl {
controlData: IControlData;
sendMessage: EventEmitter<any>;
doSomething(params: any): void;
}
In the above code, we created a property controlData
of type IControlData
to hold the data of each control.
SendMessage
: Event Emittter used to invoke a function in the parent component –form component- that will broadcast the carried message to the dependants control through form.doSomething
: will be used to elaborate how theFormControl
gets notified by otherFormControl
s changes and do actions upon them.
Step 3
Create a component for each type of the form control.
Example
The typescript file:
export class DropdownComponentComponent implements IControl, OnInit {
@Input() controlData: IControlData;
@Output() sendMessage: EventEmitter<any>;
constructor() {
this.sendMessage = new EventEmitter<any>();
}
ngOnInit() {
}
onchange(val: any): void {
this.controlData.value = val;
this.sendMessage.emit({value: 'dropdown ' + this.controlData.controlName,
controls: this.controlData.dependents});
}
doSomething(params: any) {
// filter dropdown by id ;
alert( this.controlData.controlName + ' received from ' + params);
}
}
In the above code, a component is created for the dropdown formControl
which implements the IControl
interface.
It takes data from parent form using the “@Input
” decorator.
onChange
: a function that triggers thesendMessage
when the control value changes and pass data to parent control (dynamic form) so that its dependents get notified of this change.doSomething
: a function that just sends an alert that this control received notification from theFormControl
it depends on.
Html. File should look like this:
<select [name]="controlData.controlName" [id]="controlData.controlName"
(change)="onchange($event.target.value)">
<option value="">{{controlData.placeholder}}</option>
<option *ngFor="let option of controlData.options"
[value]="option.value">{{option.optionName}}</option>
</select>
This is an implementation of the dropdown that shows how we’ll make use of the Input data (Control Data) to render the dropdown control.
*The same thing can be repeated to create other types of controls.
Step 4
Create the dynamic form component that will hold all these controls together in one form group.
export class DynamicFormComponent implements OnInit {
@Input() controlDataItems: IControlData[];
@ViewChildren('control') controls: QueryList<IControl>;
form: FormGroup;
submitted: boolean;
constructor() {
}
ngOnInit() {
const formGroup = {};
this.controlDataItems.forEach(formControl => {
formGroup[formControl.controlName] = new FormControl('');
});
this.form = new FormGroup(formGroup);
}
sendMessageToControls(params: any): void {
this.controls.forEach(control => {
if (params.controls.indexOf(control.controlData.controlName) > -1) {
control.doSomething(params.value);
}
});
}
In the above code, the ngOnInit
function loops over the formData
creating a formControl
for each control based on control name, and then adds them in formGroup
.
@ViewChildren
control will allow getting theQueryList
of elements or directives from the view DOM. Any time a child element is added, removed, or moved, the query list will be updated, and the changes observable of the query list will emit a new value [Angular Documentation].
Which means that every time a child component changes, the parent component will get notified.
sendMessageToControls
: a function that being invoked by form controls then loops over controls to check if it depends on any other child control, to call the “doSomething
” method to get notified of this change and perform operations upon this change.
The HTML file for dynamic form should look like this:
<div class="form-group" [formGroup]="form" >
<div *ngFor="let input of controlDataItems" class="form-row">
<label [attr.for]="input.controlName">{{input.controlName}}</label>
<div [ngSwitch]="input.controlType" >
<app-textbox-component *ngSwitchCase="'text'"
#control [controlData] ="input"
(sendMessage)="sendMessageToControls($event)" >
</app-textbox-component>
<app-dropdown-component #control *ngSwitchCase="'select'"
[controlData] ="input" (sendMessage)="sendMessageToControls($event)">
</app-dropdown-component>
<app-radio-component *ngSwitchCase="'radio'"
#control [controlData] ="input"
(sendMessage)="sendMessageToControls($event)">
</app-radio-component>
</div>
</div>
<button type="submit" [disabled]="form.invalid" (click)="submitForm()">
Submit
</button>
</div>
In this HTML page, *ngFor
is used to loop over control data collection to get each control data separately, and then use *ngSwitch
to create the input control based on controls types.
Conclusion
Now we could easily create dynamic forms with depending components, where each control will have their properties dynamically added, and have easier communication with each other.
References
- Dynamic forms
- How to create forms dynamically in Angular 7 using FormControl
- Angular dynamic forms: ng-switch approach
History
- 17th April, 2020: Initial version