Angular2: Display different "toolbar" components depending on main navigation

angular material
angular 2 navbar
angular 2 dynamic menu example
angular show menu after login
angular 6 dynamic menu example
angular navbar routing
angular dynamic navigation
angular get current route

This is a conceptional question on how to implement the required functionality the "right" way with Angular 2.

My application has a navigation menu, a toolbar, and a content area. The latter contains the primary <router-outlet> and displays the different views like list and details.

What I want to achieve is that the toolbar is displaying different components, depending on the component/view that is rendered in the content area. For example, the list component needs a search control in the toolbar, while the details component needs a save button.

A) My first attempt was to add another (named) <router-outlet> to the toolbar and display the toolbar components based on static routes. What feels wrong about this:

  1. The static content-toolbar relation is coupled too loosely for my taste.
  2. The relation is visible (and changeable) in the URL.
  3. The toolbar outlet keeps this path even if the user navigates away.

B) My second attempt was to navigate imperatively to the toolbar components (also using the named toolbar outlet) in the main view component's ngOnInit, which couples it more tightly. What smells bad:

  1. A2
  2. A3, to prevent this I could "clear" the toolbar outlet on ngOnDestroy, but I haven't found out how.

C) Giving the router a last chance, since I found out that this kind-of works:

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"}
    ]}
];

The idea is that the router would pick the matching path per outlet. But (nooo, it could have been so easy) unfortunately, this doesn't work:

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"}
    ]}
];

It appearently (and maybe accidentially) only works with an empty path. Why, oh why?

D) A complete different strategy would be to rework my component hierarchy, so that every main view component contains a appropriate toolbar, and use multi-slot content projection. Haven't tried this, but I'm afraid running into problems with multiple instances of the toolbar.

As sometimes, this seems to be a common use case, and I'm wondering how Angular 2 experts would solve this. Any ideas?

As suggested by Günter Zöchbauer (thank you!), I ended up adding and removing dynamic components to the toolbar. The desired toolbar component is specified in the data attribute of the route and evaluated by the central component (navbar) that contains the toolbar. Note that the navbar component doesn't need to know anything about the toolbar components (which are defined in the feauture modules). Hope this helps someone.

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

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

Displaying data in views, Angular defines a template language that expands HTML notation with syntax that allows you to define various kinds of data binding and logical directives. Creating an Angular Web App for Multiple Views and Screen Sizes the response of site creators to the different The view role is to render the data currently inside the model to the display

This may be a little late (and may not answer the original question perfectly), but as all other solutions I found were quite complicated, I hope this could help someone in the future. I was looking for an easy way to change the content of my toolbar depending on the page (or route) I'm on. What I did was: put the toolbar in its own component, and in the HTML, create a different version of the toolbar for every page but only display the one that matches the current route:

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>

As you see, I embedded other components like the widget-bar and the login-button into the toolbar, so the styling and logic behind that can be in those other components and does not have to be in the toolbar-component itself. Depending on the ngIf, it is evaluated which version of the toolbar is displayed. The functions are defined in the 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$');
  }

}

You can then embed the toolbar into another component (app, or dashboard or whatever):

<app-toolbar></app-toolbar>

This approach probably has some downsides, but it works quite well for me and I find it much easier to implement and understand than other solutions I found while researching.

How to Deal with Different Form Controls in Angular 2 ― Scotch.io, In this tutorial we will explore the way to bind these few types of controls to our form: Angular 2 - Different form controls (final) scotch. backing The product details component handles the display of each product. The Angular Router displays components based on the browser's URL and your defined routes. This section shows you how to use the Angular Router to combine the products data and route information to display the specific details for each product. Open product-details.component.ts

My solution for Angular 6

I had some issues with lazy loading modules that eventually were solved by adding my dynamic components to a shared module that I could load to the app. This is probably not very component-y, but nothing else I tried solved it. This seems to be a bit of a common issue that I saw over SO and Github. I had to read through here and here but didn't come up with a 100% solution. Based on Zeeme's answer above and the links to Günter Zöchbauer's answers, I was able to implement this in a project.

In any case, my major problem was that I couldn't always get the route's dynamic component that I wanted to load into my List-Detail route. I had a route like /events and then a child like /events/:id. When I navigated to something like /events/1234 by typing localhost:4200/events/1234 directly into the URL bar, the dynamic component wouldn't load right away. I had to click into a different list item in order for my toolbar to load. For example, I would have to navigate to localhost:4200/events/4321 and then the toolbar would load.

My fix is below: I used ngOnInit() to call this.updateToolbar with this.route.snapshot right away which allowed me to use the ActivatedRoute route immediately. Since ngOnInit is only called once, that first call to this.updateToolbar was only called once and then my Subscription was called on subsequent navigation. For whatever reason that I don't completely understand, the .subscribe() wasn't being triggered on my first navigation, so I then used subscribe to manage a subsequent change to a child route. My Subscription updates only once since I used .pipe(take(1)).... If you just use .subscribe() the updates will continue with each route change.

I had a List-Detail view going on and needed the List to get my current route.

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

Angular 2 - Data Display, In our example, we will look at displaying the values of the various properties in our class in an HTML page. Step 1 − Change the code of the app.component.ts file Angular defines a template language that expands HTML notation with syntax that allows you to define various kinds of data binding and logical directives. When the page is rendered, Angular interprets the template syntax to update the HTML according to your logic and current data state.

Angular2 Pocket Primer, When you see a Web page that contains tabs or a set of horizontal links that display different sections of an application, it's quite likely that the Web page is using The Angular 7 app will contain three pages - Home, Login and Register. For now each of the page components will just display a title so we can test navigating between them. An Angular component is a class that contains the logic to control a piece of the UI. A class becomes an Angular component when it is decorated with the @Component decorator.

Web Development with Bootstrap 4 and Angular 2, the display–o classes. There are four different sizes, and that means you can render the «h 12 element with four different styles: <h1 class="display-1">Display​ Angular is a platform for building mobile and desktop web applications. Join the community of millions of developers who build compelling user interfaces with Angular. One framework.

Angular 2 By Example, We wrap the existing Time Remaining h1 and add another h3 tag to show the next display:none is the same as that of ngIf, the mechanism is entirely different​: Multilingual Angular - Using Two (or More) Languages in Your App By Dave Ceddia April 27, 2015 If your Angular app needs to let users have multiple language options, the best way to do it is with a translation library.

Comments
  • You could just add components dynamically like shown in stackoverflow.com/questions/36325212/… Inject the router to get notified about route changes and then add a matching component. You can also add data to routes to configure at route level what component should be added like demonstrated in stackoverflow.com/questions/38644314/…
  • Thank you Günter, I will try this. I also tried another variant of the router configuration, which looked promising (C), but no luck.
  • I posted an answer based on your suggestion. Again, thank you for putting me to the right direction.
  • Thanks @Zeemee that helps. It would help much more if you put a sample for a toolbar component.
  • You might want to clear toolbarComponents array at the end of clearToolbar method
  • What is the reason for toolbarComponents to be an array if you call this.clearToolbar() for each child snapshot? I thought the array allows nested routes to add further toolbar items.