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:
parent
f0388b527d
commit
969f169eb1
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
// #docregion
|
||||
import { Type } from '@angular/core';
|
||||
|
||||
export class AdItem {
|
||||
constructor(public component: Type<any>, public data: any) {}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
// #docregion
|
||||
export interface AdComponent {
|
||||
data: any;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// #docregion
|
||||
import { Directive, ViewContainerRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[ad-host]',
|
||||
})
|
||||
export class AdDirective {
|
||||
constructor(public viewContainerRef: ViewContainerRef) { }
|
||||
}
|
||||
|
|
@ -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'}),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// #docregion
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -36,6 +36,11 @@
|
|||
"intro": "Techniques for Dependency Injection"
|
||||
},
|
||||
|
||||
"dynamic-component-loader": {
|
||||
"title": "Dynamic Component Loader",
|
||||
"intro": "Load components dynamically"
|
||||
},
|
||||
|
||||
"dynamic-form": {
|
||||
"title": "Dynamic Forms",
|
||||
"intro": "Render dynamic forms with FormGroup"
|
||||
|
|
|
@ -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 |
Loading…
Reference in New Issue