docs(cb-dynamic-component-loading): add dynamic component loading cookbook (#2896)

s

s

s

s

s

s

s

s

s

s

s

s

s

s

s
This commit is contained in:
Torgeir Helgevold 2017-01-25 18:44:17 -05:00 committed by Jules Kremer
parent f0388b527d
commit 969f169eb1
17 changed files with 392 additions and 0 deletions

View File

@ -0,0 +1,21 @@
'use strict'; // necessary for es6 output in node
import { browser, element, by } from 'protractor';
/* tslint:disable:quotemark */
describe('Dynamic Component Loader', function () {
beforeEach(function () {
browser.get('');
});
it('should load ad banner', function () {
let headline = element(by.xpath("//h4[text()='Featured Hero Profile']"));
let name = element(by.xpath("//h3[text()='Bombasto']"));
let bio = element(by.xpath("//p[text()='Brave as they come']"));
expect(name).toBeDefined();
expect(headline).toBeDefined();
expect(bio).toBeDefined();
});
});

View File

@ -0,0 +1,55 @@
// #docregion
import { Component, Input, AfterViewInit, ViewChild, ComponentFactoryResolver, OnDestroy } from '@angular/core';
import { AdDirective } from './ad.directive';
import { AdItem } from './ad-item';
import { AdComponent } from './ad.component';
@Component({
selector: 'add-banner',
// #docregion ad-host
template: `
<div class="ad-banner">
<h3>Advertisements</h3>
<template ad-host></template>
</div>
`
// #enddocregion ad-host
})
export class AdBannerComponent implements AfterViewInit, OnDestroy {
@Input() ads: AdItem[];
currentAddIndex: number = -1;
@ViewChild(AdDirective) adHost: AdDirective;
subscription: any;
interval: any;
constructor(private _componentFactoryResolver: ComponentFactoryResolver) { }
ngAfterViewInit() {
this.loadComponent();
this.getAds();
}
ngOnDestroy() {
clearInterval(this.interval);
}
loadComponent() {
this.currentAddIndex = (this.currentAddIndex + 1) % this.ads.length;
let adItem = this.ads[this.currentAddIndex];
let componentFactory = this._componentFactoryResolver.resolveComponentFactory(adItem.component);
let viewContainerRef = this.adHost.viewContainerRef;
viewContainerRef.clear();
let componentRef = viewContainerRef.createComponent(componentFactory);
(<AdComponent>componentRef.instance).data = adItem.data;
}
getAds() {
this.interval = setInterval(() => {
this.loadComponent();
}, 3000);
}
}

View File

@ -0,0 +1,6 @@
// #docregion
import { Type } from '@angular/core';
export class AdItem {
constructor(public component: Type<any>, public data: any) {}
}

View File

@ -0,0 +1,4 @@
// #docregion
export interface AdComponent {
data: any;
}

View File

@ -0,0 +1,10 @@
// #docregion
import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[ad-host]',
})
export class AdDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}

View File

@ -0,0 +1,23 @@
// #docregion
import { Injectable } from '@angular/core';
import { HeroJobAdComponent } from './hero-job-ad.component';
import { HeroProfileComponent } from './hero-profile.component';
import { AdItem } from './ad-item';
@Injectable()
export class AdService {
getAds() {
return [
new AdItem(HeroProfileComponent, {name: 'Bombasto', bio: 'Brave as they come'}),
new AdItem(HeroProfileComponent, {name: 'Dr IQ', bio: 'Smart as they come'}),
new AdItem(HeroJobAdComponent, {headline: 'Hiring for several positions',
body: 'Submit your resume today!'}),
new AdItem(HeroJobAdComponent, {headline: 'Openings in all departments',
body: 'Apply today'}),
];
}
}

View File

@ -0,0 +1,24 @@
// #docregion
import { Component, OnInit } from '@angular/core';
import { AdService } from './ad.service';
import { AdItem } from './ad-item';
@Component({
selector: 'my-app',
template: `
<div>
<add-banner [ads]="ads"></add-banner>
</div>
`
})
export class AppComponent implements OnInit {
ads: AdItem[];
constructor(private adService: AdService) {}
ngOnInit() {
this.ads = this.adService.getAds();
}
}

View File

@ -0,0 +1,27 @@
// #docregion
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { HeroJobAdComponent } from './hero-job-ad.component';
import { AdBannerComponent } from './ad-banner.component';
import { HeroProfileComponent } from './hero-profile.component';
import { AdDirective } from './ad.directive';
import { AdService } from './ad.service';
@NgModule({
imports: [ BrowserModule ],
providers: [AdService],
declarations: [ AppComponent,
AdBannerComponent,
HeroJobAdComponent,
HeroProfileComponent,
AdDirective ],
// #docregion entry-components
entryComponents: [ HeroJobAdComponent, HeroProfileComponent ],
// #enddocregion entry-components
bootstrap: [ AppComponent ]
})
export class AppModule {
constructor() {}
}

View File

@ -0,0 +1,19 @@
// #docregion
import { Component, Input } from '@angular/core';
import { AdComponent } from './ad.component';
@Component({
template: `
<div class="job-ad">
<h4>{{data.headline}}</h4>
{{data.body}}
</div>
`
})
export class HeroJobAdComponent implements AdComponent {
@Input() data: any;
}

View File

@ -0,0 +1,22 @@
// #docregion
import { Component, Input } from '@angular/core';
import { AdComponent } from './ad.component';
@Component({
template: `
<div class="hero-profile">
<h3>Featured Hero Profile</h3>
<h4>{{data.name}}</h4>
<p>{{data.bio}}</p>
<strong>Hire this hero today!</strong>
</div>
`
})
export class HeroProfileComponent implements AdComponent {
@Input() data: any;
}

