web-dev-qa-db-ja.com

Angular2:メインナビゲーションに応じて異なる「ツールバー」コンポーネントを表示します

これは、必要な機能をAngular 2で「正しい」方法で実装する方法に関する概念的な質問です。

私のアプリケーションには、ナビゲーションメニュー、ツールバー、およびコンテンツ領域があります。後者にはプライマリ<router-outlet>が含まれ、リストや詳細などのさまざまなビューが表示されます。

私が達成したいのは、コンテンツ領域にレンダリングされるコンポーネント/ビューに応じて、ツールバーにさまざまなコンポーネントが表示されることです。たとえば、リストコンポーネントにはツールバーの検索コントロールが必要ですが、詳細コンポーネントには保存ボタンが必要です。

A)私の最初の試みは、別の(名前付き)<router-outlet>をツールバーに追加し、静的ルートに基づいてツールバーコンポーネントを表示することでした。これについて何が間違っていると感じますか:

  1. 静的なコンテンツとツールバーの関係は、私の好みにはあまりにも緩く結合されています。
  2. 関係はURLに表示されます(そして変更可能です)。
  3. ツールバーのコンセントは、ユーザーが離れてもこのパスを保持します。

B)2番目の試みは、メインビューコンポーネントの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)完全に異なる戦略は、コンポーネント階層を作り直して、すべてのメインビューコンポーネントに適切なツールバーが含まれるようにし、 multi -スロットコンテンツプロジェクション 。これは試していませんが、ツールバーの複数のインスタンスで問題が発生するのではないかと心配しています。

時々、これは一般的な使用例のようですが、Angular 2人の専門家がこれをどのように解決するのか疑問に思っています。何かアイデアはありますか?

14
Zeemee

GünterZöchbauer(ありがとう!)が提案したように、最終的にツールバーに動的コンポーネントを追加および削除しました。目的のツールバーコンポーネントは、ルートのデータ属性で指定され、ツールバーを含む中央コンポーネント(ナビゲーションバー)によって評価されます。
navbarコンポーネントは、ツールバーコンポーネント(機能モジュールで定義されている)について何も知る必要がないことに注意してください。
これが誰かに役立つことを願っています。

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
ユーザーが選択したコンポーネントをクリックする角度のある2つの動的タブ
Angular 2 new router を使用してページタイトルを変更する

14
Zeemee

これは少し遅れるかもしれませんが(元の質問に完全に答えられないかもしれません)、私が見つけた他のすべての解決策は非常に複雑だったので、これが将来誰かに役立つことを願っています。
現在のページ(またはルート)に応じてツールバーのコンテンツを変更する簡単な方法を探していました。
私がしたこと:ツールバーを独自のコンポーネントに配置し、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>

このアプローチにはおそらくいくつかの欠点がありますが、私にとっては非常にうまく機能し、調査中に見つけた他のソリューションよりも実装と理解がはるかに簡単であることがわかりました。

1
DocRobson

私は本当に好きではなく、提案されたソリューションを使用することはできません。中心的な問題は、ツールバーボタンと実際のコンポーネントが2つの異なるコンポーネントであるということのようです。 2つのコンポーネントが通信する必要がある場合、問題が発生します。

私の最初の問題は更新ボタンでした。ボタンをクリックすると、APIからデータが再読み込みされます(その後、コンポーネントに保持されます)。コンポーネントAのボタンはどのようにしてコンポーネントBに更新するように指示できますか?

私のソリューションは1つのコンポーネントのみを使用し、ツールバーのアクションをテンプレートの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)
    }
    ...

コンポーネントがアクティブになったら、テンプレートを表示するだけです。これを実現するには、囲んでいるコンポーネント(ツールバーを提供)のルーターアウトレットでアクティブ化イベントと非アクティブ化イベントを使用します。

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

<sidenav>...</sidenav>

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

アクティブ化関数は、コンポーネントインスタンスを$ eventとして取得し、コンポーネントにツールバーボタンがあるかどうかを確認できます。

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

Angular 6の私の解決策

遅延読み込みモジュールでいくつかの問題が発生しましたが、アプリに読み込むことができる共有モジュールに動的コンポーネントを追加することで最終的に解決されました。これはおそらくあまりコンポーネントではありませんが、他に何も解決しようとしませんでした。これは、SOとGithubで見た、少し一般的な問題のようです。 ここここ を読み通さなければなりませんでしたが上記のZeemeの回答と、GünterZöchbauerの回答へのリンクに基づいて、これをプロジェクトに実装することができました。

いずれにせよ、私の大きな問題は、List-Detailルートにロードしたいルートの動的コンポーネントを常に取得できるとは限らないことでした。 _/events_のようなルートがあり、次に_/events/:id_のような子がありました。 URLバーに直接_/events/1234_と入力して_localhost:4200/events/1234_のようなものに移動すると、動的コンポーネントがすぐに読み込まれませんでした。ツールバーをロードするには、別のリストアイテムをクリックする必要がありました。たとえば、_localhost:4200/events/4321_に移動すると、ツールバーが読み込まれます。

私の修正は以下のとおりです。ngOnInit()を使用して_this.updateToolbar_を_this.route.snapshot_ですぐに呼び出し、ActivatedRouteルートをすぐに使用できるようにしました。 ngOnInitは1回しか呼び出されないため、_this.updateToolbar_への最初の呼び出しは1回だけ呼び出され、その後のナビゲーションでSubscriptionが呼び出されました。完全には理解できない理由で、最初のナビゲーションで.subscribe()がトリガーされなかったため、subscribeを使用して子ルートへのその後の変更を管理しました。 .pipe(take(1))...を使用して以来、Subscriptionは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);
    }
  }
}
_
0
tlm