15

I am very new to Angular. I have got some work on Angular.

I need to bind Nested dropdown list for Json data which is coming from server by calling Rest Api.

Data has one attribute Level, Specifies the level in the hierarchy of the group. Parent will have Immediate Child=1, Grandchild=2 and so on. Child and Grandchild has Group field, which shows inside which parent menu, child menu will be there.

I have tried it to develop but I found all examples of bootstrap with static data in html file and separate CSS file which was complicated to me.

I want to do it dynamically using TypeScript. How can I start working on it.

6
  • Firstly, the data format is XML and not JSON. Can you also add whatever you have tried? Maybe in more details the approach you adopted. Commented Feb 20, 2020 at 11:41
  • @vatz88 - Yeah this is xml just pasted from postman. I have tried html code which has static nested lists. I will try to edit it and will post Json data. You are not gonna like what I have tried :) Commented Feb 20, 2020 at 11:44
  • You don't have to worry what you have coded so far. The approach would be - hardcode the data in ts file, in the html make bindings according to the data you have to render the dropdown. Once that logic is correct, work on getting the data dynamically, and then let angular with the binding do the magic. Commented Feb 20, 2020 at 11:48
  • @vatz88 - My static code was in html file. I have idea to start it. You can help me out. Commented Feb 20, 2020 at 12:04
  • @ArvindChourasiya there are many child of LgLevel 1, how to identify which grandchild of LgLevel 2 belongs to which child ? Commented Feb 24, 2020 at 6:43

3 Answers 3

4
+200

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" : []
  }
Sign up to request clarification or add additional context in comments.

7 Comments

Can you please post .html file as well.
@ArvindChourasiya Do you need select tag dropdown or just dropdown (link)?
I need like it is showing in dolor option from the above link.
I am little confuse with your code. You are not using getData anywhere. Could you please check your code one and add opening and closings.
getData is just a method and you can call it where you want to use. In that method I am formatting the json data to display in the html. This is just a sample code.
|
4

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.

2 Comments

I have checked your code. It's working. Thank you for posting answer. Can you please tell me how are you getting my data. for http request(mocky.io).
Mocky is a online tool for creating mock rest api. I took the data you posted and used it to create a rest api using mocky.
2

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

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.