Click here to Skip to main content
15,881,173 members
Articles / Programming Languages / Typescript

Creating Dynamic Forms with Dependent Controls and Angular Implementation

Rate me:
Please Sign up or sign in to vote.
4.80/5 (7 votes)
17 Apr 2020CPOL3 min read 13.2K   6  
How to dynamically render form controls that depend on each other's values using json template
Developing applications with dynamic forms is required for many sections of business, and there are many approaches to write those forms natively with most of the UI frameworks. On the other hand, the problem arises when form controls have to depend on each other’s values. We will propose a solution here.

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.

Image 2

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.

JavaScript
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

JavaScript
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.

JavaScript
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:

JavaScript
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:

HTML
<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.

JavaScript
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:

HTML
<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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Egypt Egypt
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --