Angular2: отображение различных компонентов панели инструментов в зависимости от основной навигации.

Это концептуальный вопрос о том, как «правильно» реализовать требуемую функциональность с помощью Angular 2.

В моем приложении есть меню навигации, панель инструментов и область содержимого. Последний содержит основной <router-outlet> и отображает различные представления, такие как список и детали.

Я хочу добиться того, чтобы на панели инструментов отображались различные компоненты в зависимости от компонента / представления, отображаемого в области содержимого. Например, компоненту списка нужен элемент управления поиском на панели инструментов, а компоненту сведений нужна кнопка сохранения.

A) Моя первая попытка заключалась в том, чтобы добавить еще один (названный) <router-outlet> на панель инструментов и отобразить компоненты панели инструментов на основе статических маршрутов. Что в этом плохого:

  1. Отношение статического содержимого к панели инструментов, на мой взгляд, слишком слабо связано.
  2. Отношение видно (и может быть изменено) в URL-адресе.
  3. Выход на панели инструментов сохраняет этот путь, даже если пользователь уходит.

B) Моя вторая попытка состояла в том, чтобы принудительно перейти к компонентам панели инструментов (также используя названный выход панели инструментов) в ngOnInit компонента главного представления, что более тесно связывает их. Что плохо пахнет:

  1. A2
  2. A3, чтобы предотвратить это, я мог «очистить» выход на панели инструментов на ngOnDestroy, но я не понял, как это сделать.

C) Даем маршрутизатору последний шанс, поскольку я обнаружил, что это работает:

const ROUTES: Routes = [
    {path: "buildings", children: [
        {path: "", component: BuildingListComponent, pathMatch: "full", outlet: "primary"},
        {path: "", component: BuildingListToolbarComponent, pathMatch: "full", outlet: "toolbar"},
        {path: ":id", component: BuildingDashboardComponent, outlet: "primary"}
    ]}
];

Идея состоит в том, что маршрутизатор выберет соответствующий путь для каждой розетки. Но (нет, это могло быть так просто), к сожалению, это не работает:

const ROUTES: Routes = [
    {path: "buildings", children: [
        {path: "list", component: BuildingListComponent, pathMatch: "full", outlet: "primary"},
        {path: "list", component: BuildingListToolbarComponent, pathMatch: "full", outlet: "toolbar"},
        {path: ":id", component: BuildingDashboardComponent, outlet: "primary"}
    ]}
];

По-видимому (а может быть, и случайно) он работает только с пустым путем. Почему, ну почему?

D) Совершенно иная стратегия заключалась бы в переработке моей иерархии компонентов, чтобы каждый компонент главного представления содержал соответствующую панель инструментов, и использовать проекция содержимого на несколько слотов. Не пробовал, но боюсь столкнуться с проблемами с несколькими экземплярами панели инструментов.

Иногда это кажется обычным вариантом использования, и мне интересно, как специалисты Angular 2 решат эту проблему. Любые идеи?


person Zeemee    schedule 08.12.2016    source источник
comment
Вы можете просто добавлять компоненты динамически, как показано в stackoverflow.com/questions/36325212/ Вставьте маршрутизатор, чтобы получать уведомления об изменениях маршрута, а затем добавьте соответствующий компонент. Вы также можете добавить data в маршруты для настройки на уровне маршрута, какой компонент должен быть добавлен, как показано в stackoverflow.com/questions/38644314/   -  person Günter Zöchbauer    schedule 08.12.2016
comment
Спасибо, Гюнтер, я попробую. Я также попробовал другой вариант конфигурации маршрутизатора, который выглядел многообещающим (C), но безуспешно.   -  person Zeemee    schedule 08.12.2016
comment
Я отправил ответ на ваше предложение. Еще раз спасибо, что направили меня в правильное русло.   -  person Zeemee    schedule 09.12.2016
comment
Спасибо @Zeemee, это помогает. Намного больше поможет, если вы поместите образец для компонента панели инструментов.   -  person Hussain    schedule 15.10.2017


Ответы (4)


По предложению Гюнтера Цохбауэра (спасибо!), Я закончил добавлением и удалением динамических компонентов на панели инструментов. Требуемый компонент панели инструментов указывается в атрибуте данных маршрута и оценивается центральным компонентом (панелью навигации), который содержит панель инструментов.
Обратите внимание, что компоненту панели инструментов не нужно ничего знать о компонентах панели инструментов (которые определены в модулях feauture).
Надеюсь, это кому-то поможет.

