65.9K
CodeProject is changing. Read more.
Home

Creating Dynamic Forms with Dependent Controls and Angular Implementation

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (7 votes)

Apr 17, 2020

CPOL

3 min read

viewsIcon

13908

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.

Click to enlarge image

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 each FormControl
  • Dependents: Array of depending Controls on this FormControl 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 the FormControl gets notified by other FormControls 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 the sendMessage 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 the FormControl 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 the QueryList 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

History

  • 17th April, 2020: Initial version