Angular2:根据主导航

时间:2016-12-08 10:00:03

标签: angular angular2-routing angular2-template

这是一个关于如何使用Angular 2以“正确”方式实现所需功能的概念性问题。

我的应用程序有一个导航菜单,一个工具栏和一个内容区域。后者包含主<router-outlet>,并显示不同的视图,如列表和详细信息。

我想要实现的是工具栏显示不同的组件,具体取决于在内容区域中呈现的组件/视图。例如,列表组件需要工具栏中的搜索控件,而详细信息组件需要一个保存按钮。

A)我的第一次尝试是在工具栏中添加另一个(命名的)<router-outlet>,并根据静态路由显示工具栏组件。这有什么不妥之处:

  1. 根据我的口味,静态内容 - 工具栏关系过于松散。
  2. 该关系在网址中可见(且可更改)。
  3. 即使用户导航,工具栏插座也会保留此路径。
  4. B)我的第二次尝试是在主视图组件ngOnInit中以命令方式导航到工具栏组件(也使用指定的工具栏插座),这样可以更紧密地耦合它。什么闻起来很糟糕:

    1. A2
    2. A3,为了防止这种情况,我可以“清除”ngOnDestroy上的工具栏插座,但我还没有找到方法。
    3. 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)完全不同的策略是修改我的组件层次结构,以便每个主视图组件都包含一个合适的工具栏,并使用multi-slot content projection。没试过这个,但我担心多个工具栏实例会遇到问题。

      有时,这似乎是一个常见的用例,我想知道Angular 2专家将如何解决这个问题。有什么想法吗?

4 个答案:

答案 0 :(得分:12)

根据GünterZöchbauer的建议(谢谢!),我最终在工具栏中添加和删除了动态组件。所需的工具栏组件在路径的data属性中指定,并由包含工具栏的中央组件(navbar)进行评估 请注意,导航栏组件无需了解有关工具栏组件的任何信息(在teauture模块中定义) 希望这有助于某人。

<强>建筑物-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 dynamic tabs with user-click chosen components
Changing the page title using the Angular 2 new router

答案 1 :(得分:3)

我真的不喜欢,无法使用建议的解决方案。中心问题似乎是工具栏按钮和实际组件是两个不同的组件。当两个组件必须进行通信时,麻烦就开始了:

我最初的问题是刷新按钮:单击该按钮应从API(然后保存在组件中)重新加载数据。组件A中的按钮如何告诉组件B刷新?

我的解决方案仅使用一个组件,并将工具栏动作保留在模板的ng-template中:

<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>

activate函数将Component实例作为$ event获取,您可以检查Component是否具有任何工具栏按钮:

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();
}

答案 2 :(得分:1)

这可能有点晚了(可能无法完美地回答原始问题),但是由于我发现的所有其他解决方案都相当复杂,我希望这对以后的工作有所帮助。
我一直在寻找一种简单的方法来根据我所在的页面(或路线)更改工具栏的内容。
我所做的是:将工具栏放在其自己的组件中,并在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>

这种方法可能有一些缺点,但是对我来说效果很好,我发现它比我在研究中发现的其他解决方案更容易实现和理解。

答案 3 :(得分:0)

我对Angular 6的解决方案

我在延迟加载模块方面遇到了一些问题,这些问题最终通过将动态组件添加到可以加载到应用程序的共享模块中而得以解决。这可能不是组件化的问题,但是我尝试解决的其他问题。在SO和Github上,这似乎是一个常见问题。我必须通读herehere,但没有提出100%的解决方案。基于上述Zeeme的答案以及与GünterZöchbauer的答案的链接,我得以在一个项目中实现此目标。

无论如何,我的主要问题是我不能总是获得要加载到列表明细路由中的路由动态组件。我有一条像/events这样的路线,然后有一个像/events/:id这样的孩子。通过直接在网址栏中输入/events/1234导航到localhost:4200/events/1234之类的东西时,动态组件不会立即加载。我必须单击另一个列表项才能加载工具栏。例如,我将不得不导航到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);
    }
  }
}