While working on the upcoming layouts feature of Radzen I needed to target multiple Angular router outlets from the same component. Right now one can have as many named router outlets as needed e.g. <router-outlet name="navigation"></router-outlet> <router-outlet name="primary"></router-outlet> However they need separate Angular components - one cannot have a HomeComponent and say “this goes in the navigation outlet and that goes in the primary outlet”. Instead one needs HomeNavigationComponent and HomeMainComponent.

In this post I will show how I implemented that by creating two Angular components and a service.

Solution

My career in web development started with ASP.NET which has the concept of master pages: a page that defines the layout of the web application and has placeholders for the content. The actual pages defined components that live in those placeholders.

That was my inspiration and I decided to implement the same idea with Angular 5. The end result should look like this:

  • master.component.html

    <header>
      <my-placeholder name="navigation"></my-placeholder>
    </header>
    <section>
      <my-placeholder name="main"></my-placeholder>
    </section>
    <router-outlet name="master"></router-outlet>
    
  • home.component.html

    <my-content placeholder="navigation">
      Navigation content of HomeComponent
    </my-content>
    <my-content placeholder="main">
      Main content of HomeComponent
    </my-content>
    
  • about.component.html

    <my-content placeholder="navigation">
      Navigation content of AboutComponent
    </my-content>
    <my-content placeholder="main">
      Main content of AboutComponent
    </my-content>
    

Implementation

Let’s dive into the code.

ContentService

First we need a helper Angular service which will facilitate the communication between placeholder and content components.

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';

export interface ContentDescriptor {
  placeholder: string;
  elementRef: ElementRef;
}

@Injectable()
export class ContentService {
  private contentInit$ = new Subject<ContentDescriptor>();

  contentInit(): Observable<ContentDescriptor> {
    return this.contentInit$.asObservable();
  }

  registerContent(content: ContentDescriptor) {
    this.contentInit$.next(content);
  }
}

Some notes.

  1. We use ContentDescriptor as a helper to make Subject and Observable strongly typed.
  2. The contentInit method is used by the placeholder component to get notified of new content components. We do not expose the underlying contentInit$ in order not to get unwanted next() calls. IT IS BEST PRACTICE!
  3. The registerContent method is used by the content components.

ContentComponent

@Component({
  selector: 'my-content',
  template: '<ng-content></ng-content>'
})
export class ContentComponent {
  @Input() placeholder: string;

  constructor(
    private elementRef: ElementRef,
    private contentService: ContentService
  ) { }

  ngOnInit() {
    this.contentService.registerContent({
      placeholder: this.placeholder,
      elementRef: this.elementRef
    });
  }
}

It’s only job is to report for duty. During ngOnInit it invokes the registerContent method and provides the placeholder it wants to go in and the ElementRef that represents its own DOM tree.

PlaceholderComponent

@Component({
  selector: 'my-placeholder',
  template: '<ng-content></ng-content>'
})
export class PlaceholderComponent {
  @Input() name: string;

  private subscription: Subscription;

  constructor(
    private containerRef: ViewContainerRef,
    private contentService: ContentService
  ) {
    this.subscription = contentService.contentInit().subscribe((content: ContentDescriptor) => {
      if (content.placeholder == this.name) {
        this.containerRef.clear();
        this.containerRef.element.nativeElement.appendChild(content.elementRef.nativeElement);
      }
    });
  }

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

The placeholder listens to notifications and moves content components in its DOM tree.

Live demo

Here is a live demo that shows the end result.

How to use it in you own Angular application

  1. Grab the master.module.ts file from that StackBlitz project.
  2. Import it in your app

    import { MasterModule } from './master.module';
    
    @NgModule({
      imports: [BrowserModule, MasterModule, RouterModule.forRoot(appRoutes)],
      declarations: [AppComponent, MasterComponent, AboutComponent, HomeComponent,],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
  3. Create a new component called Master. Copy everything from app.component.html in it. Add <my-placeholder> components here and there. Add a named router outlet called “master” <router-outlet name="master"></router-outlet>.

    <header>
      <a [routerLink]="['/home']">Home</a>
      <a [routerLink]="['/about']">About</a>
      <my-placeholder name="navigation"></my-placeholder>
    </header>
    <section>
      <my-placeholder name="main"></my-placeholder>
    </section>
    <!-- DO NOT FORGET THIS OR IT WON'T WORK -->
    <router-outlet name="master"></router-outlet>
    
  4. Clear your app.component.html of all content and put a <router-outlet></router-outlet>. This is where the Master component will instantiate.
  5. In your other components use <my-content> to specify which content goes where.
  6. Update your routes like this:
  • OLD

    const appRoutes: Routes = [
      {
        path: 'home',
        component: HomeComponent
      }
    ]
    
  • NEW

    const appRoutes: Routes = [
      {
        path: 'home',
        component: MasterComponent,
        children: [
          {
            outlet: 'master',
            path: '',
            component: HomeComponent
          }
        ]
      }
    ]
    

That’s all! If you liked this post, do not forget to share!