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.

R15
  • 13,982
  • 14
  • 97
  • 173
  • 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. – vatz88 Feb 20 '20 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 :) – R15 Feb 20 '20 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. – vatz88 Feb 20 '20 at 11:48
  • @vatz88 - My static code was in `html` file. I have idea to start it. You can help me out. – R15 Feb 20 '20 at 12:04
  • @ArvindChourasiya there are many child of LgLevel 1, how to identify which grandchild of LgLevel 2 belongs to which child ? – Sats Feb 24 '20 at 06:43
  • @SatishPai - `LgLevel2` also has `ParentLocationGroup`. So it would inside that child. In the give data `grandchild` you may not able to find `ParentLocationGroup` record that because I have not posted all data here. If you wish to help me. I will all data as well. – R15 Feb 24 '20 at 06:52

3 Answers3

4

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" : []
  }
Yaseer
  • 506
  • 2
  • 14
  • Can you please post .html file as well. – R15 Feb 24 '20 at 08:17
  • @ArvindChourasiya Do you need select tag dropdown or just dropdown ([link](https://jsfiddle.net/csswizardry/Zj4eb/))? – Yaseer Feb 24 '20 at 08:19
  • I need like it is showing in dolor option from the above link. – R15 Feb 24 '20 at 08:21
  • 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. – R15 Feb 24 '20 at 09:41
  • `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. – Yaseer Feb 24 '20 at 09:48
  • I added my sample code to **stackblitz**. Try this link [link](https://stackblitz.com/edit/angular-z454ne) – Yaseer Feb 24 '20 at 09:55
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/208481/discussion-between-yaseer-and-arvind-chourasiya). – Yaseer Feb 25 '20 at 09:09
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.

R15
  • 13,982
  • 14
  • 97
  • 173
NiK648
  • 1,484
  • 1
  • 9
  • 18
  • 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). – R15 Mar 02 '20 at 11:30
  • 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. – NiK648 Mar 02 '20 at 15:56
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 :-)

Heehaaw
  • 2,677
  • 17
  • 27