Click here to Skip to main content
15,880,796 members
Articles / Web Development / HTML

A Note on Angular 2 Container Components

Rate me:
Please Sign up or sign in to vote.
4.29/5 (3 votes)
16 Jan 2017CPOL5 min read 13K   88   5  
This is a note on Angular 2 container components. 

Introduction

This is a note on Angular 2 container components. In a computer science terminology, it is called transclusion.

Background

Angular encourages reusability by its component based programming. Sometimes, you may find that it is convenient to create a container component and share it with different contents. If you are familiar with ASP.NET MVC, a container component is functionally similar to a layout page in ASP.NET MVC.

Image 1

The attached ASP.NET MVC project is written in Visual Studio community 2015 Update 3. This project includes all the dependency configuration files to download the "node_modules". It also has all the configurations to compile and run an Angular 2 application. But setting up the environment in Visual Studio to compile the Typescript files and run an application is not a trivial task. I strongly recommend you to take a look at my early note if you want to download and run this example. Besides Angular 2, this example used jQuery. If you want to run it, you will need an internet connection, because the jQuery library is link to a CDN.

In Angular 2, the organization of components, modules, and the "boostrap" process is not trivial. If you are not familiar with it, you can take a look at my early note, which has a small example of this process. Of course, you can always go to the official website to obtain the updated information. In this note, I will directly jump to the subject on how to create a container component and how to use it to insert the contents.

The Container Component

As an example, a slide container is implemented in the "slide-container" folder. The HTML template for this component is the following.

HTML
<div class="slide-container"
     [style.width]="Width" [style.height]="Height"
     style="position: relative; border-radius: 6px;
        box-shadow: 3px 3px 3px 3px #888888; overflow: hidden;">
    
    <div [style.width]="Width" [style.height]="ControlHeight"
         style="position: absolute; top: 0px; left: 0px">
    
        <div style="float: left; height: 100%; display: flex;
                align-items: center; margin-left: 5px">
    
            <!-- Title/header section-->
            <ng-content select="[title-header]"></ng-content>
        </div>
        <div style="float: right; height: 100%; display: flex;
                align-items: center; margin-right: 5px">
            <button style="height: 80%; border-radius: 3px"
                    (click)="Slide($event, 'left')">
    
                <!-- Button text on show left button -->
                <ng-content select="[left-button-text]"></ng-content>
            </button>
            <button style="height: 80%; border-radius: 3px"
                    (click)="Slide($event, 'right')">
    
                <!-- Button text on show right button -->
                <ng-content select="[right-button-text]"></ng-content>
            </button>
        </div>
    </div>
    
    <!-- Left side shows by default -->
    <div class="slide-left"
         [style.width]="Width" [style.height]="SlideHeight"
         [style.top]="ControlHeight"
         style="position: absolute; left: 0px;">
    
        <!-- Left content -->
        <ng-content select="[left-content]"></ng-content>
    </div>
    
    <div class="slide-right"
         [style.width]="Width" [style.height]="SlideHeight"
         [style.top]="ControlHeight" [style.left]="Width"
         style="position: absolute">
    
        <!-- Right content -->
        <ng-content select="[right-content]"></ng-content>
    </div>
</div>

The key to create a container component is the "<ng-content>" tag. It is the place where the components that use this container component to insert the actual content. In a container component, you may have multiple "<ng-content>" tags. The "select" attribute allows different contents to be inserted into the correct locations. In order that this container component does something, let us take a look at the Typescript file "slide-container.component.ts".

declare let $;
import { Component, Input, OnInit } from '@angular/core';
    
@Component({
    moduleId: module.id,
    selector: 'slide-container',
    templateUrl: 'slide-container.component.html'
})
export class SlideContainer implements OnInit {
    @Input() Width;
    @Input() Height;
    
    private animation_speed = 500;
    public ControlHeight;
    public SlideHeight;
    
    constructor() { }
    ngOnInit() {
        let height = parseInt(this.Height);
        let controlHeight = 50;
    
        this.ControlHeight = controlHeight + 'px';
        this.SlideHeight = (height - controlHeight) + 'px';
    }
    
