Angular 路由菜单集成组件遇到的一些问题?

这个是我自己写项目时遇到的问题
项目的侧边菜单用的是 Material Sidenav + Bootstrap Accordion 混合实现, 所以存在一些小 Bug. 在正常情况下点击 System, 点击 User, 那么 User 菜单是能正常激活的 (携带 .active 这个类)

0.png

但是如果这个时候我按 F5 刷新页面, 菜单就变成这样了

1.png

页面还是 User 页, 但是 System 菜单的展开效果消失了
检查元素时看到, 内部 User 菜单是激活状态, 但是父菜单的 class 没有同步

2.png

因为当时代码是这样写的

layout.component.html

<mat-sidenav-container id="page-sidenav-container">
  <mat-sidenav #sidenav mode="side" opened>
    <rxc-sidenav-logo></rxc-sidenav-logo>
    <rxc-sidenav-menus></rxc-sidenav-menus>
  </mat-sidenav>
  <mat-sidenav-content>
    <main>
      <mat-toolbar>
        <button mat-icon-button (click)="sidenav.toggle()">
          <mat-icon>menu</mat-icon>
        </button>
      </mat-toolbar>
      <rxc-wrapper>
        <div class="rxc-wrapper-content">
          <router-outlet></router-outlet>
        </div>
      </rxc-wrapper>
    </main>
  </mat-sidenav-content>
</mat-sidenav-container>

sidenav-menus.component.html

<ul id="sidenav-menus" class="accordion accordion-flush">
  <li *ngFor="let menu of menus" class="accordion-item">
    <rxc-sidenav-link [menu]="menu"></rxc-sidenav-link>
    <ul *ngIf="menu.children" id="{{ menu.link }}" class="collapse" data-bs-parent="#sidenav-menus">
      <li *ngFor="let child of menu.children">
        <rxc-sidenav-link [menu]="child"></rxc-sidenav-link>
      </li>
    </ul>
  </li>
</ul>

sidenav-link.component.html

<a #element matRipple [matRippleColor]="'rgba(230, 240, 250, .175)'"
  [ngClass]="{'active':menu.link === router.routerState.snapshot.url}" (click)="onMenuClick(menu)">
  <mat-icon [ngClass]="{'ms-3':menu.type === MenuType.SIDENAV_INSIDE}">{{ menu.icon }}</mat-icon>
  <span>{{ menu.name | translate }}</span>
</a>

sidenav-link.component.ts

import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Router } from '@angular/router';

import { MenuType } from 'src/app/base/enumeration/menu-type';
import { SidenavLink } from 'src/app/base/model/sidenav-link.model';

@Component({
  selector: 'rxc-sidenav-link',
  templateUrl: './sidenav-link.component.html',
  styleUrls: ['./sidenav-link.component.scss']
})
export class SidenavLinkComponent implements AfterViewInit {

  @Input() menu!: SidenavLink;
  @ViewChild('element') element!: ElementRef<HTMLParagraphElement>;

  constructor(public router: Router) { }

  public get MenuType() {
    return MenuType;
  }

  ngAfterViewInit(): void {
    if (this.menu.type === MenuType.SIDENAV_COLLAPSE) {
      this.element.nativeElement.setAttribute('href', '#' + this.menu.link);
      this.element.nativeElement.setAttribute('data-bs-toggle', 'collapse');
      this.element.nativeElement.classList.add('accordion-button', 'collapsed');
    }
  }

  onMenuClick(menu: SidenavLink) {
    if (menu.type !== MenuType.SIDENAV_COLLAPSE) {
      this.router.navigate([menu.link]);
    }
  }

}

a 标签中 ngClass 的判断, 如果当前路由和菜单对应的路由相等, 则 .active 激活当前菜单
但是我在外部的页面 sidenav-menus.component.html 没有加任何的判断, 所以导致父级不生效
所以才出现这样的需求场景, 可以简单理解为如下代码

<!-- 如果里面的div携带 .active 类, 则给外面的 div 增加一个 .show 类, 反之则删除 -->
<section>
  <div>
    <div class="active"></div>
  </div>
  <div>
    <div></div>
  </div>
</section>

有没有什么方法能解决这个问题? 或者好一点的思路?
在 Bootstrap 中有没有对应的解决方案?

回复
阅读 716
2 个回答

感谢大佬的回答, 路由菜单使用 routerLinkActive 属性比使用 ngClass 会更好一些, 能省去大量的代码
但是好像实现不了(上述图3中的)需求
我的理解是 routerLinkActive 只能将当前路由标签的 class 激活为 .active, 重点是只能激活当前的单个标签, 不能修改其他相关联的标签, 如果是类似以下需求场景, 没有办法仅仅通过 routerLinkrouterLinkActive 实现

