Skip to content

Commit a0aee92

Browse files
devversionjelbourn
authored andcommitted
build: support for lazily loading examples in dev-app or docs
Adds support for lazily loading examples in the dev-app, e2e-app or eventually in the docs. Since we already have modules per component and use Ivy in the docs, we can lazily load the examples and also get rid of the large bulk `ExampleModule`.
1 parent 6baffb3 commit a0aee92

13 files changed

+121
-158
lines changed

src/bazel-tsconfig-build.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
"importHelpers": true,
2121
"strictBindCallApply": true,
2222
"newLine": "lf",
23-
"module": "es2015",
23+
// Bazel either uses "umd" or "esnext". We replicate this here for IDE support.
24+
// https://github.com/bazelbuild/rules_typescript/blob/master/internal/common/tsconfig.bzl#L199
25+
"module": "esnext",
2426
"moduleResolution": "node",
2527
"sourceMap": true,
2628
"inlineSources": true,

src/components-examples/example-data.ts

+10-18
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
import {EXAMPLE_COMPONENTS} from './example-module';
44

55
/**
6-
* Example data with information about component name, selector, files used in example, and path to
7-
* examples.
6+
* Example data with information about component name, selector, files used in
7+
* example, and path to examples.
88
*/
99
export class ExampleData {
10-
1110
/** Description of the example. */
1211
description: string;
1312

@@ -20,31 +19,24 @@ export class ExampleData {
2019
/** Name of the file that contains the example component. */
2120
indexFilename: string;
2221

23-
/**
24-
* Name of the example component. For examples with multiple components, this property will
25-
* include multiple components that are comma separated (e.g. dialog-overview)
26-
*/
27-
componentName: string;
22+
/** Names of the components being used in this example. */
23+
componentNames: string[];
2824

2925
constructor(example: string) {
3026
if (!example || !EXAMPLE_COMPONENTS.hasOwnProperty(example)) {
3127
return;
3228
}
3329

34-
const exampleConfig = EXAMPLE_COMPONENTS[example];
30+
const {componentName, additionalFiles, additionalComponents, title} =
31+
EXAMPLE_COMPONENTS[example];
32+
const exampleName = example.replace(/(?:^\w|\b\w)/g, letter => letter.toUpperCase());
3533

3634
// TODO(tinayuangao): Do not hard-code extensions
3735
this.exampleFiles = ['html', 'ts', 'css'].map(extension => `${example}-example.${extension}`);
3836
this.selectorName = this.indexFilename = `${example}-example`;
3937

40-
if (exampleConfig.additionalFiles) {
41-
this.exampleFiles.push(...exampleConfig.additionalFiles);
42-
}
43-
44-
const exampleName = example.replace(/(?:^\w|\b\w)/g, letter => letter.toUpperCase());
45-
46-
this.description = exampleConfig.title || exampleName.replace(/[\-]+/g, ' ') + ' Example';
47-
this.componentName = exampleConfig.selectorName ||
48-
exampleName.replace(/[\-]+/g, '') + 'Example';
38+
this.exampleFiles.push(...additionalFiles);
39+
this.description = title || exampleName.replace(/[\-]+/g, ' ') + ' Example';
40+
this.componentNames = [componentName, ...additionalComponents];
4941
}
5042
}
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {ComponentFactory, Injector, NgModuleFactory, Type} from '@angular/core';
10+
import {EXAMPLE_COMPONENTS} from './example-module';
11+
12+
/** Asynchronously loads the specified example and returns its component factory. */
13+
export async function loadExampleFactory(name: string, injector: Injector)
14+
: Promise<ComponentFactory<any>> {
15+
const {componentName, module} = EXAMPLE_COMPONENTS[name];
16+
// TODO(devversion): remove the NgFactory import when the `--config=view-engine` switch is gone.
17+
const [moduleFactoryExports, moduleExports] = await Promise.all([
18+
import(module.importSpecifier + '/index.ngfactory'),
19+
import(module.importSpecifier)
20+
]);
21+
const moduleFactory: NgModuleFactory<any> = moduleFactoryExports[`${module.name}NgFactory`];
22+
const componentType: Type<any> = moduleExports[componentName];
23+
return moduleFactory.create(injector)
24+
.componentFactoryResolver.resolveComponentFactory(componentType);
25+
}
26+

src/components-examples/public-api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './example-data';
2+
export * from './load-example';
23

34
// The example-module file will be auto-generated. As soon as the
45
// examples are being compiled, the module file will be generated.

src/dev-app/example/example-module.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,13 @@
99
import {CommonModule} from '@angular/common';
1010
import {NgModule} from '@angular/core';
1111
import {MatExpansionModule} from '@angular/material/expansion';
12-
import {ExampleModule as DocsExampleModule} from '@angular/components-examples';
1312
import {Example} from './example';
1413

1514

1615
import {ExampleList} from './example-list';
1716

1817
@NgModule({
19-
imports: [CommonModule, MatExpansionModule, DocsExampleModule],
18+
imports: [CommonModule, MatExpansionModule],
2019
declarations: [Example, ExampleList],
2120
exports: [Example, ExampleList]
2221
})

src/dev-app/example/example.ts

+7-17
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77
*/
88

99
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
10-
import {Component, ElementRef, Injector, Input, OnInit} from '@angular/core';
11-
import {EXAMPLE_COMPONENTS} from '@angular/components-examples';
12-
import {createCustomElement} from '@angular/elements';
10+
import {EXAMPLE_COMPONENTS, loadExampleFactory} from '@angular/components-examples';
11+
import {Component, Injector, Input, OnInit, ViewContainerRef} from '@angular/core';
1312

1413
@Component({
1514
selector: 'material-example',
@@ -55,21 +54,12 @@ export class Example implements OnInit {
5554

5655
title: string;
5756

58-
constructor(private _elementRef: ElementRef<HTMLElement>, private _injector: Injector) { }
57+
constructor(private _injector: Injector,
58+
private _viewContainerRef: ViewContainerRef) {}
5959

60-
ngOnInit() {
61-
let exampleElementCtor = customElements.get(this.id);
62-
63-
if (!exampleElementCtor) {
64-
exampleElementCtor = createCustomElement(EXAMPLE_COMPONENTS[this.id].component, {
65-
injector: this._injector
66-
});
67-
68-
customElements.define(this.id, exampleElementCtor);
69-
}
70-
71-
this._elementRef.nativeElement.appendChild(new exampleElementCtor(this._injector));
72-
this.title = EXAMPLE_COMPONENTS[this.id] ? EXAMPLE_COMPONENTS[this.id].title : '';
60+
async ngOnInit() {
61+
this.title = EXAMPLE_COMPONENTS[this.id].title;
62+
this._viewContainerRef.createComponent(await loadExampleFactory(this.id, this._injector));
7363
}
7464

7565
static ngAcceptInputType_showLabel: BooleanInput;

src/e2e-app/devserver-configure.js

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ require.config({
44
paths: {
55
'moment': 'moment/min/moment.min',
66

7+
// Support for lazy-loading of component examples.
8+
'@angular/components-examples': 'angular_material/src/components-examples',
9+
710
// MDC Web
811
'@material/animation': '@material/animation/dist/mdc.animation',
912
'@material/auto-init': '@material/auto-init/dist/mdc.autoInit',

src/e2e-app/example-viewer/example-viewer-module.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@
88

99
import {CommonModule} from '@angular/common';
1010
import {NgModule} from '@angular/core';
11-
import {ExampleModule as ExampleDataModule} from '@angular/components-examples';
1211
import {ExampleListViewer} from './example-list-viewer.component';
1312
import {ExampleViewer} from './example-viewer';
1413

1514
@NgModule({
16-
imports: [CommonModule, ExampleDataModule],
15+
imports: [CommonModule],
1716
declarations: [ExampleViewer, ExampleListViewer],
1817
exports: [ExampleViewer, ExampleListViewer]
1918
})

src/e2e-app/example-viewer/example-viewer.ts

+6-15
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Component, ElementRef, Injector, Input, OnInit} from '@angular/core';
10-
import {createCustomElement} from '@angular/elements';
11-
import {EXAMPLE_COMPONENTS} from '@angular/components-examples';
9+
import {loadExampleFactory} from '@angular/components-examples';
10+
import {Component, Injector, Input, OnInit, ViewContainerRef} from '@angular/core';
1211

1312
/** Loads an example component from `@angular/components-examples` */
1413
@Component({
@@ -23,18 +22,10 @@ export class ExampleViewer implements OnInit {
2322
/** ID of the material example to display. */
2423
@Input() id: string;
2524

26-
constructor(private _elementRef: ElementRef<HTMLElement>, private _injector: Injector) {}
25+
constructor(private _injector: Injector,
26+
private _viewContainerRef: ViewContainerRef) {}
2727

28-
ngOnInit() {
29-
let exampleElementCtor = customElements.get(this.id);
30-
31-
if (!exampleElementCtor) {
32-
exampleElementCtor =
33-
createCustomElement(EXAMPLE_COMPONENTS[this.id].component, {injector: this._injector});
34-
35-
customElements.define(this.id, exampleElementCtor);
36-
}
37-
38-
this._elementRef.nativeElement.appendChild(new exampleElementCtor(this._injector));
28+
async ngOnInit() {
29+
this._viewContainerRef.createComponent(await loadExampleFactory(this.id, this._injector));
3930
}
4031
}
+16-33
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,26 @@
11
/**
22
******************************************************************************
33
* DO NOT MANUALLY EDIT THIS FILE. THIS FILE IS AUTOMATICALLY GENERATED.
4-
*
5-
* IMPORTANT: Due to a bug with the bazel ng_module rule, this file is now
6-
* checked into the repository. Please rebuild the example module by running
7-
* the following command: "yarn gulp build-examples-module"
8-
*
9-
* BUG: https://github.com/angular/angular/issues/30259
104
******************************************************************************/
115

12-
import {NgModule} from '@angular/core';
13-
import {MAT_FORM_FIELD_DEFAULT_OPTIONS} from '@angular/material/form-field';
14-
15-
${exampleImports}
16-
176
export interface LiveExample {
7+
/** Title of the example. */
188
title: string;
19-
component: any;
20-
additionalFiles?: string[];
21-
selectorName?: string;
9+
/** Name of the example component. */
10+
componentName: string;
11+
/** List of additional components which are part of the example. */
12+
additionalComponents: string[];
13+
/** List of additional files which are part of the example. */
14+
additionalFiles: string[];
15+
/** NgModule that declares this example. */
16+
module: NgModuleInfo;
2217
}
2318

24-
export const EXAMPLE_COMPONENTS: {[key: string]: LiveExample} = ${exampleComponents};
25-
26-
export const EXAMPLE_MODULES = ${exampleModules};
27-
28-
export const EXAMPLE_LIST = [${exampleList}];
29-
30-
// Default MatFormField appearance to 'fill' as that is the new recommended approach and the
31-
// `legacy` and `standard` appearances are scheduled for deprecation in version 10.
32-
@NgModule({
33-
imports: EXAMPLE_MODULES,
34-
exports: EXAMPLE_MODULES,
35-
providers: [
36-
{ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'fill' } },
37-
],
38-
})
39-
export class ExampleModule { }
19+
export interface NgModuleInfo {
20+
/** Name of the NgModule. */
21+
name: string;
22+
/** Import specifier that resolves to this NgModule. */
23+
importSpecifier: string;
24+
}
4025

41-
// Export all individual example components because ngtsc requires all
42-
// referenced components to be available in the top-level entry-point.
43-
export {${exampleList}}
26+
export const EXAMPLE_COMPONENTS: {[key: string]: LiveExample} = ${exampleComponents};

0 commit comments

Comments
 (0)