Генерация меню средствами роутов Angular

В одном из проектов был интересный таск: сделать компонент универсальной навигации. Иначе говоря такой, чтобы его можно было просто положить на какой-то роут, и в нем, магическим образом, появились ссылки на дочерние к этому роуту сегменты.


Выходит, компонент будет работать как лейаут. То есть он должен вешаться на роут как компонент, при этом внутри него будет стоять router-outlet для вывода дочерних компонентов.


Маршруты


const routes: Routes = [
  {
    path: '',
    data: {
      title: 'layout'
    },
    component: NavigationLayoutComponent,
    children: [
      {
        path: 'home',
        data: {
          title: 'Home',
        },
        component: HomePageComponent,
      },
      {
        path: 'contacts',
        data: {
          title: 'Contacts',
        },
        component: ContactsPageComponent,
      },
      { path: '**', pathMatch: 'full', redirectTo: 'home' }
    ],
  }
];

Общий принцып такой:


  1. Получаем сегменты из роутера
  2. Собираем "базовый путь"
  3. Рендерим

Итак для начала подпишемся на события роутера


  constructor(
    private _router: Router,
    public activatedRoute: ActivatedRoute,
  ) {
    _router.events.subscribe(e => {
      if (e instanceof NavigationEnd) {
        this._updateBaseUrl(
          this._flattenSegments()
        );
      }
    });
  }

Так как изначально сегменты текушего роута существуют в виде дерева, их нужно преобразовать в одномерный массив, чтобы обновить базовый URL. Берем корневой сегмент и рекурсивно "выпрямляем".


  private _flattenSegments() {

    // По сегментам будем проходить рекурсивно,
    // так как они вложены друг в друга
    function _flatten(segments: ActivatedRouteSnapshot[], res: ActivatedRouteSnapshot[] = []) {
      segments.forEach(s => {
        res.push(s);

        _flatten(s.children, res);
      });

      return res;
    }

    return _flatten([this.activatedRoute.root.snapshot]);
  }

И пересобираем базовый путь


private _updateBaseUrl(segments: ActivatedRouteSnapshot[]) {

    // Изначально ActivatedRoute хранит сегменты вложеными.
    // Ранее мы вызвали "выпрямитель", который преобразовал
    // дерево в плоский массив.
    const segmentIndex = segments
      // Теперь в этом массиве ищем сегмент на который
      // мы повесили наш лейаут
      .findIndex(s => s.component === this.constructor);

    // Собираем базовый URL
    this.baseUrl = '/' + segments
      // сегменты после нашего нас не интересуют
      .splice(0, segmentIndex + 1)
      // убираем сегменты с пустым path *
      .filter(s => s.routeConfig && s.routeConfig.path)
      // получаем path от сегментов
      .map(s => s.routeConfig.path)
      // склеиваем
      .join('/');
  }

* Здесь во время вызова .filter, сначала проверяем существование routeConfig и только потом routeConfig.path, при использовании LazyModules, начальный сегмент (куда прописыватся путь к модулю) не имеет свойсива routeConfig.

Полный код компонента
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router, ActivatedRouteSnapshot, NavigationEnd } from '@angular/router';

@Component({
  selector: 'app-navigation-layout',
  templateUrl: './navigation-layout.component.html',
  styleUrls: ['./navigation-layout.component.css']
})
export class NavigationLayoutComponent implements OnInit {

  // Базовый URL от которо будут строится дочерние
  baseUrl = '';

  constructor(
    private _router: Router,
    public activatedRoute: ActivatedRoute,
  ) {
    _router.events.subscribe(e => {
      if (e instanceof NavigationEnd) {
        this._updateBaseUrl(
          this._flattenSegments()
        );
      }
    });
  }

  ngOnInit() {
  }

  // Возвращает массив сегментов текущего роута
  private _flattenSegments() {

    // По сегментам будем проходить рекурсивно,
    // так как они вложены друг в друга
    function _flatten(segments: ActivatedRouteSnapshot[], res: ActivatedRouteSnapshot[] = []) {
      segments.forEach(s => {
        res.push(s);

        _flatten(s.children, res);
      });

      return res;
    }

    return _flatten([this.activatedRoute.root.snapshot]);
  }

  // Обновляет базовый URL
  private _updateBaseUrl(segments: ActivatedRouteSnapshot[]) {

    // Изначально ActivatedRoute хранит сегменты вложеными.
    // Ранее мы вызвали "выпрямитель", который преобразовал
    // дерево в плоский массив.
    const segmentIndex = segments
      // Теперь в этом массиве ищем сегмент на который
      // мы повесили наш лейаут
      .findIndex(s => s.component === this.constructor);

    // Собираем базовый URL
    this.baseUrl = '/' + segments
      // сегменты после нашего нас не интересуют
      .splice(0, segmentIndex + 1)
      // убираем сегометы с пустым path
      .filter(s => s.routeConfig && s.routeConfig.path)
      // получаем path сегментов
      .map(s => s.routeConfig.path)
      // склеиваем
      .join('/');
  }

}

Осталось вывести ссылки в шаблоне


<div>
  <ng-container *ngFor="let ch of activatedRoute.routeConfig.children">
    <a *ngIf="ch.data" [routerLink]="[baseUrl, ch.path]">
      {{ ch.data.title }}
    </a>
  </ng-container>
</div>

Результат


GitHub

Теги:
angular, navigation component, navigation

Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.