Peter Penzov
Peter Penzov

Reputation: 1606

Generate navigation menu items from JSON structure

I have this JSON example into which I want to store navigation menu for Angular project:

{
  "menus": [{
    "name": "nav-menu",
    "style": "nav navbar-toggler",
    "items": [{
      "id": "1",
      "name": "Navigation menu",
      "parent_id": null,
      "style": "btn btn-default w-100"
    }, {
      "id": "2",
      "name": "Home and garden",
      "parent_id": "1",
      "style": "btn btn-default w-100"
    }, {
      "id": "3",
      "name": "Cookers",
      "parent_id": "2",
      "style": "btn btn-default w-100"
    }, {
      "id": "4",
      "name": "Microwave ovens",
      "parent_id": "2",
      "style": "btn btn-default w-100"
    }, {
      "id": "5",
      "name": "Fridges",
      "parent_id": "2",
      "style": "btn btn-default w-100"
    }, {
      "id": "6",
      "name": "PC peripherials",
      "parent_id": "1",
      "style": "btn btn-default w-100"
    }, {
      "id": "7",
      "name": "Head phones",
      "parent_id": "6",
      "style": "btn btn-default w-100"
    }, {
      "id": "8",
      "name": "Monitors",
      "parent_id": "6",
      "style": "btn btn-default w-100"
    }, {
      "id": "9",
      "name": "Network",
      "parent_id": "6",
      "style": "btn btn-default w-100"
    }, {
      "id": "10",
      "name": "Laptop bags",
      "parent_id": "6",
      "style": "btn btn-default w-100"
    }, {
      "id": "11",
      "name": "Web Cams",
      "parent_id": "6",
      "style": "btn btn-default w-100"
    }, {
      "id": "12",
      "name": "Remote cameras",
      "parent_id": "11",
      "style": "btn btn-default w-100"
    }, {
      "id": "13",
      "name": "Laptops",
      "parent_id": "6",
      "style": "btn btn-default w-100"
    }, {
      "id": "14",
      "name": "15' Laptops",
      "parent_id": "13",
      "style": "btn btn-default w-100"
    }, {
      "id": "15",
      "name": "17' Laptops",
      "parent_id": "13",
      "style": "btn btn-default w-100"
    }]
  }]
}

The idea is the edit the JSON when it's needed and to generate the navigation menu based on this data. How this can be implemented?

Upvotes: 1

Views: 2190

Answers (4)

AlokeT
AlokeT

Reputation: 1196

I do this in my new Project here is the partial code for your reference. We used here as a JSON file to store our menu, load it using loader after app stat up. Then used in our html the loaded menu.

I didn't use your code or json reference so that you can take the idea and implement it in your own way.


sidenav-menu.json

Put this file under asset/config/sidenav-menu.json

 [
  {
    "icon": "dashboard",
    "name": "Dashboard",
    "isShow": false,
    "userRole": [
      "ROLE_USER",
      "ROLE_ADMIN"
    ],
    "href": "dashboard",
    "isChildAvailable": true,
    "child": [
      {
        "userRole": [
           "ROLE_USER",
           "ROLE_ADMIN"
        ],
        "icon": "cog",
        "name": "Config",
        "href": "admin/dashboard/config",
      }
    ]
  }]

menu-loader.service.ts

Put this file under Shared/Services/loader/MenuLoader.service.ts

    import { HttpClient } from '@angular/common/http';
    import { Injectable } from '@angular/core';
    
    @Injectable({
  providedIn: 'root'
})
    export class MenuLoaderService {
      sidenavMenu: any[];
      constructor(
    private http: HttpClient) {
  }

  load(): Promise<any> {
    console.log('MenuLoaderService', `getting menu details`);
    const promise = this.http.get('../assets/data/config/sidenav-menu.json')
      .toPromise()
      .then((menus: any[]) => {
        this.sidenavMenu = menus;
        return menus;
      }).catch(this.handleError());

    return promise;
  }

  private handleError(data?: any) {
    return (error: any) => {
      console.log(error);
    };
  }

  getMenu() {
   return this.sidenavMenu;
  }
}

Add MenuLoader Service to constructer and fetch the menu from it

app.component.ts

export class AppComponent implements OnInit {
  constructor(
   // other inject modules
    private menuLoader: MenuLoaderService
  ) {
    this.getMenuList();
  }

  ngOnInit(): void {
   }

  getMenuList() {
    this.menuLoader.load();
  }
}

HTML Part

my-component.component.html For the display menu, I used Material design