Buildings-routing.module.ts

const ROUTES: Routes = [
    {path: "buildings", children: [
        {
            path: "",
            component: BuildingListComponent,
            pathMatch: "full",
            data: {toolbar: BuildingListToolbarComponent}
        },
        {
            path: ":id",
            component: BuildingDashboardComponent,
            data: {toolbar: BuildingDashboardToolbarComponent}
        }
    ]}
];

@NgModule({
    imports: [
        RouterModule.forChild(ROUTES)
    ],
    exports: [
        RouterModule
    ]
})
export class BuildingsRoutingModule {
}

navbar.component.html

<div class="navbar navbar-default navbar-static-top">
    <div class="container-fluid">
        <form class="navbar-form navbar-right">
            <div #toolbarTarget></div>
        </form>
    </div>
</div>

navbar.component.ts

@Component({
    selector: 'navbar',
    templateUrl: './navbar.component.html',
    styleUrls: ['./navbar.component.scss']
})
export class NavbarComponent implements OnInit, OnDestroy {

    @ViewChild("toolbarTarget", {read: ViewContainerRef})
    toolbarTarget: ViewContainerRef;

    toolbarComponents: ComponentRef<Component>[] = new Array<ComponentRef<Component>>();
    routerEventSubscription: ISubscription;


    constructor(private router: Router,
                private componentFactoryResolver: ComponentFactoryResolver) {
    }

    ngOnInit(): void {
        this.routerEventSubscription = this.router.events.subscribe(
            (event: Event) => {
                if (event instanceof NavigationEnd) {
                    this.updateToolbarContent(this.router.routerState.snapshot.root);
                }
            }
        );
    }

    ngOnDestroy(): void {
        this.routerEventSubscription.unsubscribe();
    }

    private updateToolbarContent(snapshot: ActivatedRouteSnapshot): void {
        this.clearToolbar();
        let toolbar: any = (snapshot.data as {toolbar: Type<Component>}).toolbar;
        if (toolbar instanceof Type) {
            let factory: ComponentFactory<Component> = this.componentFactoryResolver.resolveComponentFactory(toolbar);
            let componentRef: ComponentRef<Component> = this.toolbarTarget.createComponent(factory);
            this.toolbarComponents.push(componentRef);
        }
        for (let childSnapshot of snapshot.children) {
            this.updateToolbarContent(childSnapshot);
        }
    }

    private clearToolbar() {
        this.toolbarTarget.clear();
        for (let toolbarComponent of this.toolbarComponents) {
            toolbarComponent.destroy();
        }
    }
}

Ссылки:
https://vsavkin.com/angular-router-understanding-router-state-7b5b95a12eab
https://engineering-game-dev.com/2016/08/19/angular-2-dynamically-injecting-components
Angular 2 динамические вкладки с выбранными пользователем компонентами
Изменение заголовка страницы с помощью нового маршрутизатора Angular 2

person Zeemee    schedule 09.12.2016
comment
Возможно, вы захотите очистить массив toolbarComponents в конце метода clearToolbar - person kboom; 27.08.2017
comment
В чем причина того, что toolbarComponents является массивом, если вы вызываете this.clearToolbar() для каждого дочернего снимка? Я думал, что массив позволяет вложенным маршрутам добавлять дополнительные элементы панели инструментов. - person fishbone; 29.03.2019

Я не очень люблю и не могу использовать предлагаемые решения. Основная проблема, по-видимому, заключается в том, что кнопки панели инструментов и сам компонент - это два разных компонента. Когда два компонента должны взаимодействовать, начинаются проблемы:

Моя первоначальная проблема заключалась в кнопке обновления: нажатие на кнопку должно перезагружать данные из API (которые затем сохраняются в компоненте). Как кнопка в компоненте A сможет сообщить компоненту B об обновлении?

Мое решение использует только один компонент и сохраняет действия панели инструментов в ng-шаблоне в шаблоне:

<ng-template #toolbaractions>
  <button (click)="refresh()">refresh</button>
</ng-template>

Компонент выглядит так:

export class S3BrowsePageComponent implements AfterViewInit {

    @ViewChild('toolbaractions', { read: TemplateRef })
    public toolbaractions: TemplateRef<any>;