<!-- 如果里面的div携带 .active 类(也就是当前这个a标签路由被激活时), 则给外面的 div 增加一个 .show 类, 反之则删除 -->
<section>
  <div>
    <a routerLink="/a" routerLinkActive="active"></a>
  </div>
  <div>
    <div></div>
  </div>
</section>

在上面的代码中, 我只能控制 a 标签的 class, 而不能修改外面 div-class

解决方案

实现菜单 a 标签激活的同时展开关联的 ul 子菜单, 我这边想到了一种更好的解决方案, 就是自己在封装一个 SidenavService, 这个服务类提供两个功能

  1. 获取当前路由
  2. 判断指定路由是否和当前路由匹配

然后
在组件里通过获取菜单路由和当前路由进行比对, 来进行对菜单 class 属性的控制

sidenav.service.ts

import { Injectable } from '@angular/core';

import { Router } from '@angular/router';
import { MenuType } from 'src/app/base/enumeration/menu-type';
import { SidenavLink } from 'src/app/base/model/sidenav-link.model';

@Injectable({
  providedIn: 'root'
})
export class SidenavService {

  // SidenavLink 对象省略部分属性...
  menus: SidenavLink[] = [
    {
      link: 'home',
      name: 'CM_FRONT_PAGE',
      type: MenuType.SIDENAV_OUTSIDE
    },
    {
      link: 'system',
      name: 'CM_SYSTEM',
      type: MenuType.SIDENAV_COLLAPSE,
      children: [
        {
          link: 'system/user',
          name: 'CM_USER',
          type: MenuType.SIDENAV_INSIDE
        }
      ]
    }
  ];

  url = this.router.routerState.snapshot.url;

  constructor(private router: Router) { }

  urlContain(url: string) {
    return this.url.split('/').includes(url);
  }

}

menus 数据结构中带一个 link 属性, 这个属性可以和当前路由进行比对, 判断当前菜单是否激活, 这样子就不需要在 ts 代码中操作 dom

sidenav-menus.component.ts

export class SidenavMenusComponent {
  constructor(public sidenavService: SidenavService) { }
}

sidenav-menus.component.html
只需要 [ngClass]="{'show':sidenavService.urlContain(menu.link)}" 即可判断 .show 是否生效

<nav id="sidenav-menus" class="accordion accordion-flush">
  <div *ngFor="let menu of sidenavService.menus" class="accordion-item">
    <rxc-sidenav-link [menu]="menu"></rxc-sidenav-link>
    <ul *ngIf="menu.children" id="{{ menu.link }}" class="collapse" data-bs-parent="#sidenav-menus"
      [ngClass]="{'show':sidenavService.urlContain(menu.link)}">
      <li *ngFor="let child of menu.children">
        <rxc-sidenav-link [menu]="child"></rxc-sidenav-link>
      </li>
    </ul>
  </div>
</nav>

同样的, 里面的 a 标签也可以这样判断, 优化后的代码如下
sidenav-link.component.html

<ng-container *ngIf="menu">
  <a #elementRef matRipple [matRippleColor]="matRippleColor"
    [routerLink]="menu.type === MenuType.SIDENAV_COLLAPSE ? null : menu.link" routerLinkActive="active"
    [ngClass]="{'collapsed': !sidenavService.urlContain(menu.link)}">
    <mat-icon [ngClass]="{'ms-3':menu.type === MenuType.SIDENAV_INSIDE}">{{ menu.icon }}</mat-icon>
    <span>{{ menu.name | translate }}</span>
  </a>
</ng-container>

sidenav-link.component.ts

import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';

import { MenuType } from 'src/app/base/enumeration/menu-type';
import { SidenavLink } from 'src/app/base/model/sidenav-link.model';
import { SidenavService } from 'src/app/base/service/sidenav.service';

@Component({
  selector: 'rxc-sidenav-link',
  templateUrl: './sidenav-link.component.html',
  styleUrls: ['./sidenav-link.component.scss']
})
export class SidenavLinkComponent implements AfterViewInit {

  @ViewChild('elementRef') elementRef?: ElementRef<HTMLParagraphElement>;
  @Input() menu?: SidenavLink;
  matRippleColor = 'rgba(230, 240, 250, .175)';

  constructor(public sidenavService: SidenavService) { }

  public get MenuType() {
    return MenuType;
  }

  ngAfterViewInit(): void {
    if (this.elementRef && this.menu && this.menu.type === MenuType.SIDENAV_COLLAPSE) {
      const nativeElement = this.elementRef.nativeElement;
      nativeElement.setAttribute('data-bs-toggle', 'collapse');
      nativeElement.setAttribute('data-bs-target', '#' + this.menu.link);
      nativeElement.classList.add('accordion-button');
    }
  }

}

因为带有 .active 相关联的元素已经和当前路由通过 [ngClass] 绑定了, 所以页面刷新后菜单状态能正常显示了

image.png

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
宣传栏