在Angular应用程序中,我想从数据库(.NEt Core项目)动态加载菜单。为此,我创建了一个API服务来获取json格式的数据。为了从前端请求此json格式,我在workout.service.ts
文件中使用了一项服务(我在有可观察性和无可观察性的情况下进行了尝试,但获得了相同的结果)。
要加载菜单,我使用了menu.service.ts
文件,其中的文件getVerticalMenuItems()
用于加载在初始化期间不起作用的垂直菜单;
在这里,我使用方法resultmenu.push
将数据转换为以下格式:
export const verticalMenuItems = [
new Menu(1, 'Dashboard', '/', null, 'dashboard', null, false, 0),
new Menu(100, 'Action', '/actions', null, 'extension', null, false, 0)]
垂直菜单上传,我们使用vertical-menu.component.ts
(我想这里出现的问题总是显示Array [0] = null值)
export class Menu {
constructor(public id: number,
public title: string,
public routerLink: string,
public href: string,
public icon: string,
public target: string,
public hasSubMenu: boolean,
public parentId: number) { }
}
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import 'rxjs/Rx';
import { User } from './models/user.model';
import { Observable } from 'rxjs/Rx';
import { Menu } from './theme/components/menu/menu.model';
@Injectable()
export class WorkoutService {
private headers: HttpHeaders;
private menuUrl: string = 'https://localhost:44355/api/Menus';
constructor(private http: HttpClient) {
this.headers= new HttpHeaders({'Content-Type':'application/json; charset=utf-8});}
// Get Menus
public getMenus() {return this.http.get(this.menuUrl, { headers: this.headers }).map((response: Response) => {
return response;
});
}
}
@Injectable()
export class MenuService {
my_menu: Array<Menu>;
constructor(private location:Location,
private router: Router,
private workoutService: WorkoutService) { }
// for vertical Menu items loading
public getVerticalMenuItems(): Array<Menu> {
const resultMenu: Array<Menu>=[];
this.workoutService.getMenus()
.subscribe(
(data:any) => {
data.forEach(i => {
resultMenu.push(new Menu(i.id, i.title, i.routerLink, i.href, i.icon, i.target, i.hasSubMenu, i.parentId))
})
console.log(resultMenu);
}
);
console.log(resultMenu);
console.log(verticalMenuItems);
return resultMenu;
//return verticalMenuItems; -- NOTE: old working code taken from Gradus theme templates ;(working code)
}
// working code
public getHorizontalMenuItems(): Array<Menu> {
return horizontalMenuItems;
}
public expandActiveSubMenu(menu: Observable<Menu[]>){
let url = this.location.path();
let routerLink = url; // url.substring(1, url.length);
let activeMenuItem = menu.map(items=>items.filter(item => item.routerLink === routerLink));
if(activeMenuItem[0]){
let menuItem = activeMenuItem[0];
while (menuItem.parentId != 0){
let parentMenuItem = menu.map(items=>items.filter(item => item.id == menuItem.parentId)[0]);
menuItem = parentMenuItem;
this.toggleMenuItem(menuItem.id);
}
}
}
public toggleMenuItem(menuId){
let menuItem = document.getElementById('menu-item-'+menuId);
let subMenu = document.getElementById('sub-menu-'+menuId);
if(subMenu){
if(subMenu.classList.contains('show')){
subMenu.classList.remove('show');
menuItem.classList.remove('expanded');
}
else{
subMenu.classList.add('show');
menuItem.classList.add('expanded');
}
}
}
public closeOtherSubMenus(menu: Array<Menu>, menuId) {
debugger;
let currentMenuItem = menu.filter(item => item.id == menuId)[0];
if(currentMenuItem.parentId == 0 && !currentMenuItem.target){
menu.forEach(item => {
if(item.id != menuId){
let subMenu = document.getElementById('sub-menu-'+item.id);
let menuItem = document.getElementById('menu-item-'+item.id);
if(subMenu){
if(subMenu.classList.contains('show')){
subMenu.classList.remove('show');
menuItem.classList.remove('expanded');
}
}
}
});
}
export class VerticalMenuComponent implements OnInit {
@Input() resultMenu: Array<Menu> = [];
/***************************************
* MOVED THE NEW INPUTS TO HERE
****************************************/
@Input()
set menuItems(items: Menu[]) {
this._menuItemsLoaded$.next(items);
}
_menuItemsLoaded$: ReplaySubject<Menu[]> = new ReplaySubject<Menu[]>(1);
// We must save the received items and also emit it
@Input()
set menuParentId(items: number) {
this._menuParentIdLoaded$.next(items);
}
_menuParentIdLoaded$: ReplaySubject<number> =
new ReplaySubject<number>(1);
/***************************************
* END OF NEW INPUTS
****************************************/
@Output() onClickMenuItem: EventEmitter<any> = new EventEmitter<any>();
parentMenu: Array<any>;
public settings: Settings;
constructor(public appSettings: AppSettings, public menuService: MenuService, public router: Router) {
this.settings = this.appSettings.settings;
}
ngOnInit() {
/***************************************
* FIXED THE FILTERS INSIDE THE PIPES
****************************************/
combineLatest(
this._menuItemsLoaded$.pipe(
filter(Boolean),
filter((i) => !!i.length),
debounceTime(300)),
this._menuParentIdLoaded$.pipe(
// for _menuParentId (exclusively) we cannot use
// filter(Boolean) here, or the 0 values will be blocked
filter((i) => i !== null && i !== undefined),
debounceTime(300)),
).subscribe(([menuItems, parentId]) =>
this.parentMenu = menuItems.filter(item => item.parentId == parentId)
);
}
// as we're using some subjects, we must finalize
// them in case this component is eventually destroyed
ngOnDestroy() {
if (this._menuItemsLoaded$ && !this._menuItemsLoaded$.closed) {
this._menuItemsLoaded$.complete();
}
if (this._menuParentIdLoaded$ && !this._menuParentIdLoaded$.closed) {
this._menuParentIdLoaded$.complete();
}
}
//ngOnChanges() {
// if (this.menuItems == null) {
// this.parentMenu = null;
// }
// this.parentMenu = this.menuItems.filter(item => item.parentId == this.menuParentId);
// console.log(this.parentMenu);
//}
ngAfterViewInit() {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
if (this.settings.fixedHeader) {
let mainContent = document.getElementById('main-content');
if (mainContent) {
mainContent.scrollTop = 0;
}
}
else {
document.getElementsByClassName('mat-drawer-content')[0].scrollTop = 0;
}
}
});
}
onClick(menuId) {
/*************************************
* Import take operator from 'rxjs/operators'
* As part of the changes, the values that we need are
* all on `_menuItemsLoaded$` ReplaySubject.
*************************************/
combineLatest(this._menuItemsLoaded$).pipe(take(1))
.subscribe(([items]) => {
this.menuService.toggleMenuItem(menuId);
this.menuService.closeOtherSubMenus(items, menuId);
this.onClickMenuItem.emit(menuId);
}
}
}
<div *ngFor="let menu of parentMenu" class="menu-item">
<a *ngIf="menu.routerLink && menu.hasSubMenu" mat-button
fxLayout="row" [fxLayoutAlign]="(settings.menuType=='default') ? 'start center' : 'center center'"
[routerLink]="[menu.routerLink]" routerLinkActive="active-link" [routerLinkActiveOptions]="{exact:true}"
[matTooltip]="menu.title" matTooltipPosition="after" [matTooltipDisabled]="(settings.menuType=='mini') ? 'false' : 'true'"
(click)="onClick(menu.id)" [id]="'menu-item-'+menu.id">
<mat-icon class="menu-icon">{{menu.icon}}</mat-icon>
<span class="menu-title">{{menu.title}}</span> <!-- !menu.hasSubMenu-->
</a>
<a *ngIf="menu.href && !menu.subMenu && !menu.hasSubMenu" mat-button
fxLayout="row" [fxLayoutAlign]="(settings.menuType=='default') ? 'start center' : 'center center'"
[attr.href]="menu.href || ''" [attr.target]="menu.target || ''"
[matTooltip]="menu.title" matTooltipPosition="after" [matTooltipDisabled]="(settings.menuType=='mini') ? 'false' : 'true'"
(click)="onClick(menu.id)" [id]="'menu-item-'+menu.id">
<mat-icon class="menu-icon">{{menu.icon}}</mat-icon>
<span class="menu-title">{{menu.title}}</span>
</a>
<a *ngIf="menu.hasSubMenu" mat-button
fxLayout="row" [fxLayoutAlign]="(settings.menuType=='default') ? 'start center' : 'center center'"
[matTooltip]="menu.title" matTooltipPosition="after" [matTooltipDisabled]="(settings.menuType=='mini') ? 'false' : 'true'"
(click)="onClick(menu.id)" [id]="'menu-item-'+menu.id">
<mat-icon class="menu-icon">{{menu.icon}}</mat-icon>
<span class="menu-title">{{menu.title}}</span>
<mat-icon class="menu-expand-icon transition-2">arrow_drop_down</mat-icon>
</a>
<div *ngIf="menu.hasSubMenu" class="sub-menu" [id]="'sub-menu-'+menu.id">
<app-vertical-menu [menuItems]="_menuItemsLoaded$ | async" [menuParentId]="menu.id" (onClickMenuItem)="updatePS($event)"></app-vertical-menu>
<!-- <app-vertical-menu (onClickMenuItem)="updatePS($event)"
[menuItems]="{menuItems: _menuItemsLoaded$ | async, parentId: 0}">
</app-vertical-menu> -->
</div>
</div>
<div fxLayout="column" fxLayoutAlign="center center" class="user-block transition-2" [class.show]="settings.sidenavUserBlock">
<div [fxLayout]="(settings.menuType != 'default') ? 'column' : 'row'"
[fxLayoutAlign]="(settings.menuType != 'default') ? 'center center' : 'space-around center'" class="user-info-wrapper">
<img [src]="userImage" alt="user-image">
<div class="user-info">
<p class="name">Emilio Verdines</p>
<p *ngIf="settings.menuType == 'default'" class="position">Web developer <br> <small class="muted-text">Member since May. 2016</small></p>
</div>
</div>
<div *ngIf="settings.menuType != 'mini'" fxLayout="row" fxLayoutAlign="space-around center" class="w-100 muted-text">
<button mat-icon-button><mat-icon>person_outline</mat-icon></button>
<a mat-icon-button routerLink="/mailbox">
<mat-icon>mail_outline</mat-icon>
</a>
<a mat-icon-button routerLink="/login">
<mat-icon>power_settings_new</mat-icon>
</a>
</div>
</div>
<perfect-scrollbar #sidenavPS class="sidenav-menu-outer" [class.user-block-show]="settings.sidenavUserBlock">
<span *ngIf="!menuItems">loading....</span>
<app-vertical-menu [menuItems]="menuItems" [menuParentId]="0" (onClickMenuItem)="updatePS($event)"></app-vertical-menu>
</perfect-scrollbar>
@Component({
selector: 'app-sidenav',
templateUrl: './sidenav.component.html',
styleUrls: ['./sidenav.component.scss'],
encapsulation: ViewEncapsulation.None,
providers: [MenuService]
})
export class SidenavComponent implements OnInit {
@ViewChild('sidenavPS') sidenavPS: PerfectScrollbarComponent;
public userImage = '../assets/img/users/user.jpg';
public menuItems: Array<any>;
public settings: Settings;
constructor(public appSettings: AppSettings, public menuService: MenuService) {
this.settings = this.appSettings.settings;
}
ngOnInit() {
debugger;
this.menuItems = this.menuService.getVerticalMenuItems();
}
ngOnChange() {
debugger;
this.menuItems = this.menuService.getVerticalMenuItems();
}
public closeSubMenus() {
const menu = document.querySelector('.sidenav-menu-outer');
if (menu) {
for (let i = 0; i < menu.children[0].children.length; i++) {
const child = menu.children[0].children[i];
if (child) {
if (child.children[0].classList.contains('expanded')) {
child.children[0].classList.remove('expanded');
child.children[1].classList.remove('show');
}
}
}
}
}
public updatePS(e) {
this.sidenavPS.directiveRef.update();
}
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators} from '@angular/forms';
import { MatSnackBar } from '@angular/material';
import 'rxjs/add/operator/debounceTime';
import { Menu } from '../../theme/components/menu/menu.model';
import { MenuService } from '../../theme/components/menu/menu.service';
import { DynamicMenuService } from './dynamic-menu.service';
import { AppSettings } from '../../app.settings';
import { Settings } from '../../app.settings.model';
import { VerticalMenuComponent } from '../../theme/components/menu/vertical-menu/vertical-menu.component';
import { listTransition } from '../../theme/utils/app-animation';
import { combineLatest, Observable } from 'rxjs';
@Component({
selector: 'app-dynamic-menu',
templateUrl: './dynamic-menu.component.html',
providers: [ DynamicMenuService, MenuService ],
animations: [ listTransition ],
host: {
'[@listTransition]': ''
}
})
export class DynamicMenuComponent implements OnInit {
settings: Settings;
menuItems:Array<Menu>;
_menuItems$: Observable<Menu[]>;
public icons = ['home','person', 'card_travel', 'delete', 'event', 'favorite', 'help' ]
public form:FormGroup;
constructor(public appSettings:AppSettings,
public formBuilder: FormBuilder,
public snackBar: MatSnackBar,
private menuService:MenuService,
private dynamicMenuService:DynamicMenuService) {
this.settings = this.appSettings.settings;
this._menuItems$ = this.menuService.getVerticalMenuItems()
.pipe(tap((menuItems: Menu) => this.menuItems = menuItems));
}
ngOnInit() {
this.form = this.formBuilder.group({
'title': ['', Validators.compose([Validators.required, Validators.minLength(3)])],
'icon': null,
'routerLink': ['', Validators.required],
'href': ['', Validators.required],
'target': null,
'hasSubMenu': false,
'parentId': 0
});
}
ngAfterViewInit() {
this.form.valueChanges.debounceTime(500).subscribe(menu => {
if(menu.routerLink && menu.routerLink != ''){
this.form.controls['href'].setValue(null);
this.form.controls['href'].disable();
this.form.controls['href'].clearValidators();
this.form.controls['target'].setValue(null);
this.form.controls['target'].disable();
}
else{
this.form.controls['href'].enable();
this.form.controls['href'].setValidators([Validators.required]);
this.form.controls['target'].enable();
}
this.form.controls['href'].updateValueAndValidity();
if(menu.href && menu.href != ''){
this.form.controls['routerLink'].setValue(null);
this.form.controls['routerLink'].disable();
this.form.controls['routerLink'].clearValidators();
this.form.controls['hasSubMenu'].setValue(false);
this.form.controls['hasSubMenu'].disable();
}
else{
this.form.controls['routerLink'].enable();
this.form.controls['routerLink'].setValidators([Validators.required]);
this.form.controls['hasSubMenu'].enable();
}
this.form.controls['routerLink'].updateValueAndValidity();
})
}
onSubmit(menu:Menu):void {
if (this.form.valid) {
this.dynamicMenuService.addNewMenuItem(VerticalMenuComponent, this.menuItems, menu);
this.snackBar.open('New menu item added successfully!', null, {
duration: 2000,
});
this.form.reset({
hasSubMenu:false,
parentId:0
});
}
}
}
import { Injectable, Injector, ComponentFactoryResolver, ApplicationRef, EmbeddedViewRef } from '@angular/core';
import { VerticalMenuComponent } from '../../theme/components/menu/vertical-menu/vertical-menu.component';
import { Menu } from '../../theme/components/menu/menu.model';
@Injectable()
export class DynamicMenuService {
constructor(private componentFactoryResolver: ComponentFactoryResolver,
private applicationRef: ApplicationRef,
private injector: Injector) { }
addNewMenuItem(component: any, menuItems:Array<Menu>, menuItem) {
const lastId = menuItems[menuItems.length-1].id;
const newMenuItem = new Menu(lastId+1, menuItem['title'], menuItem['routerLink'], menuItem['href'], menuItem['icon'], menuItem['target'], menuItem['hasSubMenu'], parseInt(menuItem['parentId']));
menuItems.push(newMenuItem);
let item = menuItems.filter(item=>item.id == newMenuItem.parentId)[0];
if(item) item.hasSubMenu = true;
const componentRef = this.componentFactoryResolver
.resolveComponentFactory(component)
.create(this.injector);
this.applicationRef.attachView(componentRef.hostView);
let instance = <VerticalMenuComponent>componentRef.instance;
instance.menuItems = menuItems;
instance.menuParentId = 0;
const elem = (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
const sidenav = document.getElementById('sidenav-menu-outer');
sidenav.replaceChild(elem, sidenav.children[0]);
}
}
检查图像以获取更多信息,该图像显示由于订阅延迟enter image description here而导致的数组为空 复制错误,显示在下面的链接中 enter image description here 点击绑定的新错误 enter image description here
答案 0 :(得分:2)
好吧,我将在代码中进行一些更改。
<vertical-menu>
代替:
<app-vertical-menu [menuItems]="menuItems" [menuParentId]="0" (onClickMenuItem)="updatePS($event)"></app-vertical-menu>
我愿意:
<app-vertical-menu [menuItems]="_menuItems$ | async" [menuParentId]="0" (onClickMenuItem)="updatePS($event)"></app-vertical-menu>
从现在开始,将menuItems
更改为_menuItems$
只是表面上的,这表明它是可观察的。真正的变化是async
管道。
menuService
的打字稿代码中_menuItems$: Observable<Menu>;
ngOnInit() {
this._menuItems$ = this.menuService.getVerticalMenuItems();
}
import {filter, map} from 'rxjs/operators';
...
getVerticalMenuItems(): Observable<Menu[]> {
return this.workoutService.getMenus().pipe(
// this will avoid null/undefined values
filter(Boolean),
// this will avoid empty arrays comming from getMenus()
// just comment it if empty arrays are allowed here
filter(data => !!data.length),
// here we're building the Menu array
map((data:any) => data.map(i => new Menu(i.id, i.title,
i.routerLink, i.href,
i.icon, i.target,
i.hasSubMenu, i.parentId))
); // here we're closing the pipe
}
通过这样的操作,我们将委派async
管道来订阅和取消订阅菜单服务。
我不确定getMenu()
在这里做什么。是否要从服务器获取菜单数据?如果是这样,那么在应用程序启动时将菜单数据缓存在服务中,而不是每次都去服务器时,是否更有效?只是一个猜测,因为我不完全了解您如何构建应用程序中的内容。
由于我错过了另一个错误,因此我在其他组件中添加了更多更改。
在该组件中,您具有:
@Input('menuItems') menuItems;
@Input('menuParentId') menuParentId;
...
ngOnInit() {
this.parentMenu = this.menuItems.filter(item => item.parentId == this.menuParentId); -- not working??
}
要成功初始化parentMenu
,必须确保同时加载menuItems
和menuParentId
。在OnInit
内像这样做一样是不安全的。我要提出的方法是非常可靠的IMO(我在我的某些组件中使用了它)并使用了功能强大的RxJs API。
更新2(filter(Boolean)
不能用于menuParentId):
// We must save the received items and also emit it
@Input()
set menuItems(items: Menu[]) {
this._menuItemsLoaded$.next(items);
}
private _menuItemsLoaded$: ReplaySubject<Menu[]> = new ReplaySubject<Menu[]>(1);
// We must save the received items and also emit it
@Input()
set menuParentId(items: number) {
this._menuParentIdLoaded$.next(items);
}
private _menuParentIdLoaded$: ReplaySubject<number> =
new ReplaySubject<number>(1);
...
ngOnInit() {
combineLatest(
this._menuItemsLoaded$.pipe(
filter(Boolean),
filter((_) => !!_.length),
debounceTime(300)),
this._menuParentIdLoaded$.pipe(
// for _menuParentId (exclusively) we cannot use
// filter(Boolean) here, or the 0 values will be blocked
filter((_) => _ !== null && _ !== undefined),
debounceTime(300)),
).subscribe(([menuItems, parentId]) =>
this.parentMenu = menuItems.filter(item => item.parentId == parentId)
);
}
// as we're using some subjects, we must finalize
// them in case this component is eventually destroyed
ngOnDestroy() {
if(this._menuItemsLoaded$ && !this._menuItemsLoaded$.closed) {
this._menuItemsLoaded$.complete();
}
if(this._menuParentIdLoaded$ && !this._menuParentIdLoaded$.closed) {
this._menuParentIdLoaded$.complete();
}
}
如果您认为上述方法令人不知所措(我同意,但是我认为习惯RxJ也很有帮助),并且您可以完全控制VerticalMenuComponent
及其{ {1}},您可以考虑创建菜单组件数据接口并仅传递一个参数:
@Input()
然后您应该更改一下模板:
export interface VerticalMenuComponentData {
menuItems: Menu[];
parentId: number;
}
...
// We must save the received items and also emit it
@Input()
get verticalMenuComponentData: VerticalMenuComponentData { return this._verticalMenuComponentData; }
set verticalMenuComponentData(data: VerticalMenuComponentData) {
this._verticalMenuComponentData = data;
// You don't need to call anything inside ngOnInit
this.parentMenu = data && data.menuItems && data.parentId
? data.menuItems.filter(item => item.parentId == data.parentId)
: [];
}
private _verticalMenuComponentData: VerticalMenuComponentData;
答案 1 :(得分:1)
在调用subscribe()时,您的代码不会停止等待服务器响应。相反,它将继续执行getVerticalMenuItems()
的其余部分。 http请求完成后,将执行您作为subscribe()
的参数放置的函数。但这可能需要一两秒钟。因此,您的console.log(--END OF SUBSCRIBE---)
实际上是不正确的,不是subscribe()
的结尾,subscribe()
尚未在执行时完成。
您现在可以做什么: 将resultMenu设置为组件变量,然后将数据保存在其中。然后在您的html中,您可以正常使用该变量,例如:
<ul *ngFor="let menu of resultMenu">
<li>
//other code
{{menu.title}}
</li>
</ul>
Angular将检测到您的this.resultMenu
已更改,并将自动更新视图(除非更改检测设置为onPush,否则您必须这样做)。您会看到服务器响应后菜单就会出现。
如果必须在显示接收到的数据之前对其进行处理,则必须实现ngOnChanges并在那里进行处理。每当组件的输入发生更改时,即会调用。
Here is a Stackblitz demo,查看控制台日志