From 32886cf9ace539e14e2b387cd8afb10715c8d3de Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 30 May 2019 11:55:59 -0500 Subject: [PATCH] docs(docs-infra): compile module first for Ivy when loading element modules (#30704) In View Engine, NgModule factories are created for each NgModule and loaded when the module is requested. Ivy doesn't generate the factories by design and only loads the module class, so it must be compiled after being loaded. PR Close #30704 --- .../custom-elements/custom-elements.module.ts | 3 +-- .../custom-elements/elements-loader.spec.ts | 27 ++++++++++++++++--- .../app/custom-elements/elements-loader.ts | 22 +++++++++++++-- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/aio/src/app/custom-elements/custom-elements.module.ts b/aio/src/app/custom-elements/custom-elements.module.ts index 2975720d801f0..064dfb8e40cb9 100644 --- a/aio/src/app/custom-elements/custom-elements.module.ts +++ b/aio/src/app/custom-elements/custom-elements.module.ts @@ -1,4 +1,4 @@ -import { NgModule, NgModuleFactoryLoader, SystemJsNgModuleLoader } from '@angular/core'; +import { NgModule } from '@angular/core'; import { ROUTES} from '@angular/router'; import { ElementsLoader } from './elements-loader'; import { @@ -13,7 +13,6 @@ import { LazyCustomElementComponent } from './lazy-custom-element.component'; exports: [ LazyCustomElementComponent ], providers: [ ElementsLoader, - { provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader }, { provide: ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN, useValue: ELEMENT_MODULE_LOAD_CALLBACKS }, // Providing these routes as a signal to the build system that these modules should be diff --git a/aio/src/app/custom-elements/elements-loader.spec.ts b/aio/src/app/custom-elements/elements-loader.spec.ts index d2ccf2878053a..36185a2067193 100644 --- a/aio/src/app/custom-elements/elements-loader.spec.ts +++ b/aio/src/app/custom-elements/elements-loader.spec.ts @@ -1,8 +1,9 @@ import { + Compiler, ComponentFactory, ComponentFactoryResolver, ComponentRef, Injector, NgModuleFactory, NgModuleRef, - Type + Type, } from '@angular/core'; import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; @@ -17,19 +18,25 @@ interface Deferred { describe('ElementsLoader', () => { let elementsLoader: ElementsLoader; + let compiler: Compiler; beforeEach(() => { const injector = TestBed.configureTestingModule({ providers: [ ElementsLoader, - { provide: ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN, useValue: new Map Promise>>([ + { + provide: ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN, useValue: new Map< + string, () => Promise | Type> + >([ ['element-a-selector', () => Promise.resolve(new FakeModuleFactory('element-a-module'))], - ['element-b-selector', () => Promise.resolve(new FakeModuleFactory('element-b-module'))] + ['element-b-selector', () => Promise.resolve(new FakeModuleFactory('element-b-module'))], + ['element-c-selector', () => Promise.resolve(FakeCustomElementModule)] ])}, ] }); elementsLoader = injector.get(ElementsLoader); + compiler = injector.get(Compiler); }); describe('loadContainedCustomElements()', () => { @@ -221,6 +228,20 @@ describe('ElementsLoader', () => { expect(definedSpy).toHaveBeenCalledTimes(1); }) ); + + it('should be able to load and register an element after compiling its NgModule', fakeAsync(() => { + const compilerSpy = spyOn(compiler, 'compileModuleAsync') + .and.returnValue(Promise.resolve(new FakeModuleFactory('element-c-module'))); + + elementsLoader.loadCustomElement('element-c-selector'); + flushMicrotasks(); + + expect(definedSpy).toHaveBeenCalledTimes(1); + expect(definedSpy).toHaveBeenCalledWith('element-c-selector', jasmine.any(Function)); + + expect(compilerSpy).toHaveBeenCalledTimes(1); + expect(compilerSpy).toHaveBeenCalledWith(FakeCustomElementModule); + })); }); }); diff --git a/aio/src/app/custom-elements/elements-loader.ts b/aio/src/app/custom-elements/elements-loader.ts index 7156164a16dd7..f06894082bb44 100644 --- a/aio/src/app/custom-elements/elements-loader.ts +++ b/aio/src/app/custom-elements/elements-loader.ts @@ -1,8 +1,10 @@ import { + Compiler, Inject, Injectable, NgModuleFactory, NgModuleRef, + Type, } from '@angular/core'; import { ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN, WithCustomElementComponent } from './element-registry'; import { from, Observable, of } from 'rxjs'; @@ -18,7 +20,8 @@ export class ElementsLoader { private elementsLoading = new Map>(); constructor(private moduleRef: NgModuleRef, - @Inject(ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN) elementModulePaths: Map) { + @Inject(ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN) elementModulePaths: Map, + private compiler: Compiler) { this.elementsToLoad = new Map(elementModulePaths); } @@ -48,7 +51,22 @@ export class ElementsLoader { if (this.elementsToLoad.has(selector)) { // Load and register the custom element (for the first time). const modulePathLoader = this.elementsToLoad.get(selector)!; - const loadedAndRegistered = (modulePathLoader() as Promise>) + const loadedAndRegistered = + (modulePathLoader() as Promise | Type>) + .then(elementModuleOrFactory => { + /** + * With View Engine, the NgModule factory is created and provided when loaded. + * With Ivy, only the NgModule class is provided loaded and must be compiled. + * This uses the same mechanism as the deprecated `SystemJsNgModuleLoader` in + * in `packages/core/src/linker/system_js_ng_module_factory_loader.ts` + * to pass on the NgModuleFactory, or compile the NgModule and return its NgModuleFactory. + */ + if (elementModuleOrFactory instanceof NgModuleFactory) { + return elementModuleOrFactory; + } else { + return this.compiler.compileModuleAsync(elementModuleOrFactory); + } + }) .then(elementModuleFactory => { const elementModuleRef = elementModuleFactory.create(this.moduleRef.injector); const injector = elementModuleRef.injector;