View File

@ -0,0 +1,6 @@
// #docregion
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<base href="/">
<title>Dynamic Component Loader</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="sample.css">
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<my-app>Loading app...</my-app>
</body>
</html>

View File

@ -0,0 +1,23 @@
.hero-profile {
border: 1px solid gray;
padding: 5px;
padding-bottom: 20px;
padding-left: 20px;
border-radius: 10px;
background-color: lightgreen;
color: black;
}
.job-ad {
border: 1px solid gray;
padding: 5px;
padding-bottom: 20px;
padding-left: 20px;
border-radius: 10px;
background-color: lightblue;
color: black;
}
.ad-banner {
width: 400px;
}

View File

@ -36,6 +36,11 @@
"intro": "Techniques for Dependency Injection" "intro": "Techniques for Dependency Injection"
}, },
"dynamic-component-loader": {
"title": "Dynamic Component Loader",
"intro": "Load components dynamically"
},
"dynamic-form": { "dynamic-form": {
"title": "Dynamic Forms", "title": "Dynamic Forms",
"intro": "Render dynamic forms with FormGroup" "intro": "Render dynamic forms with FormGroup"

View File

@ -0,0 +1,121 @@
include ../_util-fns
:marked
Component templates are not always fixed. An application may need to load new components at runtime.
In this cookbook we show how to use `ComponentFactoryResolver` to add components dynamically.
<a id="toc"></a>
:marked
## Table of contents
[Dynamic Component Loading](#dynamic-loading)
[Where to load the component](#where-to-load)
[Loading components](#loading-components)
.l-main-section
<a id="dynamic-loading"></a>
:marked
## Dynamic Component Loading
The following example shows how to build a dynamic ad banner.
The hero agency is planning an ad campaign with several different ads cycling through the banner.
New ad components are added frequently by several different teams. This makes it impractical to use a template with a static component structure.
Instead we need a way to load a new component without a fixed reference to the component in the ad banner's template.
Angular comes with its own API for loading components dynamically. In the following sections you will learn how to use it.
.l-main-section
<a id="where-to-load"></a>
:marked
## Where to load the component
Before components can be added we have to define an anchor point to mark where components can be inserted dynamically.
The ad banner uses a helper directive called `AdDirective` to mark valid insertion points in the template.
+makeExample('cb-dynamic-component-loader/ts/app/ad.directive.ts',null,'app/ad.directive.ts')(format='.')
:marked
`AdDirective` injects `ViewContainerRef` to gain access to the view container of the element that will become the host of the dynamically added component.
.l-main-section
<a id="loading-components"></a>
:marked
## Loading components
The next step is to implement the ad banner. Most of the implementation is in `AdBannerComponent`.
We start by adding a `template` element with the `AdDirective` directive applied.
+makeTabs(
`cb-dynamic-component-loader/ts/app/ad-banner.component.ts,
cb-dynamic-component-loader/ts/app/ad.service.ts,
cb-dynamic-component-loader/ts/app/ad-item.ts,
cb-dynamic-component-loader/ts/app/app.module.ts,
cb-dynamic-component-loader/ts/app/app.component.ts`,
null,
`ad-banner.component.ts,
ad.service.ts,
ad-item.ts,
app.module.ts,
app.component`
)
:marked
The `template` element decorated with the `ad-host` directive marks where dynamically loaded components will be added.
Using a `template` element is recommended since it doesn't render any additional output.
+makeExample('cb-dynamic-component-loader/ts/app/ad-banner.component.ts', 'ad-host' ,'app/ad-banner.component.ts (template)')(format='.')
:marked
### Resolving Components
`AdBanner` takes an array of `AdItem` objects as input. `AdItem` objects specify the type of component to load and any data to bind to the component.
The ad components making up the ad campaign are returned from `AdService`.
Passing an array of components to `AdBannerComponent` allows for a dynamic list of ads without static elements in the template.
`AdBannerComponent` cycles through the array of `AdItems` and loads the corresponding components on an interval. Every 3 seconds a new component is loaded.
`ComponentFactoryResolver` is used to resolve a `ComponentFactory` for each specific component. The component factory is need to create an instance of the component.
`ComponentFactories` are generated by the Angular compiler.
Generally the compiler will generate a component factory for any component referenced in a template.
With dynamically loaded components there are no selector references in the templates since components are loaded at runtime. In order to ensure that the compiler will still generate a factory, dynamically loaded components have to be added to their `NgModule`'s `entryComponents` array.
+makeExample('cb-dynamic-component-loader/ts/app/app.module.ts', 'entry-components' ,'app/app.module.ts (entry components)')(format='.')
:marked
Components are added to the template by calling `createComponent` on the `ViewContainerRef` reference.
`createComponent` returns a reference to the loaded component. The component reference can be used to pass input data or call methods to interact with the component.
In the Ad banner, all components implement a common `AdComponent` interface to standardize the api for passing data to the components.
Two sample components and the `AdComponent` interface are shown below:
+makeTabs(
`cb-dynamic-component-loader/ts/app/hero-job-ad.component.ts,
cb-dynamic-component-loader/ts/app/hero-profile.component.ts,
cb-dynamic-component-loader/ts/app/ad.component.ts`,
null,
`hero-job-ad.component.ts,
hero-profile.component.ts,
ad.component.ts`
)
:marked
The final ad banner looks like this:
figure.image-display
img(src="/resources/images/cookbooks/dynamic-component-loader/ads.gif" alt="Ads")

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 KiB