从 JSON 结构生成导航菜单项

Generate navigation menu items from JSON structure

我有这个 JSON 示例,我想在其中存储 Angular 项目的导航菜单:

{
  "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"
    }]
  }]
}

想法是在需要时编辑 JSON 并根据此数据生成导航菜单。 如何实现?

我需要有关您的项目的更多数据,但现在...您可以创建一个界面和一个对象 class。当您需要时,您可以随时将数据添加到对象中。

如果你需要做一个POST到APIend-point你可以发送这个对象。实现接口 pre-defined 的想法是随时编辑此对象。

这是一个可能的解决方案: https://stackblitz.com/edit/dashjoin-ddx71w

菜单是使用常规 angular 树 (https://v9.material.angular.io/components/tree/overview) 实现的。 该树使用嵌套的 JSON 结构,而不是您建议的带有 id / parent_id 的平面结构。

如果我们直接采用和编辑这个结构,JSON schema (https://json-schema.org/) 是编辑树模型的一个很好的基础。检查应用程序组件中的“schema”变量。它是树模型结构的简单 JSON 模式表示:

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

示例中的架构仅支持三层嵌套。您还可以使用 $ref 机制来支持任意嵌套级别。

然后,我使用 JSON 模式表单组件,它显示基于模型和模式的表单:

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

apply($event) 通过首先删除模型然后将其设置为从表单组件发出的新值来重绘 material 树。

表单中的样式(应该称为class)如下应用于树节点:

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

所以总而言之,我认为这是一个非常优雅的解决方案,代码很少。

所以首先你需要将平面列表转换为 tree-like 结构。

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

为了支持Material UI,在上面的代码中我添加了额外的字段。 1.displayName 2.icon

一旦我们获得嵌套结构,我们就可以在组件的模板中使用它。 Angular 实现的其余部分正在 stackblitz

我在我的新项目中这样做,这里是部分代码供您参考。 我们在这里用作 JSON 文件来存储我们的菜单,在应用程序启动后使用加载程序加载它。然后在我们的 html 加载的菜单中使用。

我没有使用您的代码或 json 参考,因此您可以采用您的想法并以自己的方式实现它。


sidenav-menu.json

将此文件放在 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

将此文件放在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;
  }
}

将 MenuLoader 服务添加到构造函数并从中获取菜单

app.component.ts

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

  ngOnInit(): void {
   }

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

HTML 部分

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 = []; 构造函数(

    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

动画代码:

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)')),
    ]),
  ]

我在这里放了一小部分代码,这样你就可以用你自己的方式实现它,这是你如何实现它的一个想法,我也没有添加任何演示。如果您在理解代码时遇到任何问题,请进入评论并解决它。 快乐编码...:D