<!-- Other codes -->
<mat-nav-list style="overflow-y: auto; height: 70.5vh;">
            <div *ngFor="let link of sidenavMenu" [@slideInOut]="link.show ? 'out' : 'in'">
              <span *ngIf="checkUrlAccessibleOrNot(link?.userRole, link?.loginType)">
                <ng-container *ngIf="link.isChildAvailable; else elseTemplate">
                  <mat-list-item role="link" #links [ngClass]="{'bg-amber-gradient': routeIsActive(link?.href)}"
                    (click)="showHideNavMenu(link)">
                    <mat-icon class="component-viewer-nav" matListIcon svgIcon="{{link.icon}}"></mat-icon>
                    <a matLine [matTooltip]="link.name" matTooltipPosition="after">
                      {{ link.name }}</a>
                    <mat-icon *ngIf="!link.isShow">add</mat-icon>
                    <mat-icon *ngIf="link.isShow">remove</mat-icon>
                  </mat-list-item>
                  <mat-divider></mat-divider>
                  <span *ngIf="link.isShow">
                    <div *ngFor="let clink of link.child; let last = last;">
                      <mat-list-item *ngIf="checkUrlAccessibleOrNot(clink?.userRole, link?.loginType)" role="link" #links
                        routerLinkActive="active-link" routerLink="{{clink.href}}" (click)="onLinkClick()" class="p-l-10">
                        <mat-icon class="component-viewer-nav" matListIcon svgIcon="{{clink.icon}}"></mat-icon>
                        <a matLine [matTooltip]="clink.name" matTooltipPosition="after">
                          {{ clink.name}}</a>
                      </mat-list-item>
                      <mat-divider></mat-divider>
                    </div>
                  </span>
                </ng-container>
                <ng-template #elseTemplate>
                  <mat-list-item role="link" #links routerLinkActive="active-link" routerLink="{{link.href}}"
                    (click)="onLinkClick()">
                    <mat-icon class="component-viewer-nav" matListIcon svgIcon="{{link.icon}}"></mat-icon>
                    <a matLine [matTooltip]="link.name" matTooltipPosition="after">
                      {{ link.name }}</a>
                  </mat-list-item>
                  <mat-divider></mat-divider>
                </ng-template>
              </span>
        </div>
              </mat-nav-list>
<!-- Other html codes -->

my-component.component.ts

export class MyComponentComponent implements OnInit {

this.sidenavMenu = []; constructor(

    menuLoader: MenuLoaderService
  ) {
      this.sidenavMenu = menuLoader.getMenu();
  }

  ngOnInit() {
    
  }


  showHideNavMenu(link: any) {
    this.sidenavMenu.forEach(indLink => {
      if (indLink.name !== link.name) {
        indLink.isShow = false;
      }
    });
    this.sidenavMenu[this.sidenavMenu.indexOf(link)].isShow =
      !this.sidenavMenu[this.sidenavMenu.indexOf(link)].isShow;
  }

 
  routeIsActive(routePath: string) {
    const mainUrl = this.router.url;
    const splitUrls = mainUrl.split('/');
    return splitUrls[1] === routePath;
  }

  onLinkClick(): void {
    if (this.isMobileView) {
      this.menuSidenav.close();
    }
  }

  checkUrlAccessibleOrNot(roleList: string[], loginType: 'USER' | 'ADMIN'): boolean {
    // Implement it in your own way
  }

  
}

EDIT

Animation Code:

animations: [
    trigger('slideInOut', [
      state('in', style({
        transform: 'translate3d(0, 0, 0)'
      })),
      state('out', style({
        transform: 'translate3d(100%, 0, 0)'
      })),
      transition('in => out', animate('400ms ease-in-out')),
      transition('out => in', animate('400ms ease-in-out'))
    ]),
    trigger('detailExpand', [
      state('collapsed', style({ height: '0px', minHeight: '0', display: 'none' })),
      state('expanded', style({ height: '*' })),
      transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
    ]),
  ]

I put a fraction of code here so you can implement it in your own way this is an idea how you can achieve it also I didn't add any demo. If you have any issue understanding the code let come into the comment and resolved it. Happy Coding... :D

Upvotes: 1

Rakesh Choyal
Rakesh Choyal

Reputation: 56