    menu = new BehaviorSubject<TemplateRef<any>>(null);

    ngAfterViewInit(): void {
        this.menu.next(this.toolbaractions)
    }
    ...

Теперь просто нужно отобразить шаблон, когда компонент активен. Я решил добиться этого, используя события activate и deactivate на выходе маршрутизатора включающего Компонента (который предоставляет панель инструментов):

<toolbar>
   <ng-container [ngTemplateOutlet]="menu"></ng-container>
</toolbar>

<sidenav>...</sidenav>

<maincontent>
    <router-outlet (activate)='onActivate($event)'
               (deactivate)='onDeactivate($event)'></router-outlet>
</maincontent>

Функция активации получает экземпляр компонента как событие $, и вы можете проверить, есть ли у компонента какие-либо кнопки панели инструментов:

onActivate($event: any) {
  console.log($event);
  if ($event.hasOwnProperty('menu')) {
    this.menuSubscription = $event['menu']
      .subscribe((tr: TemplateRef<any>) => this.menu = tr)
    }
  }
}

onDeactivate($event: any) {
  this.menu = null;
  this.menuSubscription.unsubscribe();
}
person Sebastian Annies    schedule 04.04.2020
comment
у вас есть, может быть, пример на Stackblitz? - person Art; 17.11.2020

Это может быть немного поздно (и может не дать точного ответа на исходный вопрос), но, поскольку все другие решения, которые я нашел, были довольно сложными, я надеюсь, что это может помочь кому-то в будущем.
Я искал простой способ изменить содержимое моей панели инструментов в зависимости от страницы (или маршрута), на которой я нахожусь.
Я сделал следующее: поместил панель инструментов в отдельный компонент, а в HTML создал другую версию панели инструментов для каждой страницы, но отображать только тот, который соответствует текущему маршруту:

app-toolbar.component.html

<mat-toolbar-row class="toolbar" *ngIf="isHomeView()">
    <span class="pagetitle">Home</span>
    <app-widget-bar></app-widget-bar>
</mat-toolbar-row>

<mat-toolbar-row class="toolbar" *ngIf="isLoginView()">
  <span class="pagetitle">Login</span>
  <app-login-button></app-login-button>
</mat-toolbar-row>

Как видите, я встроил другие компоненты, такие как панель виджетов и кнопка входа в систему, в панель инструментов, поэтому стиль и логика, лежащие в основе этого, могут быть в этих других компонентах и ​​не обязательно должны быть в самом компоненте панели инструментов. В зависимости от ngIf оценивается, какая версия панели инструментов отображается. Функции определены в app-toolbar.component.ts:

app-toolbar.component.ts

import { Component } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-toolbar',
  templateUrl: './app-toolbar.component.html',
  styleUrls: ['./app-toolbar.component.scss']
})
export class ToolbarComponent {

  constructor( private router: Router ) {

  }

  isHomeView() {
    // return true if the current page is home
    return this.router.url.match('^/$');
  }

  isLoginView() {
    // return true if the current page is login
    return this.router.url.match('^/login$');
  }

}

Затем вы можете встроить панель инструментов в другой компонент (приложение, панель инструментов или что-то еще):

<app-toolbar></app-toolbar>

У этого подхода, вероятно, есть некоторые недостатки, но он работает для меня довольно хорошо, и я считаю, что его гораздо проще реализовать и понять, чем другие решения, которые я нашел во время исследования.

person DocRobson    schedule 25.10.2019
comment
Я предполагаю, что проблема с этим подходом заключается в том, что как только вы захотите отобразить контент на панели инструментов в зависимости от состояния, принадлежащего отображаемому компоненту, или вызвать логику, размещенную на отображаемом компоненте, вы столкнетесь с той же проблемой. Вам нужен сервис для настройки панели инструментов. - person Alex Suzuki; 14.07.2020

Мое решение для Angular 6

У меня были некоторые проблемы с ленивой загрузкой модулей, которые в конечном итоге были решены путем добавления моих динамических компонентов в общий модуль, который я мог загрузить в приложение. Это, вероятно, не очень компонентно, но больше я ничего не пытался решить. Кажется, это обычная проблема, которую я видел в SO и Github. Мне пришлось прочитать здесь и здесь, но не нашел 100% решения. Основываясь на ответе Zeeme выше и ссылках на ответы Günter Zöchbauer, я смог реализовать это в проекте.

