From 3ef73c2b1945340ca6bd21f1790260c88698ae26 Mon Sep 17 00:00:00 2001 From: Jason Aden Date: Wed, 25 Jan 2017 17:41:08 -0800 Subject: [PATCH] feat(NgComponentOutlet): add NgModule support to NgComponentOutlet directive (#14088) Allow NgComponentOutlet to dynamically load a module, then load a component from that module. Useful for lazy loading code, then add the lazy loaded code to the page using NgComponentOutlet. Closes #14043 --- .../src/directives/ng_component_outlet.ts | 71 ++++++++---- .../directives/ng_component_outlet_spec.ts | 104 +++++++++++++++++- .../ts/e2e_test/ngComponentOutlet_spec.ts | 8 ++ .../common/ngComponentOutlet/ts/module.ts | 39 ++++++- tools/public_api_guard/common/index.d.ts | 7 +- 5 files changed, 196 insertions(+), 33 deletions(-) diff --git a/modules/@angular/common/src/directives/ng_component_outlet.ts b/modules/@angular/common/src/directives/ng_component_outlet.ts index 9ad6c3988f..d20dacbc8b 100644 --- a/modules/@angular/common/src/directives/ng_component_outlet.ts +++ b/modules/@angular/common/src/directives/ng_component_outlet.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentFactoryResolver, ComponentRef, Directive, Injector, Input, OnChanges, Provider, SimpleChanges, Type, ViewContainerRef} from '@angular/core'; +import {ComponentFactoryResolver, ComponentRef, Directive, Injector, Input, NgModuleFactory, NgModuleRef, OnChanges, OnDestroy, Provider, SimpleChanges, Type, ViewContainerRef} from '@angular/core'; + /** @@ -20,16 +21,17 @@ import {ComponentFactoryResolver, ComponentRef, Directive, Injector, Input, OnCh * * 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. + * * `ngComponentOutletInjector`: 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. + * * `ngComponentOutletProviders`: 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}). + * * `ngComponentOutletContent`: Optional list of projectable nodes to insert into the content + * section of the component, if exists. * + * * `ngComponentOutletNgModuleFactory`: Optional module factory to allow dynamically loading other + * module, then load a component from that module. * * ### Syntax * @@ -38,14 +40,20 @@ import {ComponentFactoryResolver, ComponentRef, Directive, Injector, Input, OnCh * * ``` * - * Customized + * Customized injector/content * ``` * + * content: contentNodesExpression;"> * * ``` * + * Customized ngModuleFactory + * ``` + * + * + * ``` * # Example * * {@example common/ngComponentOutlet/ts/module.ts region='SimpleExample'} @@ -53,34 +61,55 @@ import {ComponentFactoryResolver, ComponentRef, Directive, Injector, Input, OnCh * A more complete example with additional options: * * {@example common/ngComponentOutlet/ts/module.ts region='CompleteExample'} + + * A more complete example with ngModuleFactory: + * + * {@example common/ngComponentOutlet/ts/module.ts region='NgModuleFactoryExample'} * * @experimental */ @Directive({selector: '[ngComponentOutlet]'}) -export class NgComponentOutlet implements OnChanges { +export class NgComponentOutlet implements OnChanges, OnDestroy { @Input() ngComponentOutlet: Type; @Input() ngComponentOutletInjector: Injector; @Input() ngComponentOutletContent: any[][]; + @Input() ngComponentOutletNgModuleFactory: NgModuleFactory; - componentRef: ComponentRef; + private _componentRef: ComponentRef = null; + private _moduleRef: NgModuleRef = null; - constructor( - private _cmpFactoryResolver: ComponentFactoryResolver, - private _viewContainerRef: ViewContainerRef) {} + constructor(private _viewContainerRef: ViewContainerRef) {} ngOnChanges(changes: SimpleChanges) { - if (this.componentRef) { - this._viewContainerRef.remove(this._viewContainerRef.indexOf(this.componentRef.hostView)); + if (this._componentRef) { + this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._componentRef.hostView)); } this._viewContainerRef.clear(); - this.componentRef = null; + 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); + if ((changes as any).ngComponentOutletNgModuleFactory) { + if (this._moduleRef) this._moduleRef.destroy(); + if (this.ngComponentOutletNgModuleFactory) { + this._moduleRef = this.ngComponentOutletNgModuleFactory.create(injector); + } else { + this._moduleRef = null; + } + } + if (this._moduleRef) { + injector = this._moduleRef.injector; + } + + let componentFactory = + injector.get(ComponentFactoryResolver).resolveComponentFactory(this.ngComponentOutlet); + + this._componentRef = this._viewContainerRef.createComponent( + componentFactory, this._viewContainerRef.length, injector, this.ngComponentOutletContent); } } + ngOnDestroy() { + if (this._moduleRef) this._moduleRef.destroy(); + } } diff --git a/modules/@angular/common/test/directives/ng_component_outlet_spec.ts b/modules/@angular/common/test/directives/ng_component_outlet_spec.ts index ff78514614..24c01fe006 100644 --- a/modules/@angular/common/test/directives/ng_component_outlet_spec.ts +++ b/modules/@angular/common/test/directives/ng_component_outlet_spec.ts @@ -8,8 +8,8 @@ import {CommonModule} from '@angular/common'; import {NgComponentOutlet} from '@angular/common/src/directives/ng_component_outlet'; -import {Component, ComponentRef, Inject, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, Optional, Provider, QueryList, ReflectiveInjector, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; -import {TestBed, async} from '@angular/core/testing'; +import {Compiler, Component, ComponentRef, Inject, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleFactory, Optional, Provider, QueryList, ReflectiveInjector, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; +import {TestBed, async, fakeAsync} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/matchers'; export function main() { @@ -143,6 +143,69 @@ export function main() { fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('projected foo'); })); + + it('should resolve components from other modules, if supplied', async(() => { + const compiler = TestBed.get(Compiler) as Compiler; + let fixture = TestBed.createComponent(TestComponent); + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText(''); + + fixture.componentInstance.module = compiler.compileModuleSync(TestModule2); + fixture.componentInstance.currentComponent = Module2InjectedComponent; + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('baz'); + })); + + it('should clean up moduleRef, if supplied', async(() => { + let destroyed = false; + const compiler = TestBed.get(Compiler) as Compiler; + const fixture = TestBed.createComponent(TestComponent); + fixture.componentInstance.module = compiler.compileModuleSync(TestModule2); + fixture.componentInstance.currentComponent = Module2InjectedComponent; + fixture.detectChanges(); + + const moduleRef = fixture.componentInstance.ngComponentOutlet['_moduleRef']; + spyOn(moduleRef, 'destroy').and.callThrough(); + + expect(moduleRef.destroy).not.toHaveBeenCalled(); + fixture.destroy(); + expect(moduleRef.destroy).toHaveBeenCalled(); + })); + + it('should not re-create moduleRef when it didn\'t actually change', async(() => { + const compiler = TestBed.get(Compiler) as Compiler; + const fixture = TestBed.createComponent(TestComponent); + fixture.componentInstance.module = compiler.compileModuleSync(TestModule2); + fixture.componentInstance.currentComponent = Module2InjectedComponent; + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveText('baz'); + + const moduleRef = fixture.componentInstance.ngComponentOutlet['_moduleRef']; + fixture.componentInstance.currentComponent = Module2InjectedComponent2; + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveText('baz2'); + expect(moduleRef).toBe(fixture.componentInstance.ngComponentOutlet['_moduleRef']); + })); + + it('should re-create moduleRef when changed', async(() => { + const compiler = TestBed.get(Compiler) as Compiler; + const fixture = TestBed.createComponent(TestComponent); + fixture.componentInstance.module = compiler.compileModuleSync(TestModule2); + fixture.componentInstance.currentComponent = Module2InjectedComponent; + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveText('baz'); + + fixture.componentInstance.module = compiler.compileModuleSync(TestModule3); + fixture.componentInstance.currentComponent = Module3InjectedComponent; + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveText('bat'); + })); }); } @@ -158,15 +221,16 @@ class InjectedComponentAgain { } const TEST_CMP_TEMPLATE = - ``; + ``; @Component({selector: 'test-cmp', template: TEST_CMP_TEMPLATE}) class TestComponent { currentComponent: Type; injector: Injector; projectables: any[][]; + module: NgModuleFactory; - get cmpRef(): ComponentRef { return this.ngComponentOutlet.componentRef; } - set cmpRef(value: ComponentRef) { this.ngComponentOutlet.componentRef = value; } + get cmpRef(): ComponentRef { return this.ngComponentOutlet['_componentRef']; } + set cmpRef(value: ComponentRef) { this.ngComponentOutlet['_componentRef'] = value; } @ViewChildren(TemplateRef) tplRefs: QueryList>; @ViewChild(NgComponentOutlet) ngComponentOutlet: NgComponentOutlet; @@ -182,3 +246,33 @@ class TestComponent { }) export class TestModule { } + +@Component({selector: 'mdoule-2-injected-component', template: 'baz'}) +class Module2InjectedComponent { +} + +@Component({selector: 'mdoule-2-injected-component-2', template: 'baz2'}) +class Module2InjectedComponent2 { +} + +@NgModule({ + imports: [CommonModule], + declarations: [Module2InjectedComponent, Module2InjectedComponent2], + exports: [Module2InjectedComponent, Module2InjectedComponent2], + entryComponents: [Module2InjectedComponent, Module2InjectedComponent2] +}) +export class TestModule2 { +} + +@Component({selector: 'mdoule-3-injected-component', template: 'bat'}) +class Module3InjectedComponent { +} + +@NgModule({ + imports: [CommonModule], + declarations: [Module3InjectedComponent], + exports: [Module3InjectedComponent], + entryComponents: [Module3InjectedComponent] +}) +export class TestModule3 { +} \ No newline at end of file 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 index 57bf9d0cd7..f205f775e3 100644 --- a/modules/@angular/examples/common/ngComponentOutlet/ts/e2e_test/ngComponentOutlet_spec.ts +++ b/modules/@angular/examples/common/ngComponentOutlet/ts/e2e_test/ngComponentOutlet_spec.ts @@ -31,5 +31,13 @@ describe('ngComponentOutlet', () => { waitForElement('ng-component-outlet-complete-example'); expect(element.all(by.css('complete-component')).getText()).toEqual(['Complete: Ahoj Svet!']); }); + + it('should render other module', () => { + browser.get(URL); + waitForElement('ng-component-outlet-other-module-example'); + expect(element.all(by.css('other-module-component')).getText()).toEqual([ + 'Other Module Component!' + ]); + }); }); }); diff --git a/modules/@angular/examples/common/ngComponentOutlet/ts/module.ts b/modules/@angular/examples/common/ngComponentOutlet/ts/module.ts index f5586c50de..e56f528cea 100644 --- a/modules/@angular/examples/common/ngComponentOutlet/ts/module.ts +++ b/modules/@angular/examples/common/ngComponentOutlet/ts/module.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Injectable, Injector, NgModule, ReflectiveInjector} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Compiler, Component, Injectable, Injector, NgModule, NgModuleFactory, ReflectiveInjector} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; @@ -60,12 +61,34 @@ class NgTemplateOutletCompleteExample { } // #enddocregion +// #docregion NgModuleFactoryExample +@Component({selector: 'other-module-component', template: `Other Module Component!`}) +class OtherModuleComponent { +} + +@Component({ + selector: 'ng-component-outlet-other-module-example', + template: ` + ` +}) +class NgTemplateOutletOtherModuleExample { + // This field is necessary to expose OtherModuleComponent to the template. + OtherModuleComponent = OtherModuleComponent; + myModule: NgModuleFactory; + + constructor(compiler: Compiler) { this.myModule = compiler.compileModuleSync(OtherModule); } +} +// #enddocregion + @Component({ selector: 'example-app', template: `
- ` + +
+ ` }) class ExampleApp { } @@ -73,11 +96,19 @@ class ExampleApp { @NgModule({ imports: [BrowserModule], declarations: [ - ExampleApp, NgTemplateOutletSimpleExample, NgTemplateOutletCompleteExample, HelloWorld, - CompleteComponent + ExampleApp, NgTemplateOutletSimpleExample, NgTemplateOutletCompleteExample, + NgTemplateOutletOtherModuleExample, HelloWorld, CompleteComponent ], entryComponents: [HelloWorld, CompleteComponent], bootstrap: [ExampleApp] }) export class AppModule { } + +@NgModule({ + imports: [CommonModule], + declarations: [OtherModuleComponent], + entryComponents: [OtherModuleComponent] +}) +export class OtherModule { +} \ No newline at end of file diff --git a/tools/public_api_guard/common/index.d.ts b/tools/public_api_guard/common/index.d.ts index 6384324f1f..3419149a77 100644 --- a/tools/public_api_guard/common/index.d.ts +++ b/tools/public_api_guard/common/index.d.ts @@ -118,13 +118,14 @@ export declare class NgClass implements DoCheck { } /** @experimental */ -export declare class NgComponentOutlet implements OnChanges { - componentRef: ComponentRef; +export declare class NgComponentOutlet implements OnChanges, OnDestroy { ngComponentOutlet: Type; ngComponentOutletContent: any[][]; ngComponentOutletInjector: Injector; - constructor(_cmpFactoryResolver: ComponentFactoryResolver, _viewContainerRef: ViewContainerRef); + ngComponentOutletNgModuleFactory: NgModuleFactory; + constructor(_viewContainerRef: ViewContainerRef); ngOnChanges(changes: SimpleChanges): void; + ngOnDestroy(): void; } /** @stable */