So first you need to convert your flat list to tree-like structure.

    function unflatten(arr) {
      var tree = [],
          mappedArr = {},
          arrElem,
          mappedElem;

      // First map the nodes of the array to an object -> create a hash table.
      for(var i = 0, len = arr.length; i < len; i++) {
        arrElem = arr[i];
        mappedArr[arrElem.id] = arrElem;
        mappedArr[arrElem.id]['children'] = [];
      }


      for (var id in mappedArr) {
        if (mappedArr.hasOwnProperty(id)) {
          mappedElem = mappedArr[id];
          // If the element is not at the root level, add it to its parent array of children.
          mappedElem.displayName = mappedElem.name;
          mappedElem.icon = '';
          if (mappedElem.parent_id) {
            mappedArr[mappedElem['parent_id']]['children'].push(mappedElem);
          }
          // If the element is at the root level, add it to first level elements array.
          else {
            tree.push(mappedElem);
          }
        }
      }
      return tree;
    }

var arr = [{
      "id": "1",
      "name": "Navigation menu",
      "parent_id": null,
      "style": "btn btn-default w-100"
    },  {
      "id": "2",
      "name": "Home and garden",
      "parent_id": "1",
      "style": "btn btn-default w-100"
    }, {
      "id": "3",
      "name": "Cookers",
      "parent_id": "2",
      "style": "btn btn-default w-100"
    }, {
      "id": "4",
      "name": "Microwave ovens",
      "parent_id": "2",
      "style": "btn btn-default w-100"
    }, {
      "id": "5",
      "name": "Fridges",
      "parent_id": "2",
      "style": "btn btn-default w-100"
    }, {
      "id": "6",
      "name": "PC peripherials",
      "parent_id": "1",
      "style": "btn btn-default w-100"
    }, {
      "id": "7",
      "name": "Head phones",
      "parent_id": "6",
      "style": "btn btn-default w-100"
    }, {
      "id": "8",
      "name": "Monitors",
      "parent_id": "6",
      "style": "btn btn-default w-100"
    }, {
      "id": "9",
      "name": "Network",
      "parent_id": "6",
      "style": "btn btn-default w-100"
    }, {
      "id": "10",
      "name": "Laptop bags",
      "parent_id": "6",
      "style": "btn btn-default w-100"
    }, {
      "id": "11",
      "name": "Web Cams",
      "parent_id": "6",
      "style": "btn btn-default w-100"
    }, {
      "id": "12",
      "name": "Remote cameras",
      "parent_id": "11",
      "style": "btn btn-default w-100"
    }, {
      "id": "13",
      "name": "Laptops",
      "parent_id": "6",
      "style": "btn btn-default w-100"
    }, {
      "id": "14",
      "name": "15' Laptops",
      "parent_id": "13",
      "style": "btn btn-default w-100"
    }, {
      "id": "15",
      "name": "17' Laptops",
      "parent_id": "13",
      "style": "btn btn-default w-100"
    }]
var tree = unflatten(arr);
console.log(tree);

To support Material UI, in the above code I have added to extra fields. 1.displayName 2.icon

Once we get the nested structure we can use that in the component's template. Rest of the Angular Implementation is giving in stackblitz

Upvotes: 1

aeberhart
aeberhart

Reputation: 889

Here's a possible solution: https://stackblitz.com/edit/dashjoin-ddx71w

The menu is implemented using a regular angular tree (https://v9.material.angular.io/components/tree/overview). The tree uses a nested JSON structure rather than the flat structure with id / parent_id you suggested.

If we adopt and edit this structure directly, JSON schema (https://json-schema.org/) is a good basis for editing the tree model. Check out the "schema" variable in the app component. It is a simple JSON schema representation of the tree model structure:

  schema: Schema = {
    type: "object",
    properties: {
      name: {
        type: "string"
      },
      style: {
        type: "string"
      },
      children: {
        type: "array",
      ...

The schema in the example only supports a nesting level of three. You could also use the $ref mechanism to support arbitrary nesting levels.

Then, I'm using a JSON schema form component which displays a form based on the model and the schema:

<lib-json-schema-form [value]="value" (valueChange)="apply($event)" [schema]="schema"></lib-json-schema-form>

The apply($event) causes the material tree to redraw by first deleting the model and then setting it to the new value emitted from the form component.

The style (should probably be called class) from the form is applied to the tree nodes as follows:

<span [ngClass]="node.style">{{node.name}}</span>

So all in all I think it is a pretty elegant solution with very little code.

Upvotes: 1

Adri&#225;n
Adri&#225;n

Reputation: 1

I need more data about your project but for now... You can create an interface and a object class. When you need it you can add the data to the object anytime.

If you need to do a POST to API end-point you will can send this object for example. The idea, which implements the interface pre-defined, is edit this object anytime.

Upvotes: 0

Related Questions