From dcf88408314ccc384d827715c29d25f3a12c8d00 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Thu, 12 Oct 2017 12:38:29 +0300 Subject: [PATCH] feat(elements): implement `registerAsCustomElements()` closes #19469 --- packages/elements/public_api.ts | 1 + .../src/register-as-custom-elements.ts | 37 +++++ .../test/register-as-custom-elements_spec.ts | 127 ++++++++++++++++++ tools/public_api_guard/elements/elements.d.ts | 3 + 4 files changed, 168 insertions(+) create mode 100644 packages/elements/src/register-as-custom-elements.ts create mode 100644 packages/elements/test/register-as-custom-elements_spec.ts diff --git a/packages/elements/public_api.ts b/packages/elements/public_api.ts index 6bba985ec1..f502eaf43a 100644 --- a/packages/elements/public_api.ts +++ b/packages/elements/public_api.ts @@ -13,6 +13,7 @@ */ export {NgElement, NgElementWithProps} from './src/ng-element'; export {NgElementConstructor} from './src/ng-element-constructor'; +export {registerAsCustomElements} from './src/register-as-custom-elements'; export {VERSION} from './src/version'; // This file only reexports content of the `src` folder. Keep it that way. diff --git a/packages/elements/src/register-as-custom-elements.ts b/packages/elements/src/register-as-custom-elements.ts new file mode 100644 index 0000000000..0d10074183 --- /dev/null +++ b/packages/elements/src/register-as-custom-elements.ts @@ -0,0 +1,37 @@ +/** + * @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 {NgModuleFactory, NgModuleRef, PlatformRef, Type} from '@angular/core'; + +import {NgElements} from './ng-elements'; +import {isFunction} from './utils'; + +/** + * TODO(gkalpak): Add docs. + * @experimental + */ +export function registerAsCustomElements( + customElementComponents: Type[], platformRef: PlatformRef, + moduleFactory: NgModuleFactory): Promise>; +export function registerAsCustomElements( + customElementComponents: Type[], + bootstrapFn: () => Promise>): Promise>; +export function registerAsCustomElements( + customElementComponents: Type[], + platformRefOrBootstrapFn: PlatformRef | (() => Promise>), + moduleFactory?: NgModuleFactory): Promise> { + const bootstrapFn = isFunction(platformRefOrBootstrapFn) ? + platformRefOrBootstrapFn : + () => platformRefOrBootstrapFn.bootstrapModuleFactory(moduleFactory !); + + return bootstrapFn().then(moduleRef => { + const ngElements = new NgElements(moduleRef, customElementComponents); + ngElements.register(); + return moduleRef; + }); +} diff --git a/packages/elements/test/register-as-custom-elements_spec.ts b/packages/elements/test/register-as-custom-elements_spec.ts new file mode 100644 index 0000000000..85dc91715b --- /dev/null +++ b/packages/elements/test/register-as-custom-elements_spec.ts @@ -0,0 +1,127 @@ +/** + * @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 {CompilerFactory, Component, NgModule, NgModuleFactory, NgModuleRef, PlatformRef, Type, destroyPlatform} from '@angular/core'; +import {BrowserModule, platformBrowser} from '@angular/platform-browser'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {NgElementImpl} from '../src/ng-element'; +import {registerAsCustomElements} from '../src/register-as-custom-elements'; +import {isFunction} from '../src/utils'; +import {patchEnv, restoreEnv, supportsCustomElements} from '../testing/index'; + +type BootstrapFn = () => Promise>; +type ArgsWithModuleFactory = [PlatformRef, NgModuleFactory]; +type ArgsWithBootstrapFn = [BootstrapFn]; + +export function main() { + if (!supportsCustomElements()) { + return; + } + + describe('registerAsCustomElements()', () => { + const createArgsToRegisterWithModuleFactory = (platformFn: () => PlatformRef) => { + const tempPlatformRef = platformBrowserDynamic(); + const compilerFactory = tempPlatformRef.injector.get(CompilerFactory) as CompilerFactory; + const compiler = compilerFactory.createCompiler([]); + tempPlatformRef.destroy(); + + const platformRef = platformFn(); + const moduleFactory = compiler.compileModuleSync(TestModule); + + return [platformRef, moduleFactory] as ArgsWithModuleFactory; + }; + const createArgsToRegisterWithBootstrapFn = + () => [() => platformBrowserDynamic().bootstrapModule(TestModule)] as + ArgsWithBootstrapFn; + + beforeAll(() => patchEnv()); + afterAll(() => restoreEnv()); + + // Run the tests with both an `NgModuleFactory` and a `bootstrapFn()`. + runTests( + 'with `NgModuleFactory` (on `platformBrowserDynamic`)', + () => createArgsToRegisterWithModuleFactory(platformBrowserDynamic)); + runTests( + 'with `NgModuleFactory` (on `platformBrowser`)', + () => createArgsToRegisterWithModuleFactory(platformBrowser)); + runTests('with `bootstrapFn()`', createArgsToRegisterWithBootstrapFn); + + function runTests( + description: string, createArgs: () => ArgsWithModuleFactory| ArgsWithBootstrapFn) { + describe(description, () => { + const customElementComponents: Type[] = [FooBarComponent, BazQuxComponent]; + const hasBootstrapFn = (arr: any[]): arr is ArgsWithBootstrapFn => isFunction(arr[0]); + let doRegister: () => Promise>; + let defineSpy: jasmine.Spy; + + beforeEach(() => { + destroyPlatform(); + + const args = createArgs(); + doRegister = hasBootstrapFn(args) ? + () => registerAsCustomElements(customElementComponents, args[0]) : + () => registerAsCustomElements(customElementComponents, args[0], args[1]); + + defineSpy = spyOn(customElements, 'define'); + }); + + afterEach(() => destroyPlatform()); + + it('should bootstrap the `NgModule` and return an `NgModuleRef` instance', done => { + doRegister() + .then(ref => expect(ref.instance).toEqual(jasmine.any(TestModule))) + .then(done, done.fail); + }); + + it('should define a custom element for each component', done => { + doRegister() + .then(() => { + expect(defineSpy).toHaveBeenCalledTimes(2); + expect(defineSpy).toHaveBeenCalledWith('foo-bar', jasmine.any(Function)); + expect(defineSpy).toHaveBeenCalledWith('baz-qux', jasmine.any(Function)); + + expect(defineSpy.calls.argsFor(0)[1]).toEqual(jasmine.objectContaining({ + is: 'foo-bar', + observedAttributes: [], + upgrade: jasmine.any(Function), + })); + expect(defineSpy.calls.argsFor(1)[1]).toEqual(jasmine.objectContaining({ + is: 'baz-qux', + observedAttributes: [], + upgrade: jasmine.any(Function), + })); + }) + .then(done, done.fail); + }); + }); + } + }); +} + +@Component({ + selector: 'foo-bar', + template: 'FooBar', +}) +class FooBarComponent { +} + +@Component({ + selector: 'baz-qux', + template: 'BazQux', +}) +class BazQuxComponent { +} + +@NgModule({ + imports: [BrowserModule], + declarations: [FooBarComponent, BazQuxComponent], + entryComponents: [FooBarComponent, BazQuxComponent], +}) +class TestModule { + ngDoBootstrap() {} +} diff --git a/tools/public_api_guard/elements/elements.d.ts b/tools/public_api_guard/elements/elements.d.ts index 3f44258609..c8afdc824e 100644 --- a/tools/public_api_guard/elements/elements.d.ts +++ b/tools/public_api_guard/elements/elements.d.ts @@ -23,5 +23,8 @@ export interface NgElementConstructor { export declare type NgElementWithProps = NgElement & { [property in keyof +/** @experimental */ +export declare function registerAsCustomElements(customElementComponents: Type[], platformRef: PlatformRef, moduleFactory: NgModuleFactory): Promise>; + /** @experimental */ export declare const VERSION: Version;