How to create dropdown list and bind with data coming from server

It seems you already have another answer that meets your requirements. But this solution took me sometime to come up with. So decided to post it anyway.

The below code snippet is used to construct the tree like structure of the parent-child hierarchical data:

  processData(data) {
    let locationData = data.ArrayOfLocationGroup.LocationGroup;
    let level0 = [];
    let tempMap = {};
    for (let i = 0; i < locationData.length; i++) {
      let currItem = this.getDataObject(locationData[i]);
      if (tempMap[currItem.id] == undefined) {
        tempMap[currItem.id] = currItem;
        if (tempMap[currItem.parentLocationGroup] == undefined) {
          tempMap[currItem.parentLocationGroup] = { children: [] };
        }
        tempMap[currItem.parentLocationGroup].children.push(currItem);
      } else {
        if (tempMap[currItem.id]) {
          currItem.children = tempMap[currItem.id].children;
        }
        tempMap[currItem.id] = currItem;
        if (tempMap[currItem.parentLocationGroup] == undefined) {
          tempMap[currItem.parentLocationGroup] = { children: [] };
        }
        tempMap[currItem.parentLocationGroup].children.push(currItem);
      }
      if (currItem.lgLevel == "0") {
        if (level0.indexOf(currItem) == -1) {
          level0.push(currItem);
        }
      }
    }
    this.levelData = level0;
  }

The aggregated data is passed as input to a dropdown component which renders it as a multilevel dropdown menu.

This solution will supposedly work for any level of children. The dropdown component can be modified to change the way the data is rendered as per your requirements.

I took the html and css for the multilevel dropdown menu from here:
https://phppot.com/css/multilevel-dropdown-menu-with-pure-css/
The code to close the menu dropdown when clicked outside from this answer:
https://stackoverflow.com/a/59234391/9262488

Hope you find this useful.


This is a sample coded which you need as per nested level data from your json data. Now you can for loop the formatted json data in the DOM using model data. I hope this will help you out create a multi-level drop-down

groupBy(xs, key) {
   return xs.reduce(function (rv, x) {
     (rv[x[key]] = rv[x[key]] || []).push(x);
    return rv;
   }, {});
 }

var model;

getData() {
 var   sampleData = {
  "ArrayOfLocationGroup": {
    "LocationGroup": [
      ...
      ...//Server response data
      ],
    "_xmlns:xsd": "http://www.w3.org/2001/XMLSchema"
  }
 }    

var list = this.sampleData["ArrayOfLocationGroup"]["LocationGroup"];
var formattedList = [];

list.forEach(element => {

  var obj = {  //Make sure your server response data to like this structure
    "Id": element.Id,
    "Name": element.Name,
    "GroupId": element.GroupId.__text,
    "ParentLocationGroup": element.ParentLocationGroup.__text,
    "LgLevel": element.LgLevel.__text,
    "Child" : []
  }
  formattedList.push(obj);
});

var groupDataList = this.groupBy(formattedList, "LgLevel");

var parents = groupDataList[0];
var child = groupDataList[1];
var childOfChild = groupDataList[2];

child.forEach(c => {
  c.Child = childOfChild.filter(x => x.ParentLocationGroup == c.Id);
})

parents.forEach(p => {
  p.Child = child.filter(x => x.ParentLocationGroup == p.Id);
})

this.model = parents;
}

Html File

    <ul class="nav site-nav">
     <li class=flyout>
      <a href=#>Dropdown</a>
      <!-- Flyout -->
      <ul class="flyout-content nav stacked">
        <li *ngFor="let parent of model" [class.flyout-alt]="parent.Child.length > 0"><a href=#>{{parent.Name}}</a>
          <ul *ngIf="parent.Child.length > 0" class="flyout-content nav stacked">
            <li *ngFor="let c of parent.Child" [class.flyout-alt]="c.Child.length > 0"><a href=#>{{c.Name}}</a>
              <ul *ngIf="c.Child.length > 0" class="flyout-content nav stacked">
                <li *ngFor="let cc of c.Child" [class.flyout-alt]="cc.Child.length > 0"><a href=#>{{cc.Name}}</a></li>
              </ul>
            </li>
          </ul>
        </li>
      </ul>
    </li>
  </ul>

As per your server response data organize the model data. Response json format changed (__text to #text)

 var obj = {
    "Id": element.Id,
    "Name": element.Name && element.Name.#text ? element.Name.#text : element.Name,
    "GroupId": element.GroupId && element.GroupId.#text ? element.GroupId.#text : element.GroupId,
    "ParentLocationGroup": element.ParentLocationGroup && element.ParentLocationGroup.#text ? element.ParentLocationGroup.#text : element.ParentLocationGroup,
    "LgLevel": element.LgLevel && element.LgLevel.#text ? element.LgLevel.#text : element.LgLevel,
    "Child" : []
  }

Why not create a tree component and bind inputs to it recursively?

The proposed solution is

  • depth-agnostic - it will work for any number of levels in your data tree (even if it changes ad-hoc)
  • quite efficient - it aggregates your data in O(n).

First design the data model - it has to be a tree-node structure:

export interface GroupId { /* appropriate members... */ }

export interface ParentLocationGroup { /* other members... */ __text: string; }

export interface LgLevel { /* other members... */ __text: string; }

export interface DataModel {
  Id: string,
  Name: string,
  GroupId: GroupId,
  ParentLocationGroup: ParentLocationGroup,
  LgLevel: LgLevel,
  children: DataModel[]
}

Then aggregate your data in the top-level component (or even better - in your data service; you should be able to abstract that easily enough):

// dropdown.component.ts

@Component({
  selector: 'app-dropdown',
  template: `
    <ul class="nav site-nav">
      <li class=flyout>
        <a href=#>Dropdown</a>
        <app-dynamic-flyout [data]="data"></app-dynamic-flyout>
      </li>
    </ul>
  `
})
export class DropdownComponent {

  data: DataModel[];

  constructor(dataService: YourDataService){

    let data;
    dataService.getYourData()
      .pipe(map(d => d.ArrayOfLocationGroup.LocationGroup)))
      // Make sure every item has the `children` array property initialized
      // since it does not come with your data
      .subscribe(d => data = d.map(i => ({...i, children: []})));

    // Create a lookup map for building the data tree
    let idMap = data.reduce((acc, curr) => ({...acc, [curr.Id]: curr}), {});
    this.data = data.reduce(
      (acc, curr) => curr.LgLevel.__text == 0 || !curr.ParentLocationGroup
        // Either the level is 0 or there is no parent group
        // (the logic is unclear here - even some of your lvl 0 nodes have a `ParentGroup`)
        // -> we assume this is a top-level node and put it to the accumulator
        ? [...acc, curr]
        // Otherwise push this node to an appropriate `children` array
        // and return the current accumulator
        : idMap[curr.ParentLocationGroup.__text].children.push(curr) && acc, 
      []
    );
  }
}

And create the recurrent dynamic flyout component:

// dynamic-flyout.component.ts

@Component({
  selector: 'app-dynamic-flyout',
  template: `
    <ul *ngIf="!!data.length" class="flyout-content nav stacked">
      <li *ngFor="let item of data" [class.flyout-alt]="!!item.children.length">
        <a href=#>{{item.Name}}</a>
        <app-dynamic-flyout [data]="item.children"></app-dynamic-flyout>
      </li>
    </ul>
  `
})
export class DynamicFlyoutComponent {
  @Input() data: DataModel[];
}

The solution is not tested but it shall point you in a right direction...

Hope this helps a little :-)