Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(docs-infra): update loading of custom elements to use dynamic imports #30704

4 changes: 2 additions & 2 deletions aio/scripts/_payload-limits.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"aio": {
"master": {
"uncompressed": {
"runtime-es5": 2980,
"runtime-es2015": 2986,
"runtime-es5": 2516,
"runtime-es2015": 2522,
brandonroberts marked this conversation as resolved.
Show resolved Hide resolved
"main-es5": 504760,
"main-es2015": 443497,
"polyfills-es5": 128751,
Expand Down
13 changes: 6 additions & 7 deletions aio/src/app/custom-elements/custom-elements.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { NgModule, NgModuleFactoryLoader, SystemJsNgModuleLoader } from '@angular/core';
import { NgModule } from '@angular/core';
import { ROUTES} from '@angular/router';
import { ElementsLoader } from './elements-loader';
import {
ELEMENT_MODULE_PATHS,
ELEMENT_MODULE_PATHS_AS_ROUTES,
ELEMENT_MODULE_PATHS_TOKEN
ELEMENT_MODULE_LOAD_CALLBACKS,
ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES,
ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN
} from './element-registry';
import { LazyCustomElementComponent } from './lazy-custom-element.component';

Expand All @@ -13,13 +13,12 @@ import { LazyCustomElementComponent } from './lazy-custom-element.component';
exports: [ LazyCustomElementComponent ],
providers: [
ElementsLoader,
{ provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader },
{ provide: ELEMENT_MODULE_PATHS_TOKEN, useValue: ELEMENT_MODULE_PATHS },
{ 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
// registered as lazy-loadable.
// TODO(andrewjs): Provide first-class support for providing this.
{ provide: ROUTES, useValue: ELEMENT_MODULE_PATHS_AS_ROUTES, multi: true },
{ provide: ROUTES, useValue: ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES, multi: true },
],
})
export class CustomElementsModule { }
29 changes: 15 additions & 14 deletions aio/src/app/custom-elements/element-registry.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,45 @@
import { InjectionToken, Type } from '@angular/core';
import { LoadChildrenCallback } from '@angular/router';

// Modules containing custom elements must be set up as lazy-loaded routes (loadChildren)
// TODO(andrewjs): This is a hack, Angular should have first-class support for preparing a module
// that contains custom elements.
export const ELEMENT_MODULE_PATHS_AS_ROUTES = [
export const ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES = [
{
selector: 'aio-announcement-bar',
loadChildren: './announcement-bar/announcement-bar.module#AnnouncementBarModule'
loadChildren: () => import('./announcement-bar/announcement-bar.module').then(mod => mod.AnnouncementBarModule)
},
{
selector: 'aio-api-list',
loadChildren: './api/api-list.module#ApiListModule'
loadChildren: () => import('./api/api-list.module').then(mod => mod.ApiListModule)
},
{
selector: 'aio-contributor-list',
loadChildren: './contributor/contributor-list.module#ContributorListModule'
loadChildren: () => import('./contributor/contributor-list.module').then(mod => mod.ContributorListModule)
},
{
selector: 'aio-file-not-found-search',
loadChildren: './search/file-not-found-search.module#FileNotFoundSearchModule'
loadChildren: () => import('./search/file-not-found-search.module').then(mod => mod.FileNotFoundSearchModule)
},
{
selector: 'aio-resource-list',
loadChildren: './resource/resource-list.module#ResourceListModule'
loadChildren: () => import('./resource/resource-list.module').then(mod => mod.ResourceListModule)
},
{
selector: 'aio-toc',
loadChildren: './toc/toc.module#TocModule'
loadChildren: () => import('./toc/toc.module').then(mod => mod.TocModule)
},
{
selector: 'code-example',
loadChildren: './code/code-example.module#CodeExampleModule'
loadChildren: () => import('./code/code-example.module').then(mod => mod.CodeExampleModule)
},
{
selector: 'code-tabs',
loadChildren: './code/code-tabs.module#CodeTabsModule'
loadChildren: () => import('./code/code-tabs.module').then(mod => mod.CodeTabsModule)
},
{
selector: 'live-example',
loadChildren: './live-example/live-example.module#LiveExampleModule'
loadChildren: () => import('./live-example/live-example.module').then(mod => mod.LiveExampleModule)
}
];

Expand All @@ -51,10 +52,10 @@ export interface WithCustomElementComponent {
}

/** Injection token to provide the element path modules. */
export const ELEMENT_MODULE_PATHS_TOKEN = new InjectionToken('aio/elements-map');
export const ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN = new InjectionToken<Map<string, LoadChildrenCallback>>('aio/elements-map');

/** Map of possible custom element selectors to their lazy-loadable module paths. */
export const ELEMENT_MODULE_PATHS = new Map<string, string>();
ELEMENT_MODULE_PATHS_AS_ROUTES.forEach(route => {
ELEMENT_MODULE_PATHS.set(route.selector, route.loadChildren);
export const ELEMENT_MODULE_LOAD_CALLBACKS = new Map<string, LoadChildrenCallback>();
ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES.forEach(route => {
ELEMENT_MODULE_LOAD_CALLBACKS.set(route.selector, route.loadChildren);
});
43 changes: 28 additions & 15 deletions aio/src/app/custom-elements/elements-loader.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {
Compiler,
ComponentFactory,
ComponentFactoryResolver, ComponentRef, Injector, NgModuleFactory, NgModuleFactoryLoader,
ComponentFactoryResolver, ComponentRef, Injector, NgModuleFactory,
NgModuleRef,
Type
Type,
} from '@angular/core';
import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing';

import { ElementsLoader } from './elements-loader';
import { ELEMENT_MODULE_PATHS_TOKEN, WithCustomElementComponent } from './element-registry';
import { ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN, WithCustomElementComponent } from './element-registry';


interface Deferred {
Expand All @@ -17,20 +18,25 @@ interface Deferred {

describe('ElementsLoader', () => {
let elementsLoader: ElementsLoader;
let compiler: Compiler;

beforeEach(() => {
const injector = TestBed.configureTestingModule({
providers: [
ElementsLoader,
{ provide: NgModuleFactoryLoader, useClass: FakeModuleFactoryLoader },
{ provide: ELEMENT_MODULE_PATHS_TOKEN, useValue: new Map([
['element-a-selector', 'element-a-module-path'],
['element-b-selector', 'element-b-module-path']
{
provide: ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN, useValue: new Map<
string, () => Promise<NgModuleFactory<WithCustomElementComponent> | Type<WithCustomElementComponent>>
>([
['element-a-selector', () => Promise.resolve(new FakeModuleFactory('element-a-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()', () => {
Expand Down Expand Up @@ -148,7 +154,7 @@ describe('ElementsLoader', () => {

// Verify the right component was loaded/registered.
const Ctor = definedSpy.calls.argsFor(0)[1];
expect(Ctor.observedAttributes).toEqual(['element-a-module-path']);
expect(Ctor.observedAttributes).toEqual(['element-a-module']);
}));

it('should wait until the element is defined', fakeAsync(() => {
Expand Down Expand Up @@ -222,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);
}));
});
});

Expand Down Expand Up @@ -282,13 +302,6 @@ class FakeModuleFactory extends NgModuleFactory<any> {
}
}

class FakeModuleFactoryLoader extends NgModuleFactoryLoader {
load(modulePath: string): Promise<NgModuleFactory<any>> {
const fakeModuleFactory = new FakeModuleFactory(modulePath);
return Promise.resolve(fakeModuleFactory);
}
}

function returnPromisesFromSpy(spy: jasmine.Spy): Deferred[] {
const deferreds: Deferred[] = [];
spy.and.callFake(() => new Promise((resolve, reject) => deferreds.push({resolve, reject})));
Expand Down
35 changes: 26 additions & 9 deletions aio/src/app/custom-elements/elements-loader.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import {
Compiler,
brandonroberts marked this conversation as resolved.
Show resolved Hide resolved
Inject,
Injectable,
NgModuleFactoryLoader,
NgModuleFactory,
brandonroberts marked this conversation as resolved.
Show resolved Hide resolved
NgModuleRef,
Type,
} from '@angular/core';
import { ELEMENT_MODULE_PATHS_TOKEN } from './element-registry';
import { ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN, WithCustomElementComponent } from './element-registry';
import { from, Observable, of } from 'rxjs';
import { createCustomElement } from '@angular/elements';
import { LoadChildrenCallback } from '@angular/router';


@Injectable()
export class ElementsLoader {
/** Map of unregistered custom elements and their respective module paths to load. */
private elementsToLoad: Map<string, string>;
private elementsToLoad: Map<string, LoadChildrenCallback>;
/** Map of custom elements that are in the process of being loaded and registered. */
private elementsLoading = new Map<string, Promise<void>>();

constructor(private moduleFactoryLoader: NgModuleFactoryLoader,
private moduleRef: NgModuleRef<any>,
@Inject(ELEMENT_MODULE_PATHS_TOKEN) elementModulePaths: Map<string, string>) {
constructor(private moduleRef: NgModuleRef<any>,
@Inject(ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN) elementModulePaths: Map<string, LoadChildrenCallback>,
private compiler: Compiler) {
this.elementsToLoad = new Map(elementModulePaths);
}

Expand Down Expand Up @@ -47,9 +50,23 @@ export class ElementsLoader {

if (this.elementsToLoad.has(selector)) {
// Load and register the custom element (for the first time).
const modulePath = this.elementsToLoad.get(selector)!;
const loadedAndRegistered = this.moduleFactoryLoader
.load(modulePath)
const modulePathLoader = this.elementsToLoad.get(selector)!;
const loadedAndRegistered =
(modulePathLoader() as Promise<NgModuleFactory<WithCustomElementComponent> | Type<WithCustomElementComponent>>)
.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;
Expand Down