В любом случае, моя главная проблема заключалась в том, что я не всегда мог получить динамический компонент маршрута, который я хотел загрузить в свой маршрут List-Detail. У меня был такой маршрут, как /events, а затем ребенок, например, /events/:id. Когда я перешел к чему-то вроде /events/1234, набрав localhost:4200/events/1234 прямо в строке URL, динамический компонент не загрузился сразу. Мне пришлось щелкнуть другой элемент списка, чтобы моя панель инструментов загрузилась. Например, мне нужно было бы перейти к localhost:4200/events/4321, после чего загрузилась бы панель инструментов.

Мое исправление приведено ниже: я использовал ngOnInit() для вызова this.updateToolbar с this.route.snapshot сразу, что позволило мне сразу использовать маршрут ActivatedRoute. Поскольку ngOnInit вызывается только один раз, этот первый вызов this.updateToolbar был вызван только один раз, а затем мой Subscription был вызван при последующей навигации. По какой-то причине, которую я не совсем понимаю, .subscribe() не запускался при моей первой навигации, поэтому я затем использовал subscribe для управления последующим изменением дочернего маршрута. Мои Subscription обновляются только один раз, так как я использовал .pipe(take(1)).... Если вы просто используете .subscribe(), обновления будут продолжаться при каждом изменении маршрута.

У меня был просмотр подробностей списка, и мне нужен был список, чтобы получить мой текущий маршрут.

import { ParamMap } from '@angular/router';

import { SEvent, SService } from '../s-service.service';

import { Observable } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';

import { Component, OnInit, OnDestroy, ViewChild, ViewContainerRef, ComponentRef, ComponentFactory, ComponentFactoryResolver, Type  } from '@angular/core';

import { SubscriptionLike } from 'rxjs';
import { Router, NavigationEnd, ActivatedRouteSnapshot, ResolveStart, ChildActivationEnd, ActivatedRoute } from '@angular/router';
import { Event } from '@angular/router';
import { filter, take } from 'rxjs/operators';

@Component({
  selector: 'app-list',
  templateUrl: './s-list.component.html',
  styleUrls: ['./s-list.component.scss']
})
export class SListComponent implements OnInit, OnDestroy {

  isActive = false;

  @ViewChild("toolbarTarget", {read: ViewContainerRef}) toolbarTarget: ViewContainerRef;

  toolbarComponents: ComponentRef<Component>[] = new Array<ComponentRef<Component>>();
  routerEventSubscription: SubscriptionLike;

    seismicEvents: Observable<SEvent[]>;
    selectedId: number;

   constructor(private service: SService,
    private router: Router,
    private route: ActivatedRoute,
    private componentFactoryResolver: ComponentFactoryResolver) { }

    ngOnInit() {
        this.sEvents = this.route.paramMap.pipe(
            switchMap((params: ParamMap) => {
                    this.selectedId = +params.get('id');
                    return this.service.getSEvents();
                })
      );

      // used this on component init to trigger updateToolbarContent 
      this.updateToolbarContent(this.route.snapshot);

      // kept this like above (with minor modification) to trigger subsequent changes
      this.routerEventSubscription = this.router.events.pipe(
        filter(e => e instanceof ChildActivationEnd),
        take(1)
      ).subscribe(
        (event: Event) => {
            if (event instanceof ChildActivationEnd) {
              this.updateToolbarContent(this.route.snapshot);
            }
        }
      );
   }

  ngOnDestroy() {
    this.routerEventSubscription.unsubscribe();
  }

  private clearToolbar() {
    this.toolbarTarget.clear();
    for (let toolbarComponent of this.toolbarComponents) {
        toolbarComponent.destroy();
    }
  }

  private updateToolbarContent(snapshot: ActivatedRouteSnapshot) {

    // some minor modifications here from above for my use case

    this.clearToolbar();
    console.log(snapshot);
    let toolbar: any = (snapshot.data as {toolbar: Type<Component>}).toolbar;
    if (toolbar instanceof Type) {
      let factory: ComponentFactory<Component> = this.componentFactoryResolver.resolveComponentFactory(toolbar);
      let componentRef: ComponentRef<Component> = this.toolbarTarget.createComponent(factory);
      this.toolbarComponents.push(componentRef);
    }
  }
}
person tlm    schedule 27.09.2018