Checkout Radzen Demos and download latest Radzen!
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.
- We use
ContentDescriptor
as a helper to makeSubject
andObservable
strongly typed. - The
contentInit
method is used by the placeholder component to get notified of new content components. We do not expose the underlyingcontentInit$
in order not to get unwantednext()
calls. IT IS BEST PRACTICE! - 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
- Grab the
master.module.ts
file from that StackBlitz project. -
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 { }
-
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>
- Clear your
app.component.html
of all content and put a<router-outlet></router-outlet>
. This is where the Master component will instantiate. - In your other components use
<my-content>
to specify which content goes where. - 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!