    public Slide(e, show: string) {
        let container = $(e.target).closest('.slide-container');
        let l = $('.slide-left', container);
        let r = $('.slide-right', container);
    
        let width = parseInt(this.Width);
        if (show == 'left') {
            l.animate({ left: 0 }, this.animation_speed);
            r.animate({ left: width }, this.animation_speed);
        }
        else {
            l.animate({ left: -1 * width }, this.animation_speed);
            r.animate({ left: 0 }, this.animation_speed);
        }
    
        return false;
    }
}

To keep this example simple, the container component does not have much functionality. All it does it to bind the two buttons to the "Slide" method in the "SlideContainer" class. When the "left" button is clicked, it slides to show the left content, when the "right" button is clicked, it slides to show the right content.

Insert the Contents into the Container Component

Inserting the contents into the container component is pretty easy. Let us take a look at the component implemented in the "slide-container-contents" folder.

HTML
<slide-container Width="400px" Height="200px">
    <span left-button-text>SHOW LEFT</span>
    <span right-button-text>SHOW RIGHT</span>
    
    <span title-header>NG2-Transclusion Example</span>
    
    <div left-content style="width: 100%; height: 100%;
        box-sizing: border-box; padding: 20px;
        color: white;
        background-color: green">
        This is the left content...
    </div>
    <div right-content style="width: 100%; height: 100%;
        box-sizing: border-box; padding: 20px;
        color: white;
        background-color: blue">
        This is the right content...
    </div>
</slide-container>

In this component, I inserted the contents into each "<ng-content>" in the "<slide-container>". You may need to pay a little attention to how the "select" attribute is used to insert the content to its desired location. Since this note is on how to use a container component, I did not add any functionality in the "slide-container-contents" component, so the "slide-container-contents.component.ts" file is left virtually empty.

import { Component, Input, OnInit } from '@angular/core';
    
@Component({
    moduleId: module.id,
    selector: 'slide-container-contents',
    templateUrl: 'slide-container-contents.component.html'
})
export class SlideContainerContents implements OnInit {
    ngOnInit() {}
}

Run the Application

The environment to compile Typescript files and run an Angular 2 application in Visual Studio is not trivial. If you encounter problems, you may take a look at my early note. If everything goes nicely, when you run the application, you should see the following page.

Image 2

You can see all the contents are properly inserted into the container component. If you click on the "SHOW RIGHT" button, you can see that the content slides to the right.

Image 3

In your Angular 2 applications, if you have multiple places that need this sliding effect, creating a container component and use it in these places can save you from writing duplicated code to implement this effect in multiple places.

This Container Component is Terrible!

You have seen that the container component works, but it is a terrible Angular component. Let us make a small change on the "slide-container.component.ts" file to see how terrible it is.

declare let $;
import { Component, Input, OnInit, DoCheck } from '@angular/core';
    
@Component({
    moduleId: module.id,
    selector: 'slide-container',
    templateUrl: 'slide-container.component.html'
})
export class SlideContainer implements OnInit, DoCheck {
    @Input() Width;
    @Input() Height;
    
    private animation_speed = 500;
    public ControlHeight;
    public SlideHeight;
    
    constructor() { }
    
    // Console.log() to see if change detection runs
    ngDoCheck() {
        console.log('Angular change detection runs!');
    }
    
    ngOnInit() {
        let height = parseInt(this.Height);
        let controlHeight = 50;
    
        this.ControlHeight = controlHeight + 'px';
        this.SlideHeight = (height - controlHeight) + 'px';
    }
    
    public Slide(e, show: string) {
        let container = $(e.target).closest('.slide-container');
        let l = $('.slide-left', container);
        let r = $('.slide-right', container);
    
        let width = parseInt(this.Width);
        if (show == 'left') {
            l.animate({ left: 0 }, this.animation_speed);
            r.animate({ left: width }, this.animation_speed);
        }
        else {
            l.animate({ left: -1 * width }, this.animation_speed);
            r.animate({ left: 0 }, this.animation_speed);
        }
    
        return false;
    }
}

