diff --git a/public/docs/_examples/cb-dynamic-component-loader/e2e-spec.ts b/public/docs/_examples/cb-dynamic-component-loader/e2e-spec.ts new file mode 100644 index 0000000000..5036ac2a88 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-component-loader/e2e-spec.ts @@ -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(); + }); +}); diff --git a/public/docs/_examples/cb-dynamic-component-loader/ts/app/ad-banner.component.ts b/public/docs/_examples/cb-dynamic-component-loader/ts/app/ad-banner.component.ts new file mode 100644 index 0000000000..293d31dea9 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-component-loader/ts/app/ad-banner.component.ts @@ -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: ` +
+

Advertisements

+ +
+ ` + // #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); + (componentRef.instance).data = adItem.data; + } + + getAds() { + this.interval = setInterval(() => { + this.loadComponent(); + }, 3000); + } +} diff --git a/public/docs/_examples/cb-dynamic-component-loader/ts/app/ad-item.ts b/public/docs/_examples/cb-dynamic-component-loader/ts/app/ad-item.ts new file mode 100644 index 0000000000..ef8ca70577 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-component-loader/ts/app/ad-item.ts @@ -0,0 +1,6 @@ +// #docregion +import { Type } from '@angular/core'; + +export class AdItem { + constructor(public component: Type, public data: any) {} +} diff --git a/public/docs/_examples/cb-dynamic-component-loader/ts/app/ad.component.ts b/public/docs/_examples/cb-dynamic-component-loader/ts/app/ad.component.ts new file mode 100644 index 0000000000..dee3b47953 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-component-loader/ts/app/ad.component.ts @@ -0,0 +1,4 @@ +// #docregion +export interface AdComponent { + data: any; +} diff --git a/public/docs/_examples/cb-dynamic-component-loader/ts/app/ad.directive.ts b/public/docs/_examples/cb-dynamic-component-loader/ts/app/ad.directive.ts new file mode 100644 index 0000000000..312e605228 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-component-loader/ts/app/ad.directive.ts @@ -0,0 +1,10 @@ +// #docregion +import { Directive, ViewContainerRef } from '@angular/core'; + +@Directive({ + selector: '[ad-host]', +}) +export class AdDirective { + constructor(public viewContainerRef: ViewContainerRef) { } +} + diff --git a/public/docs/_examples/cb-dynamic-component-loader/ts/app/ad.service.ts b/public/docs/_examples/cb-dynamic-component-loader/ts/app/ad.service.ts new file mode 100644 index 0000000000..91b0758771 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-component-loader/ts/app/ad.service.ts @@ -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'}), + ]; + } +} diff --git a/public/docs/_examples/cb-dynamic-component-loader/ts/app/app.component.ts b/public/docs/_examples/cb-dynamic-component-loader/ts/app/app.component.ts new file mode 100644 index 0000000000..89359ccdf6 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-component-loader/ts/app/app.component.ts @@ -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: ` +
+ +
+ ` +}) +export class AppComponent implements OnInit { + ads: AdItem[]; + + constructor(private adService: AdService) {} + + ngOnInit() { + this.ads = this.adService.getAds(); + } +} + diff --git a/public/docs/_examples/cb-dynamic-component-loader/ts/app/app.module.ts b/public/docs/_examples/cb-dynamic-component-loader/ts/app/app.module.ts new file mode 100644 index 0000000000..a65d394709 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-component-loader/ts/app/app.module.ts @@ -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() {} +} + diff --git a/public/docs/_examples/cb-dynamic-component-loader/ts/app/hero-job-ad.component.ts b/public/docs/_examples/cb-dynamic-component-loader/ts/app/hero-job-ad.component.ts new file mode 100644 index 0000000000..675a03d0e0 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-component-loader/ts/app/hero-job-ad.component.ts @@ -0,0 +1,19 @@ +// #docregion +import { Component, Input } from '@angular/core'; + +import { AdComponent } from './ad.component'; + +@Component({ + template: ` +
+

{{data.headline}}

+ + {{data.body}} +
+ ` +}) +export class HeroJobAdComponent implements AdComponent { + @Input() data: any; + +} + diff --git a/public/docs/_examples/cb-dynamic-component-loader/ts/app/hero-profile.component.ts b/public/docs/_examples/cb-dynamic-component-loader/ts/app/hero-profile.component.ts new file mode 100644 index 0000000000..1c266db3c9 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-component-loader/ts/app/hero-profile.component.ts @@ -0,0 +1,22 @@ +// #docregion +import { Component, Input } from '@angular/core'; + +import { AdComponent } from './ad.component'; + +@Component({ + template: ` +
+

Featured Hero Profile

+

{{data.name}}

+ +

{{data.bio}}

+ + Hire this hero today! +
+ ` +}) +export class HeroProfileComponent implements AdComponent { + @Input() data: any; +} + + diff --git a/public/docs/_examples/cb-dynamic-component-loader/ts/app/main.ts b/public/docs/_examples/cb-dynamic-component-loader/ts/app/main.ts new file mode 100644 index 0000000000..bc69d2bd33 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-component-loader/ts/app/main.ts @@ -0,0 +1,6 @@ +// #docregion +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); + diff --git a/public/docs/_examples/cb-dynamic-component-loader/ts/example-config.json b/public/docs/_examples/cb-dynamic-component-loader/ts/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/cb-dynamic-component-loader/ts/index.html b/public/docs/_examples/cb-dynamic-component-loader/ts/index.html new file mode 100644 index 0000000000..45529915c2 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-component-loader/ts/index.html @@ -0,0 +1,26 @@ + + + + + + Dynamic Component Loader + + + + + + + + + + + + + + + Loading app... + + + diff --git a/public/docs/_examples/cb-dynamic-component-loader/ts/sample.css b/public/docs/_examples/cb-dynamic-component-loader/ts/sample.css new file mode 100644 index 0000000000..7a2ca1f2dc --- /dev/null +++ b/public/docs/_examples/cb-dynamic-component-loader/ts/sample.css @@ -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; +} \ No newline at end of file diff --git a/public/docs/ts/latest/cookbook/_data.json b/public/docs/ts/latest/cookbook/_data.json index 82f126f85b..e7ffea0ace 100644 --- a/public/docs/ts/latest/cookbook/_data.json +++ b/public/docs/ts/latest/cookbook/_data.json @@ -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" diff --git a/public/docs/ts/latest/cookbook/dynamic-component-loader.jade b/public/docs/ts/latest/cookbook/dynamic-component-loader.jade new file mode 100644 index 0000000000..d6a35e56c2 --- /dev/null +++ b/public/docs/ts/latest/cookbook/dynamic-component-loader.jade @@ -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. + + +:marked + ## Table of contents + + [Dynamic Component Loading](#dynamic-loading) + + [Where to load the component](#where-to-load) + + [Loading components](#loading-components) + +.l-main-section + +: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 + +: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 + +: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") \ No newline at end of file diff --git a/public/resources/images/cookbooks/dynamic-component-loader/ads.gif b/public/resources/images/cookbooks/dynamic-component-loader/ads.gif new file mode 100644 index 0000000000..f7a990fb11 Binary files /dev/null and b/public/resources/images/cookbooks/dynamic-component-loader/ads.gif differ