从 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
我有这个 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