The only change here is adding a function called "ngDoCheck()" that prints a message saying that "Angular change detection runs!". The Angular document says that the "ngDoCheck()" functon runs for every change detection.

Image 4

If you run the example and if you keep the developer tool open. If you now click the "SHOW RIGHT" button, you can see that Angular change detection ran 34 times for this simple button click.

Image 5

Angular change detection runs for any DOM event or callback function that is under Angular's control. To achieve the sliding effect, jQuery scheduled 33 "setTimeout()" callbacks to move the content in the container in the sliding fashion. Each callback triggered a change detection.

Take It Out From Angular

Angular change detection is the Angular way to keep the data in sync with the DOM. According to some blogs, Angular change detection runs very fast. But it is very well justified that we want to take it out from this container component.

  • This container component does not have any data for Angular to bind, its only purpose is to slide the contents;
  • Angular change detection runs at the global level. It means that if we trigger a change detection from this component, Angular will potentially look at the whole component tree in the whole Angular application. Although Angular claims that change detection runs very fast, 34 change detections for a simple button click is outrageous.

To take it out from Angular, we need to inject the "NgZone" into the component.

declare let $;
import { Component, Input, OnInit, DoCheck } from '@angular/core';
import { NgZone, ElementRef } from '@angular/core';
    
@Component({
    moduleId: module.id,
    selector: 'slide-container',
    templateUrl: 'slide-container.component.html'
})
export class SlideContainer implements OnInit, DoCheck {
    @Input() Width;
    @Input() Height;
    
    private animation_speed = 500;
    public ControlHeight;
    public SlideHeight;
    
    constructor(private zone: NgZone, private eRef: ElementRef) { }
    
    // Console.log() to see if change detection runs
    ngDoCheck() {
        console.log('Angular change detection runs!');
    }
    
    ngOnInit() {
        let height = parseInt(this.Height);
        let controlHeight = 50;
    
        this.ControlHeight = controlHeight + 'px';
        this.SlideHeight = (height - controlHeight) + 'px';
    
        // Hook up the button click events out of Angular
        this.zone.runOutsideAngular(() => {
            let container = $(this.eRef.nativeElement);
            let l = $('.slide-left', container);
            let r = $('.slide-right', container);
            let width = parseInt(this.Width);
            let speed = this.animation_speed;
    
            let slide = function (show) {
                if (show == 'left') {
                    l.animate({ left: 0 }, speed);
                    r.animate({ left: width }, speed);
                }
                else {
                    l.animate({ left: -1 * width }, speed);
                    r.animate({ left: 0 }, speed);
                }
            };
    
            // Show left button
            $('.left-btn', container).click(function () {
                slide('left');
                return false;
            });
    
            // Show right button
            $('.right-btn', container).click(function () {
                slide('right');
                return false;
            });

        });
    }
}

The "NgZone.runOutsideAngular()" function allows us hook up the button click events outside of Angular, so they will not trigger change detections.  We also need to change the template file to remove the bindings to the "Slide()" function that has been removed from the "slide-container.component.ts" file.

HTML
<div style="float: right; height: 100%; display: flex;
        align-items: center; margin-right: 5px">
    <button class="left-btn" style="height: 80%; border-radius: 3px">

        <!-- Button text on show left button -->
        <ng-content select="[left-button-text]"></ng-content>
    </button>
    <button class="right-btn" style="height: 80%; border-radius: 3px">

        <!-- Button text on show right button -->
        <ng-content select="[right-button-text]"></ng-content>
    </button>
</div>

If you run the example application and click on the buttons, you will find that the button clicks no longer trigger change detections.

Points of Interest

  • This is a note on Angular 2 container components. In Angular terminology, it is called transclusion;
  • I hope you like my postings and I hope this note can help you one way or the other.

History

First Revision - 1/14/2017

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
I have been working in the IT industry for some time. It is still exciting and I am still learning. I am a happy and honest person, and I want to be your friend.

Comments and Discussions

 
-- There are no messages in this forum --