As you know
ASP.NET MVC provides a great feature for consistent look and feel with Layout
engine. This layout engine consists of two main components, RenderBody and
RenderSection which helps in defining hot areas in the page where we can
substitute content from the current/active page. Almost everyone who worked/working
in ASP.NET MVC have used this feature. In this blog I will go over how to
recreate the same feature in Angular.
RenderBody
Similar to
ASP.NET MVC we need to fill the content of the View at this location in HTML.
In Angular this is similar to the router-outlet. So I will wrap the
router-outlet with our own component called render-body. Here is the code for
this:
import { Component } from '@angular/core';
@Component({
selector: 'render-body',
template: '<router-outlet></router-outlet>'
})
export class RenderBodyComponent {
}
RenderBody
is straight forward. Whenever there is a route transition, Angular routing
automatically inject the content of the view at this location. Here is how
RenderBody can be used in the layout page
<render-body></render-body>
RenderSection
In MVC
RenderSection allows a part of the layout page be replaced by the content
defined in the current page. RenderSection allows us to define multiple named
sections in the layout for which the content be defined in each page. It also
allows to define a default content if the page does not specify the content.
I
implemented the similar functionality in my RenderSection component. In
addition I extended the RenderSection with an ability to hide from the content
page. I also added the capability to have nested levels of RenderSection.
Similar to
MVC, first you would define a named section and then in the content page
specify the target where you would like the content to be displayed. Here is
how you can use the RenderSection in the layout page with a default content
(showing a menu)
<render-section name="menu" fxFlex="25">
<b>Menu</b> <br />
<a routerLink="/home">Home</a> <br />
<a routerLink="/counter">Counter</a> <br />
<a routerLink="/fetch-data">Fetch Data</a> <br />
</render-section>
In the
content page this menu section can be replaced with page specific menu or
content.
<render-section target="menu">
Custom Menu from content page
</render-section>
Let’s delve
into the code on how to implement the RenderSection component in Angular.
As we are
showing content dynamically when a user navigates from page to page, we have to
use ngTemplateOutlet. Angular transclusion (ng-content) is used for displaying
the content. Here is the template code for RenderSection component for dynamic
content:
<ng-container
*ngTemplateOutlet="getTemplate()"></ng-container>
<ng-template
#sectionRef>
<ng-content></ng-content>
</ng-template>`
In the
above code I am getting the current page’s template using the getTemplate
function call and showing its content.
For each
section I am capturing the following data (as defined in the SectionModel):
export class SectionModel {
name: string;
hidden: boolean;
changeDetectorRef: ChangeDetectorRef;
template: TemplateRef<any>;
currentTemplate: TemplateRef<any>;
targets: SectionTargetModel[];
}
Here are
the details for each of the properties in the SectioModel
- name
– This holds the name of the section. This is used to display the content when
the target is equal to the name of the template
- hidden
– This is the initial setting whether the section be hidden or not
- changeDetectorRef
– This is used to force a change detection when we set the target’s content to
the section’s content. If a page has specified the content for a section (using
target attribute), this page’s content is set to the section defined with name
and the change detection is manually invoked for this section component
- template
– This is the default content for the section. If no target is defined in the
content page, then this template is displayed as the section’s content
- currentTemplate
– This the template of the content which is currently shown. The
currentTemplate is bound to the section template. This currentTemplate would
contain either the default template or the template defined in the current
page. A change detection is called whenever there is a change in this variable
- targets
– This is the stack of all the contents defined for a section. As my design
support hierarchical replacement of the content, I am using stack. During the
page load (ngAfterContentInit) if the page has a render-section, then the
section’s template is pushed into this array. When this page is destroyed(ngOnDestroy),
then the page’s template is poped. The targets are of type SectionTargetModel
array whose definition is below:
export class SectionTargetModel {
target: string;
hidden: boolean;
template: TemplateRef<any>;
}
Now let’s
delve into the code. Surprisingly the code is very simple. We need three functions,
push, pop and update. Here is important code for each of these functions.
push
As the name
indicates it pushes the section defined either in the layout or in the page.
This is also needed to support the default section content or the nested
sections. During the page load (ngAfterContentInit), the section is pushed into
the array.
public push(
name: string,
target: string,
hidden: boolean,
template: TemplateRef<any>,
changeDetectorRef:
ChangeDetectorRef
): void {
if (name !== undefined) {
this.sections[name] = <SectionModel>{
name: name,
hidden: hidden,
changeDetectorRef:
changeDetectorRef,
targets:
[<SectionTargetModel>{
target: name,
hidden: hidden,
template: template
}],
template: template,
currentTemplate: template
};
this.update(section);
}
else if(target !==
undefined) {
section.targets.push(<SectionTargetModel>{
target: name,
hidden: hidden,
template: template
});
this.update(section);
}
}
As you see
above the code checks if the section with the name already exists or not, if
not exists creates and pushes the section. If it exists it just pushes the
section into the array. After pushing the section into the array, update is
called to refresh the section in the layout and show the relevant section content
from the page.
pop
Again as the name indicates, pop removes the
page’s section content from the array. During page destroyed(ngOnDestroy) the section is poped
from the array. It also changes the section content from the array.
private popTemplate(name: string): void {
let section: SectionModel = this.sections[name];
section.targets.pop();
this.update(section);
}
Similar to
push the upgrade function is called in pop to refresh the section and to show
the active content.
update
This sets
the current template to a section and calls the change detection to refresh the
section. This always shows the last section content in the array. It also has
the logic to show or hide the section.
private update(section: SectionModel) {
let targetSection = section.targets[section.targets.length - 1];
section.currentTemplate =
targetSection.template;
if (targetSection.hidden) {
section.template.elementRef.nativeElement.parentElement.style.display = "none";
}
if (!targetSection.hidden) {
section.template.elementRef.nativeElement.parentElement.style.display = "block";
}
if (!(section.changeDetectorRef as ViewRef).destroyed) {
section.changeDetectorRef.detectChanges();
}
}
With these
simple three functions, push, pop and update we can get the behaviour similar
to that of ASP.NET MVC sections.
Finally
here is the MVC type layout, which is a regular component in Angular.
AppComponent can also be used for this.
<div fxLayout="row">
<render-section name="menu" fxFlex="25">
<b>Menu</b> <br />
<a routerLink="/home">Home</a> <br />
<a routerLink="/counter">Counter</a> <br />
<a routerLink="/fetch-data">Fetch Data</a> <br />
</render-section>
<div fxFlex>
<render-body></render-body>
</div>
</div>
Finally I
created an Angular module to wrap the RenderBody, RenderSection and supporting
providers. You can review this code from my GitHub site