From 19368085aad53ad97b96e675b3e7ac9fc1fd6b8d Mon Sep 17 00:00:00 2001 From: Andrew Seguin Date: Thu, 1 Mar 2018 22:34:21 -0800 Subject: [PATCH] feat(elements): provide type, not factory; remove config need (#22413) PR Close #22413 --- .../custom-elements/elements-loader.spec.ts | 8 ++- .../app/custom-elements/elements-loader.ts | 24 +++------ aio/yarn.lock | 6 +-- packages/elements/public_api.ts | 1 - .../src/component-factory-strategy.ts | 21 -------- .../elements/src/ng-element-constructor.ts | 50 ++++++++++++++----- .../test/component-factory-strategy_spec.ts | 10 +--- .../test/ng-element-constructor_spec.ts | 21 +++----- tools/public_api_guard/elements/elements.d.ts | 39 ++------------- 9 files changed, 65 insertions(+), 115 deletions(-) diff --git a/aio/src/app/custom-elements/elements-loader.spec.ts b/aio/src/app/custom-elements/elements-loader.spec.ts index 58c9175908..1f4028cdf6 100644 --- a/aio/src/app/custom-elements/elements-loader.spec.ts +++ b/aio/src/app/custom-elements/elements-loader.spec.ts @@ -111,11 +111,15 @@ class FakeComponentFactoryResolver extends ComponentFactoryResolver { } class FakeModuleRef extends NgModuleRef { - injector: Injector; + injector = jasmine.createSpyObj('injector', ['get']); componentFactoryResolver = new FakeComponentFactoryResolver(this.modulePath); instance: WithCustomElementComponent = new FakeCustomElementModule(); - constructor(private modulePath) { super(); } + constructor(private modulePath) { + super(); + + this.injector.get.and.returnValue(this.componentFactoryResolver); + } destroy() {} onDestroy(callback: () => void) {} diff --git a/aio/src/app/custom-elements/elements-loader.ts b/aio/src/app/custom-elements/elements-loader.ts index 2216c74079..7a951f506c 100644 --- a/aio/src/app/custom-elements/elements-loader.ts +++ b/aio/src/app/custom-elements/elements-loader.ts @@ -1,15 +1,14 @@ import { - ComponentFactory, Inject, Injectable, NgModuleFactoryLoader, NgModuleRef, } from '@angular/core'; -import { ELEMENT_MODULE_PATHS_TOKEN, WithCustomElementComponent } from './element-registry'; +import { ELEMENT_MODULE_PATHS_TOKEN } from './element-registry'; import { of } from 'rxjs/observable/of'; import { Observable } from 'rxjs/Observable'; import { fromPromise } from 'rxjs/observable/fromPromise'; -import { createNgElementConstructor, getConfigFromComponentFactory } from '@angular/elements'; +import { createNgElementConstructor } from '@angular/elements'; @Injectable() export class ElementsLoader { @@ -45,12 +44,10 @@ export class ElementsLoader { return this.moduleFactoryLoader.load(modulePath).then(elementModuleFactory => { if (!this.elementsToLoad.has(selector)) { return; } - const injector = this.moduleRef.injector; - const elementModuleRef = elementModuleFactory.create(injector); - const componentFactory = this.getCustomElementComponentFactory(elementModuleRef); - - const ngElementConfig = getConfigFromComponentFactory(componentFactory, injector); - const NgElement = createNgElementConstructor(ngElementConfig); + const elementModuleRef = elementModuleFactory.create(this.moduleRef.injector); + const CustomElementComponent = elementModuleRef.instance.customElementComponent; + const NgElement = + createNgElementConstructor(CustomElementComponent, {injector: elementModuleRef.injector}); customElements!.define(selector, NgElement); this.elementsToLoad.delete(selector); @@ -58,13 +55,4 @@ export class ElementsLoader { return customElements.whenDefined(selector); }); } - - /** Gets the component factory of the custom element defined on the NgModuleRef. */ - private getCustomElementComponentFactory( - customElementModuleRef: NgModuleRef): ComponentFactory { - const resolver = customElementModuleRef.componentFactoryResolver; - const customElementComponent = customElementModuleRef.instance.customElementComponent; - - return resolver.resolveComponentFactory(customElementComponent); - } } diff --git a/aio/yarn.lock b/aio/yarn.lock index 05c477a53e..7550858987 100644 --- a/aio/yarn.lock +++ b/aio/yarn.lock @@ -131,7 +131,7 @@ tslib "^1.7.1" "@angular/elements@file:../dist/packages-dist/elements": - version "6.0.0-beta.5-8531ff3335" + version "6.0.0-beta.5-0968e9f16a" dependencies: tslib "^1.7.1" @@ -340,10 +340,6 @@ version "1.0.8" resolved "https://registry.yarnpkg.com/@webcomponents/custom-elements/-/custom-elements-1.0.8.tgz#b7b8ef7248f7681d1ad4286a0ada5fe3c2bc7228" -"@webcomponents/webcomponentsjs@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-1.1.0.tgz#1392799c266fca142622a720176f688beb74d181" - JSONStream@^1.2.1: version "1.3.1" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a" diff --git a/packages/elements/public_api.ts b/packages/elements/public_api.ts index 83a55578e7..5f72c0d16b 100644 --- a/packages/elements/public_api.ts +++ b/packages/elements/public_api.ts @@ -11,7 +11,6 @@ * @description * Entry point for all public APIs of the `elements` package. */ -export {ComponentFactoryNgElementStrategy, ComponentFactoryNgElementStrategyFactory, getConfigFromComponentFactory} from './src/component-factory-strategy'; export {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from './src/element-strategy'; export {NgElement, NgElementConfig, NgElementConstructor, createNgElementConstructor} from './src/ng-element-constructor'; export {VERSION} from './src/version'; diff --git a/packages/elements/src/component-factory-strategy.ts b/packages/elements/src/component-factory-strategy.ts index 33f05bccfe..ec443a142a 100644 --- a/packages/elements/src/component-factory-strategy.ts +++ b/packages/elements/src/component-factory-strategy.ts @@ -18,27 +18,6 @@ import {camelToDashCase, isFunction, scheduler, strictEquals} from './utils'; /** Time in milliseconds to wait before destroying the component ref when disconnected. */ const DESTROY_DELAY = 10; -/** - * Creates an NgElementConfig based on the provided component factory and injector. By default, - * the observed attributes on the NgElement will be the kebab-case version of the component inputs. - * - * @experimental - */ -export function getConfigFromComponentFactory( - componentFactory: ComponentFactory, injector: Injector) { - const attributeToPropertyInputs = new Map(); - componentFactory.inputs.forEach(({propName, templateName}) => { - const attr = camelToDashCase(templateName); - attributeToPropertyInputs.set(attr, propName); - }); - - return { - strategyFactory: new ComponentFactoryNgElementStrategyFactory(componentFactory, injector), - propertyInputs: componentFactory.inputs.map(({propName}) => propName), - attributeToPropertyInputs, - }; -} - /** * Factory that creates new ComponentFactoryNgElementStrategy instances with the strategy factory's * injector. A new strategy instance is created with the provided component factory which will diff --git a/packages/elements/src/ng-element-constructor.ts b/packages/elements/src/ng-element-constructor.ts index 025d7a68d1..38cec8c63c 100644 --- a/packages/elements/src/ng-element-constructor.ts +++ b/packages/elements/src/ng-element-constructor.ts @@ -6,10 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ +import {ComponentFactoryResolver, Injector, Type} from '@angular/core'; import {Subscription} from 'rxjs/Subscription'; +import {ComponentFactoryNgElementStrategyFactory} from './component-factory-strategy'; import {NgElementStrategy, NgElementStrategyFactory} from './element-strategy'; -import {createCustomEvent} from './utils'; +import {camelToDashCase, createCustomEvent} from './utils'; /** * Class constructor based on an Angular Component to be used for custom element registration. @@ -54,9 +56,20 @@ export type WithProperties

= { * @experimental */ export interface NgElementConfig { - strategyFactory: NgElementStrategyFactory; - propertyInputs: string[]; - attributeToPropertyInputs: Map; + injector: Injector; + strategyFactory?: NgElementStrategyFactory; + propertyInputs?: string[]; + attributeToPropertyInputs?: Map; +} + +/** Gets a map of default set of attributes to observe and the properties they affect. */ +function getDefaultAttributeToPropertyInputs(inputs: {propName: string, templateName: string}[]) { + const attributeToPropertyInputs = new Map(); + inputs.forEach(({propName, templateName}) => { + attributeToPropertyInputs.set(camelToDashCase(templateName), propName); + }); + + return attributeToPropertyInputs; } /** @@ -73,28 +86,40 @@ export interface NgElementConfig { * * @experimental */ -export function createNgElementConstructor

(config: NgElementConfig): NgElementConstructor

{ +export function createNgElementConstructor

( + component: Type, config: NgElementConfig): NgElementConstructor

{ + const componentFactoryResolver = + config.injector.get(ComponentFactoryResolver) as ComponentFactoryResolver; + const componentFactory = componentFactoryResolver.resolveComponentFactory(component); + const inputs = componentFactory.inputs; + + const defaultStrategyFactory = config.strategyFactory || + new ComponentFactoryNgElementStrategyFactory(componentFactory, config.injector); + + const attributeToPropertyInputs = + config.attributeToPropertyInputs || getDefaultAttributeToPropertyInputs(inputs); + class NgElementImpl extends NgElement { - static readonly observedAttributes = Array.from(config.attributeToPropertyInputs.keys()); + static readonly observedAttributes = Array.from(attributeToPropertyInputs.keys()); constructor(strategyFactoryOverride?: NgElementStrategyFactory) { super(); // Use the constructor's strategy factory override if it is present, otherwise default to // the config's factory. - const strategyFactory = strategyFactoryOverride || config.strategyFactory; + const strategyFactory = strategyFactoryOverride || defaultStrategyFactory; this.ngElementStrategy = strategyFactory.create(); } attributeChangedCallback( attrName: string, oldValue: string|null, newValue: string, namespace?: string): void { - const propName = config.attributeToPropertyInputs.get(attrName) !; + const propName = attributeToPropertyInputs.get(attrName) !; this.ngElementStrategy.setPropertyValue(propName, newValue); } connectedCallback(): void { // Take element attribute inputs and set them as inputs on the strategy - config.attributeToPropertyInputs.forEach((propName, attrName) => { + attributeToPropertyInputs.forEach((propName, attrName) => { const value = this.getAttribute(attrName); if (value) { this.ngElementStrategy.setPropertyValue(propName, value); @@ -120,9 +145,10 @@ export function createNgElementConstructor

(config: NgElementConfig): NgElemen } } - // Add getters and setters for each input defined on the Angular Component so that the input - // changes can be known. - config.propertyInputs.forEach(property => { + // Add getters and setters to the prototype for each property input. If the config does not + // contain property inputs, use all inputs by default. + const propertyInputs = config.propertyInputs || inputs.map(({propName}) => propName); + propertyInputs.forEach(property => { Object.defineProperty(NgElementImpl.prototype, property, { get: function() { return this.ngElementStrategy.getPropertyValue(property); }, set: function(newValue: any) { this.ngElementStrategy.setPropertyValue(property, newValue); }, diff --git a/packages/elements/test/component-factory-strategy_spec.ts b/packages/elements/test/component-factory-strategy_spec.ts index 5f035aa6d9..95ab423d40 100644 --- a/packages/elements/test/component-factory-strategy_spec.ts +++ b/packages/elements/test/component-factory-strategy_spec.ts @@ -10,7 +10,7 @@ import {ComponentFactory, ComponentRef, Injector, NgModuleRef, SimpleChange, Sim import {fakeAsync, tick} from '@angular/core/testing'; import {Subject} from 'rxjs/Subject'; -import {ComponentFactoryNgElementStrategy, ComponentFactoryNgElementStrategyFactory, getConfigFromComponentFactory} from '../src/component-factory-strategy'; +import {ComponentFactoryNgElementStrategy, ComponentFactoryNgElementStrategyFactory} from '../src/component-factory-strategy'; import {NgElementStrategyEvent} from '../src/element-strategy'; describe('ComponentFactoryNgElementStrategy', () => { @@ -32,14 +32,6 @@ describe('ComponentFactoryNgElementStrategy', () => { strategy = new ComponentFactoryNgElementStrategy(factory, injector); }); - it('should generate a default config for NgElement', () => { - let config = getConfigFromComponentFactory(factory, injector); - expect(config.strategyFactory).toBeTruthy(); - expect(config.propertyInputs).toEqual(['fooFoo', 'barBar']); - expect(config.attributeToPropertyInputs.get('foo-foo')).toBe('fooFoo'); - expect(config.attributeToPropertyInputs.get('my-bar-bar')).toBe('barBar'); - }); - it('should create a new strategy from the factory', () => { const strategyFactory = new ComponentFactoryNgElementStrategyFactory(factory, injector); expect(strategyFactory.create()).toBeTruthy(); diff --git a/packages/elements/test/ng-element-constructor_spec.ts b/packages/elements/test/ng-element-constructor_spec.ts index a7e9114a96..adeb25783c 100644 --- a/packages/elements/test/ng-element-constructor_spec.ts +++ b/packages/elements/test/ng-element-constructor_spec.ts @@ -6,13 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, ComponentFactory, EventEmitter, Input, NgModule, Output, destroyPlatform} from '@angular/core'; +import {Component, EventEmitter, Injector, Input, NgModule, Output, destroyPlatform} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {Subject} from 'rxjs/Subject'; import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from '../src/element-strategy'; -import {NgElementConfig, NgElementConstructor, createNgElementConstructor} from '../src/ng-element-constructor'; +import {NgElementConstructor, createNgElementConstructor} from '../src/ng-element-constructor'; import {patchEnv, restoreEnv} from '../testing/index'; type WithFooBar = { @@ -23,9 +23,9 @@ type WithFooBar = { if (typeof customElements !== 'undefined') { describe('createNgElementConstructor', () => { let NgElementCtor: NgElementConstructor; - let factory: ComponentFactory; let strategy: TestStrategy; let strategyFactory: TestStrategyFactory; + let injector: Injector; beforeAll(() => patchEnv()); beforeAll(done => { @@ -33,17 +33,11 @@ if (typeof customElements !== 'undefined') { platformBrowserDynamic() .bootstrapModule(TestModule) .then(ref => { - factory = ref.componentFactoryResolver.resolveComponentFactory(TestComponent); + injector = ref.injector; strategyFactory = new TestStrategyFactory(); strategy = strategyFactory.testStrategy; - const config: NgElementConfig = { - strategyFactory, - propertyInputs: ['fooFoo', 'barBar'], - attributeToPropertyInputs: - new Map([['foo-foo', 'fooFoo'], ['barbar', 'barBar']]) - }; - NgElementCtor = createNgElementConstructor(config); + NgElementCtor = createNgElementConstructor(TestComponent, {injector, strategyFactory}); // The `@webcomponents/custom-elements/src/native-shim.js` polyfill allows us to create // new instances of the NgElement which extends HTMLElement, as long as we define it. @@ -110,8 +104,9 @@ if (typeof customElements !== 'undefined') { beforeAll(() => { strategyFactory = new TestStrategyFactory(); strategy = strategyFactory.testStrategy; - NgElementCtorWithChangedAttr = createNgElementConstructor({ - strategyFactory: strategyFactory, + NgElementCtorWithChangedAttr = createNgElementConstructor(TestComponent, { + injector, + strategyFactory, propertyInputs: ['prop1', 'prop2'], attributeToPropertyInputs: new Map([['attr-1', 'prop1'], ['attr-2', 'prop2']]) diff --git a/tools/public_api_guard/elements/elements.d.ts b/tools/public_api_guard/elements/elements.d.ts index 9f9e8a996a..58eb3e8b9f 100644 --- a/tools/public_api_guard/elements/elements.d.ts +++ b/tools/public_api_guard/elements/elements.d.ts @@ -1,35 +1,5 @@ /** @experimental */ -export declare class ComponentFactoryNgElementStrategy implements NgElementStrategy { - events: Observable; - constructor(componentFactory: ComponentFactory, injector: Injector); - protected callNgOnChanges(): void; - connect(element: HTMLElement): void; - protected detectChanges(): void; - disconnect(): void; - getPropertyValue(property: string): any; - protected initializeComponent(element: HTMLElement): void; - protected initializeInputs(): void; - protected initializeOutputs(): void; - protected recordInputChange(property: string, currentValue: any): void; - protected scheduleDetectChanges(): void; - setPropertyValue(property: string, value: any): void; -} - -/** @experimental */ -export declare class ComponentFactoryNgElementStrategyFactory implements NgElementStrategyFactory { - constructor(componentFactory: ComponentFactory, injector: Injector); - create(): ComponentFactoryNgElementStrategy; -} - -/** @experimental */ -export declare function createNgElementConstructor

(config: NgElementConfig): NgElementConstructor

; - -/** @experimental */ -export declare function getConfigFromComponentFactory(componentFactory: ComponentFactory, injector: Injector): { - strategyFactory: ComponentFactoryNgElementStrategyFactory; - propertyInputs: string[]; - attributeToPropertyInputs: Map; -}; +export declare function createNgElementConstructor

(component: Type, config: NgElementConfig): NgElementConstructor

; /** @experimental */ export declare abstract class NgElement extends HTMLElement { @@ -42,9 +12,10 @@ export declare abstract class NgElement extends HTMLElement { /** @experimental */ export interface NgElementConfig { - attributeToPropertyInputs: Map; - propertyInputs: string[]; - strategyFactory: NgElementStrategyFactory; + attributeToPropertyInputs?: Map; + injector: Injector; + propertyInputs?: string[]; + strategyFactory?: NgElementStrategyFactory; } /** @experimental */