Friday, November 24, 2017

ASP.NET MVC style layout and section in Angular

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


No comments:

Post a Comment