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.
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.
<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">
<!--
<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')">
<!--
<ng-content select="[left-button-text]"></ng-content>
</button>
<button style="height: 80%; border-radius: 3px"
(click)="Slide($event, 'right')">
<!--
<ng-content select="[right-button-text]"></ng-content>
</button>
</div>
</div>
<!--
<div class="slide-left"
[style.width]="Width" [style.height]="SlideHeight"
[style.top]="ControlHeight"
style="position: absolute; left: 0px;">
<!--
<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">
<!--
<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.
<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.
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.
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() { }
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.
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.
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) { }
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';
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);
}
};
$('.left-btn', container).click(function () {
slide('left');
return false;
});
$('.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.
<div style="float: right; height: 100%; display: flex;
align-items: center; margin-right: 5px">
<button class="left-btn" style="height: 80%; border-radius: 3px">
<!--
<ng-content select="[left-button-text]"></ng-content>
</button>
<button class="right-btn" style="height: 80%; border-radius: 3px">
<!--
<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