feat(elements): provide type, not factory; remove config need (#22413)

PR Close #22413
This commit is contained in:
Andrew Seguin
2018-03-01 22:34:21 -08:00
committed by Miško Hevery
parent d2be675acc
commit 19368085aa
9 changed files with 65 additions and 115 deletions

View File

@ -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';

View File

@ -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<any>, injector: Injector) {
const attributeToPropertyInputs = new Map<string, string>();
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

View File

@ -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<P> = {
* @experimental
*/
export interface NgElementConfig {
strategyFactory: NgElementStrategyFactory;
propertyInputs: string[];
attributeToPropertyInputs: Map<string, string>;
injector: Injector;
strategyFactory?: NgElementStrategyFactory;
propertyInputs?: string[];
attributeToPropertyInputs?: Map<string, string>;
}
/** 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<string, string>();
inputs.forEach(({propName, templateName}) => {
attributeToPropertyInputs.set(camelToDashCase(templateName), propName);
});
return attributeToPropertyInputs;
}
/**
@ -73,28 +86,40 @@ export interface NgElementConfig {
*
* @experimental
*/
export function createNgElementConstructor<P>(config: NgElementConfig): NgElementConstructor<P> {
export function createNgElementConstructor<P>(
component: Type<any>, config: NgElementConfig): NgElementConstructor<P> {
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<P>(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); },

View File

@ -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();

View File

@ -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<WithFooBar>;
let factory: ComponentFactory<TestComponent>;
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<string, string>([['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<string, string>([['attr-1', 'prop1'], ['attr-2', 'prop2']])