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"
|
"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"
|
||||||
|
|
|
@ -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