diff --git a/modules/@angular/common/src/common.ts b/modules/@angular/common/src/common.ts index 014de76d8a..74cd089c05 100644 --- a/modules/@angular/common/src/common.ts +++ b/modules/@angular/common/src/common.ts @@ -14,7 +14,7 @@ export * from './location/index'; export {NgLocaleLocalization, NgLocalization} from './localization'; export {CommonModule} from './common_module'; -export {NgClass, NgFor, NgIf, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet} from './directives/index'; +export {NgClass, NgFor, NgIf, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet, NgComponentOutlet} from './directives/index'; export {AsyncPipe, DatePipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, UpperCasePipe, TitleCasePipe} from './pipes/index'; export {VERSION} from './version'; export {Version} from '@angular/core'; diff --git a/modules/@angular/common/src/directives/index.ts b/modules/@angular/common/src/directives/index.ts index 5984efc0a1..38ba2878d8 100644 --- a/modules/@angular/common/src/directives/index.ts +++ b/modules/@angular/common/src/directives/index.ts @@ -9,6 +9,7 @@ import {Provider} from '@angular/core'; import {NgClass} from './ng_class'; +import {NgComponentOutlet} from './ng_component_outlet'; import {NgFor} from './ng_for'; import {NgIf} from './ng_if'; import {NgPlural, NgPluralCase} from './ng_plural'; @@ -18,6 +19,7 @@ import {NgTemplateOutlet} from './ng_template_outlet'; export { NgClass, + NgComponentOutlet, NgFor, NgIf, NgPlural, @@ -29,12 +31,14 @@ export { NgTemplateOutlet }; + /** * A collection of Angular directives that are likely to be used in each and every Angular * application. */ export const COMMON_DIRECTIVES: Provider[] = [ NgClass, + NgComponentOutlet, NgFor, NgIf, NgTemplateOutlet, diff --git a/modules/@angular/common/src/directives/ng_component_outlet.ts b/modules/@angular/common/src/directives/ng_component_outlet.ts new file mode 100644 index 0000000000..9ad6c3988f --- /dev/null +++ b/modules/@angular/common/src/directives/ng_component_outlet.ts @@ -0,0 +1,86 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ComponentFactoryResolver, ComponentRef, Directive, Injector, Input, OnChanges, Provider, SimpleChanges, Type, ViewContainerRef} from '@angular/core'; + + +/** + * Instantiates a single {@link Component} type and inserts its Host View into current View. + * `NgComponentOutlet` provides a declarative approach for dynamic component creation. + * + * `NgComponentOutlet` requires a component type, if a falsy value is set the view will clear and + * any existing component will get destroyed. + * + * ### Fine tune control + * + * You can control the component creation process by using the following optional attributes: + * + * * `ngOutletInjector`: Optional custom {@link Injector} that will be used as parent for the + * Component. + * Defaults to the injector of the current view container. + * + * * `ngOutletProviders`: Optional injectable objects ({@link Provider}) that are visible to the + * component. + * + * * `ngOutletContent`: Optional list of projectable nodes to insert into the content + * section of the component, if exists. ({@link NgContent}). + * + * + * ### Syntax + * + * Simple + * ``` + * + * ``` + * + * Customized + * ``` + * + * + * ``` + * + * # Example + * + * {@example common/ngComponentOutlet/ts/module.ts region='SimpleExample'} + * + * A more complete example with additional options: + * + * {@example common/ngComponentOutlet/ts/module.ts region='CompleteExample'} + * + * @experimental + */ +@Directive({selector: '[ngComponentOutlet]'}) +export class NgComponentOutlet implements OnChanges { + @Input() ngComponentOutlet: Type; + @Input() ngComponentOutletInjector: Injector; + @Input() ngComponentOutletContent: any[][]; + + componentRef: ComponentRef; + + constructor( + private _cmpFactoryResolver: ComponentFactoryResolver, + private _viewContainerRef: ViewContainerRef) {} + + ngOnChanges(changes: SimpleChanges) { + if (this.componentRef) { + this._viewContainerRef.remove(this._viewContainerRef.indexOf(this.componentRef.hostView)); + } + this._viewContainerRef.clear(); + this.componentRef = null; + + if (this.ngComponentOutlet) { + let injector = this.ngComponentOutletInjector || this._viewContainerRef.parentInjector; + + this.componentRef = this._viewContainerRef.createComponent( + this._cmpFactoryResolver.resolveComponentFactory(this.ngComponentOutlet), + this._viewContainerRef.length, injector, this.ngComponentOutletContent); + } + } +} diff --git a/modules/@angular/common/test/directives/ng_component_outlet_spec.ts b/modules/@angular/common/test/directives/ng_component_outlet_spec.ts new file mode 100644 index 0000000000..58e91d6317 --- /dev/null +++ b/modules/@angular/common/test/directives/ng_component_outlet_spec.ts @@ -0,0 +1,184 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {CommonModule} from '@angular/common'; +import {NgComponentOutlet} from '@angular/common/src/directives/ng_component_outlet'; +import {Component, ComponentRef, Inject, Injector, NO_ERRORS_SCHEMA, NgModule, OpaqueToken, Optional, Provider, QueryList, ReflectiveInjector, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; +import {TestBed, async} from '@angular/core/testing'; +import {expect} from '@angular/platform-browser/testing/matchers'; + +export function main() { + describe('insert/remove', () => { + + beforeEach(() => { TestBed.configureTestingModule({imports: [TestModule]}); }); + + it('should do nothing if component is null', async(() => { + const template = ``; + TestBed.overrideComponent(TestComponent, {set: {template: template}}); + let fixture = TestBed.createComponent(TestComponent); + + fixture.componentInstance.currentComponent = null; + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveText(''); + })); + + it('should insert content specified by a component', async(() => { + let fixture = TestBed.createComponent(TestComponent); + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText(''); + + fixture.componentInstance.currentComponent = InjectedComponent; + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('foo'); + })); + + it('should emit a ComponentRef once a component was created', async(() => { + let fixture = TestBed.createComponent(TestComponent); + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText(''); + + fixture.componentInstance.cmpRef = null; + fixture.componentInstance.currentComponent = InjectedComponent; + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('foo'); + expect(fixture.componentInstance.cmpRef).toBeAnInstanceOf(ComponentRef); + expect(fixture.componentInstance.cmpRef.instance).toBeAnInstanceOf(InjectedComponent); + })); + + + it('should clear view if component becomes null', async(() => { + let fixture = TestBed.createComponent(TestComponent); + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText(''); + + fixture.componentInstance.currentComponent = InjectedComponent; + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('foo'); + + fixture.componentInstance.currentComponent = null; + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText(''); + })); + + + it('should swap content if component changes', async(() => { + let fixture = TestBed.createComponent(TestComponent); + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText(''); + + fixture.componentInstance.currentComponent = InjectedComponent; + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('foo'); + + fixture.componentInstance.currentComponent = InjectedComponentAgain; + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('bar'); + })); + + it('should use the injector, if one supplied', async(() => { + let fixture = TestBed.createComponent(TestComponent); + + const uniqueValue = {}; + fixture.componentInstance.currentComponent = InjectedComponent; + fixture.componentInstance.injector = ReflectiveInjector.resolveAndCreate( + [{provide: TEST_TOKEN, useValue: uniqueValue}], fixture.componentRef.injector); + + fixture.detectChanges(); + let cmpRef: ComponentRef = fixture.componentInstance.cmpRef; + expect(cmpRef).toBeAnInstanceOf(ComponentRef); + expect(cmpRef.instance).toBeAnInstanceOf(InjectedComponent); + expect(cmpRef.instance.testToken).toBe(uniqueValue); + + })); + + it('should resolve a with injector', async(() => { + let fixture = TestBed.createComponent(TestComponent); + + fixture.componentInstance.cmpRef = null; + fixture.componentInstance.currentComponent = InjectedComponent; + fixture.detectChanges(); + let cmpRef: ComponentRef = fixture.componentInstance.cmpRef; + expect(cmpRef).toBeAnInstanceOf(ComponentRef); + expect(cmpRef.instance).toBeAnInstanceOf(InjectedComponent); + expect(cmpRef.instance.testToken).toBeNull(); + })); + + it('should render projectable nodes, if supplied', async(() => { + const template = `${TEST_CMP_TEMPLATE}`; + TestBed.overrideComponent(TestComponent, {set: {template: template}}) + .configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}); + + TestBed + .overrideComponent(InjectedComponent, {set: {template: ``}}) + .configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}); + + let fixture = TestBed.createComponent(TestComponent); + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText(''); + + fixture.componentInstance.currentComponent = InjectedComponent; + fixture.componentInstance.projectables = + [fixture.componentInstance.vcRef + .createEmbeddedView(fixture.componentInstance.tplRefs.first) + .rootNodes]; + + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('projected foo'); + })); + }); +} + +const TEST_TOKEN = new OpaqueToken('TestToken'); +@Component({selector: 'injected-component', template: 'foo'}) +class InjectedComponent { + constructor(@Optional() @Inject(TEST_TOKEN) public testToken: any) {} +} + + +@Component({selector: 'injected-component-again', template: 'bar'}) +class InjectedComponentAgain { +} + +const TEST_CMP_TEMPLATE = + ``; +@Component({selector: 'test-cmp', template: TEST_CMP_TEMPLATE}) +class TestComponent { + currentComponent: Type; + injector: Injector; + projectables: any[][]; + + get cmpRef(): ComponentRef { return this.ngComponentOutlet.componentRef; } + set cmpRef(value: ComponentRef) { this.ngComponentOutlet.componentRef = value; } + + @ViewChildren(TemplateRef) tplRefs: QueryList>; + @ViewChild(NgComponentOutlet) ngComponentOutlet: NgComponentOutlet; + + constructor(public vcRef: ViewContainerRef) {} +} + +@NgModule({ + imports: [CommonModule], + declarations: [TestComponent, InjectedComponent, InjectedComponentAgain], + exports: [TestComponent, InjectedComponent, InjectedComponentAgain], + entryComponents: [InjectedComponent, InjectedComponentAgain] +}) +export class TestModule { +} diff --git a/modules/@angular/examples/common/ngComponentOutlet/ts/e2e_test/ngComponentOutlet_spec.ts b/modules/@angular/examples/common/ngComponentOutlet/ts/e2e_test/ngComponentOutlet_spec.ts new file mode 100644 index 0000000000..57bf9d0cd7 --- /dev/null +++ b/modules/@angular/examples/common/ngComponentOutlet/ts/e2e_test/ngComponentOutlet_spec.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {$, ExpectedConditions, browser, by, element} from 'protractor'; +import {verifyNoBrowserErrors} from '../../../../_common/e2e_util'; + +function waitForElement(selector: string) { + const EC = ExpectedConditions; + // Waits for the element with id 'abc' to be present on the dom. + browser.wait(EC.presenceOf($(selector)), 20000); +} + +describe('ngComponentOutlet', () => { + const URL = 'common/ngComponentOutlet/ts/'; + afterEach(verifyNoBrowserErrors); + + describe('ng-component-outlet-example', () => { + it('should render simple', () => { + browser.get(URL); + waitForElement('ng-component-outlet-simple-example'); + expect(element.all(by.css('hello-world')).getText()).toEqual(['Hello World!']); + }); + + it('should render complete', () => { + browser.get(URL); + waitForElement('ng-component-outlet-complete-example'); + expect(element.all(by.css('complete-component')).getText()).toEqual(['Complete: Ahoj Svet!']); + }); + }); +}); diff --git a/modules/@angular/examples/common/ngComponentOutlet/ts/module.ts b/modules/@angular/examples/common/ngComponentOutlet/ts/module.ts new file mode 100644 index 0000000000..ff1cba7824 --- /dev/null +++ b/modules/@angular/examples/common/ngComponentOutlet/ts/module.ts @@ -0,0 +1,83 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Component, Injectable, Injector, NgModule, ReflectiveInjector} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; + + + +// #docregion SimpleExample +@Component({selector: 'hello-world', template: 'Hello World!'}) +class HelloWorld { +} + +@Component({ + selector: 'ng-component-outlet-simple-example', + template: `` +}) +class NgTemplateOutletSimpleExample { + // This field is necessary to expose HelloWorld to the template. + HelloWorld = HelloWorld; +} +// #enddocregion + +// #docregion CompleteExample +@Injectable() +class Greeter { + suffix = '!' +} + +@Component({ + selector: 'complete-component', + template: `Complete: {{ greeter.suffix }}` +}) +class CompleteComponent { + constructor(public greeter: Greeter) {} +} + +@Component({ + selector: 'ng-component-outlet-complete-example', + template: ` + ` +}) +class NgTemplateOutletCompleteExample { + // This field is necessary to expose CompleteComponent to the template. + CompleteComponent = CompleteComponent; + myInjector: Injector; + + myContent = [[document.createTextNode('Ahoj')], [document.createTextNode('Svet')]]; + + constructor(injector: Injector) { + this.myInjector = ReflectiveInjector.resolveAndCreate([Greeter], injector); + } +} +// #enddocregion + + +@Component({ + selector: 'example-app', + template: ` +
+ ` +}) +class ExampleApp { +} + +@NgModule({ + imports: [BrowserModule], + declarations: [ + ExampleApp, NgTemplateOutletSimpleExample, NgTemplateOutletCompleteExample, HelloWorld, + CompleteComponent + ], + entryComponents: [HelloWorld, CompleteComponent], + bootstrap: [ExampleApp] +}) +export class AppModule { +} diff --git a/tools/public_api_guard/common/index.d.ts b/tools/public_api_guard/common/index.d.ts index 595497fc86..eb2bff554f 100644 --- a/tools/public_api_guard/common/index.d.ts +++ b/tools/public_api_guard/common/index.d.ts @@ -117,6 +117,16 @@ export declare class NgClass implements DoCheck { ngDoCheck(): void; } +/** @experimental */ +export declare class NgComponentOutlet implements OnChanges { + componentRef: ComponentRef; + ngComponentOutlet: Type; + ngComponentOutletContent: any[][]; + ngComponentOutletInjector: Injector; + constructor(_cmpFactoryResolver: ComponentFactoryResolver, _viewContainerRef: ViewContainerRef); + ngOnChanges(changes: SimpleChanges): void; +} + /** @stable */ export declare class NgFor implements DoCheck, OnChanges { ngForOf: any;