
committed by
Miško Hevery

parent
46efd4b938
commit
87f60bccfd
@ -6,29 +6,35 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ApplicationRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges} from '@angular/core';
|
||||
import {ApplicationRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, Type} from '@angular/core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {merge} from 'rxjs/observable/merge';
|
||||
import {map} from 'rxjs/operator/map';
|
||||
|
||||
import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from './element-strategy';
|
||||
import {extractProjectableNodes} from './extract-projectable-nodes';
|
||||
import {camelToDashCase, isFunction, scheduler, strictEquals} from './utils';
|
||||
import {isFunction, scheduler, strictEquals} from './utils';
|
||||
|
||||
/** Time in milliseconds to wait before destroying the component ref when disconnected. */
|
||||
const DESTROY_DELAY = 10;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* create its components on connect.
|
||||
* Factory that creates new ComponentNgElementStrategy instance. Gets the component factory with the
|
||||
* constructor's injector's factory resolver and passes that factory to each strategy.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export class ComponentFactoryNgElementStrategyFactory implements NgElementStrategyFactory {
|
||||
constructor(private componentFactory: ComponentFactory<any>, private injector: Injector) {}
|
||||
export class ComponentNgElementStrategyFactory implements NgElementStrategyFactory {
|
||||
componentFactory: ComponentFactory<any>;
|
||||
|
||||
create() { return new ComponentFactoryNgElementStrategy(this.componentFactory, this.injector); }
|
||||
constructor(private component: Type<any>, private injector: Injector) {
|
||||
this.componentFactory =
|
||||
injector.get(ComponentFactoryResolver).resolveComponentFactory(component);
|
||||
}
|
||||
|
||||
create(injector: Injector) {
|
||||
return new ComponentNgElementStrategy(this.componentFactory, injector);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -37,12 +43,12 @@ export class ComponentFactoryNgElementStrategyFactory implements NgElementStrate
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export class ComponentFactoryNgElementStrategy implements NgElementStrategy {
|
||||
export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
/** Merged stream of the component's output events. */
|
||||
events: Observable<NgElementStrategyEvent>;
|
||||
|
||||
/** Reference to the component that was created on connect. */
|
||||
private componentRef: ComponentRef<any>;
|
||||
private componentRef: ComponentRef<any>|null;
|
||||
|
||||
/** Changes that have been made to the component ref since the last time onChanges was called. */
|
||||
private inputChanges: SimpleChanges|null = null;
|
||||
@ -96,6 +102,7 @@ export class ComponentFactoryNgElementStrategy implements NgElementStrategy {
|
||||
this.scheduledDestroyFn = scheduler.schedule(() => {
|
||||
if (this.componentRef) {
|
||||
this.componentRef !.destroy();
|
||||
this.componentRef = null;
|
||||
}
|
||||
}, DESTROY_DELAY);
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
* 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 {ComponentFactory} from '@angular/core';
|
||||
import {ComponentFactory, Injector} from '@angular/core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
|
||||
/**
|
||||
@ -38,4 +38,7 @@ export interface NgElementStrategy {
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export interface NgElementStrategyFactory { create(): NgElementStrategy; }
|
||||
export interface NgElementStrategyFactory {
|
||||
/** Creates a new instance to be used for an NgElement. */
|
||||
create(injector: Injector): NgElementStrategy;
|
||||
}
|
||||
|
@ -6,12 +6,12 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ComponentFactoryResolver, Injector, Type} from '@angular/core';
|
||||
import {Injector, Type} from '@angular/core';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
|
||||
import {ComponentFactoryNgElementStrategyFactory} from './component-factory-strategy';
|
||||
import {ComponentNgElementStrategyFactory} from './component-factory-strategy';
|
||||
import {NgElementStrategy, NgElementStrategyFactory} from './element-strategy';
|
||||
import {camelToDashCase, createCustomEvent} from './utils';
|
||||
import {createCustomEvent, getComponentInputs, getDefaultAttributeToPropertyInputs} from './utils';
|
||||
|
||||
/**
|
||||
* Class constructor based on an Angular Component to be used for custom element registration.
|
||||
@ -21,7 +21,7 @@ import {camelToDashCase, createCustomEvent} from './utils';
|
||||
export interface NgElementConstructor<P> {
|
||||
readonly observedAttributes: string[];
|
||||
|
||||
new (): NgElement&WithProperties<P>;
|
||||
new (injector: Injector): NgElement&WithProperties<P>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,30 +50,19 @@ export type WithProperties<P> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialization configuration for the NgElementConstructor. Provides the strategy factory
|
||||
* that produces a strategy for each instantiated element. Additionally, provides a function
|
||||
* that takes the component factory and provides a map of which attributes should be observed on
|
||||
* the element and which property they are associated with.
|
||||
* Initialization configuration for the NgElementConstructor which contains the injector to be used
|
||||
* for retrieving the component's factory as well as the default context for the component. May
|
||||
* provide a custom strategy factory to be used instead of the default. May provide a custom mapping
|
||||
* of attribute names to component inputs.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export interface NgElementConfig {
|
||||
injector: Injector;
|
||||
strategyFactory?: NgElementStrategyFactory;
|
||||
propertyInputs?: string[];
|
||||
attributeToPropertyInputs?: {[key: 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: {[key: string]: string} = {};
|
||||
inputs.forEach(({propName, templateName}) => {
|
||||
attributeToPropertyInputs[camelToDashCase(templateName)] = propName;
|
||||
});
|
||||
|
||||
return attributeToPropertyInputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Creates a custom element class based on an Angular Component. Takes a configuration
|
||||
* that provides initialization information to the created class. E.g. the configuration's injector
|
||||
@ -90,13 +79,10 @@ function getDefaultAttributeToPropertyInputs(inputs: {propName: string, template
|
||||
*/
|
||||
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 inputs = getComponentInputs(component, config.injector);
|
||||
|
||||
const defaultStrategyFactory = config.strategyFactory ||
|
||||
new ComponentFactoryNgElementStrategyFactory(componentFactory, config.injector);
|
||||
const strategyFactory =
|
||||
config.strategyFactory || new ComponentNgElementStrategyFactory(component, config.injector);
|
||||
|
||||
const attributeToPropertyInputs =
|
||||
config.attributeToPropertyInputs || getDefaultAttributeToPropertyInputs(inputs);
|
||||
@ -104,13 +90,9 @@ export function createNgElementConstructor<P>(
|
||||
class NgElementImpl extends NgElement {
|
||||
static readonly observedAttributes = Object.keys(attributeToPropertyInputs);
|
||||
|
||||
constructor(strategyFactoryOverride?: NgElementStrategyFactory) {
|
||||
constructor(injector?: Injector) {
|
||||
super();
|
||||
|
||||
// Use the constructor's strategy factory override if it is present, otherwise default to
|
||||
// the config's factory.
|
||||
const strategyFactory = strategyFactoryOverride || defaultStrategyFactory;
|
||||
this.ngElementStrategy = strategyFactory.create();
|
||||
this.ngElementStrategy = strategyFactory.create(injector || config.injector);
|
||||
}
|
||||
|
||||
attributeChangedCallback(
|
||||
@ -120,14 +102,6 @@ export function createNgElementConstructor<P>(
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
// Take element attribute inputs and set them as inputs on the strategy
|
||||
NgElementImpl.observedAttributes.forEach(attrName => {
|
||||
const propName = attributeToPropertyInputs[attrName] !;
|
||||
if (this.hasAttribute(attrName)) {
|
||||
this.ngElementStrategy.setInputValue(propName, this.getAttribute(attrName) !);
|
||||
}
|
||||
});
|
||||
|
||||
this.ngElementStrategy.connect(this);
|
||||
|
||||
// Listen for events from the strategy and dispatch them as custom events
|
||||
@ -149,8 +123,7 @@ export function createNgElementConstructor<P>(
|
||||
|
||||
// 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 => {
|
||||
inputs.map(({propName}) => propName).forEach(property => {
|
||||
Object.defineProperty(NgElementImpl.prototype, property, {
|
||||
get: function() { return this.ngElementStrategy.getInputValue(property); },
|
||||
set: function(newValue: any) { this.ngElementStrategy.setInputValue(property, newValue); },
|
||||
|
@ -5,8 +5,7 @@
|
||||
* 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 {Type} from '@angular/core';
|
||||
import {ComponentFactoryResolver, Injector, Type} from '@angular/core';
|
||||
|
||||
const elProto = Element.prototype as any;
|
||||
const matches = elProto.matches || elProto.matchesSelector || elProto.mozMatchesSelector ||
|
||||
@ -101,3 +100,25 @@ export function matchesSelector(element: Element, selector: string): boolean {
|
||||
export function strictEquals(value1: any, value2: any): boolean {
|
||||
return value1 === value2 || (value1 !== value1 && value2 !== value2);
|
||||
}
|
||||
|
||||
/** Gets a map of default set of attributes to observe and the properties they affect. */
|
||||
export function getDefaultAttributeToPropertyInputs(
|
||||
inputs: {propName: string, templateName: string}[]) {
|
||||
const attributeToPropertyInputs: {[key: string]: string} = {};
|
||||
inputs.forEach(({propName, templateName}) => {
|
||||
attributeToPropertyInputs[camelToDashCase(templateName)] = propName;
|
||||
});
|
||||
|
||||
return attributeToPropertyInputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a component's set of inputs. Uses the injector to get the component factory where the inputs
|
||||
* are defined.
|
||||
*/
|
||||
export function getComponentInputs(
|
||||
component: Type<any>, injector: Injector): {propName: string, templateName: string}[] {
|
||||
const componentFactoryResolver: ComponentFactoryResolver = injector.get(ComponentFactoryResolver);
|
||||
const componentFactory = componentFactoryResolver.resolveComponentFactory(component);
|
||||
return componentFactory.inputs;
|
||||
}
|
||||
|
@ -10,12 +10,12 @@ import {ComponentFactory, ComponentRef, Injector, NgModuleRef, SimpleChange, Sim
|
||||
import {fakeAsync, tick} from '@angular/core/testing';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
|
||||
import {ComponentFactoryNgElementStrategy, ComponentFactoryNgElementStrategyFactory} from '../src/component-factory-strategy';
|
||||
import {ComponentNgElementStrategy, ComponentNgElementStrategyFactory} from '../src/component-factory-strategy';
|
||||
import {NgElementStrategyEvent} from '../src/element-strategy';
|
||||
|
||||
describe('ComponentFactoryNgElementStrategy', () => {
|
||||
let factory: FakeComponentFactory;
|
||||
let strategy: ComponentFactoryNgElementStrategy;
|
||||
let strategy: ComponentNgElementStrategy;
|
||||
|
||||
let injector: any;
|
||||
let componentRef: any;
|
||||
@ -25,22 +25,26 @@ describe('ComponentFactoryNgElementStrategy', () => {
|
||||
factory = new FakeComponentFactory();
|
||||
componentRef = factory.componentRef;
|
||||
|
||||
injector = jasmine.createSpyObj('injector', ['get']);
|
||||
applicationRef = jasmine.createSpyObj('applicationRef', ['attachView']);
|
||||
injector.get.and.returnValue(applicationRef);
|
||||
|
||||
strategy = new ComponentFactoryNgElementStrategy(factory, injector);
|
||||
strategy = new ComponentNgElementStrategy(factory, injector);
|
||||
});
|
||||
|
||||
it('should create a new strategy from the factory', () => {
|
||||
const strategyFactory = new ComponentFactoryNgElementStrategyFactory(factory, injector);
|
||||
expect(strategyFactory.create()).toBeTruthy();
|
||||
const factoryResolver = jasmine.createSpyObj('factoryResolver', ['resolveComponentFactory']);
|
||||
factoryResolver.resolveComponentFactory.and.returnValue(factory);
|
||||
injector = jasmine.createSpyObj('injector', ['get']);
|
||||
injector.get.and.returnValue(factoryResolver);
|
||||
|
||||
const strategyFactory = new ComponentNgElementStrategyFactory(FakeComponent, injector);
|
||||
expect(strategyFactory.create(injector)).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('after connected', () => {
|
||||
beforeEach(() => {
|
||||
// Set up an initial value to make sure it is passed to the component
|
||||
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
||||
injector.get.and.returnValue(applicationRef);
|
||||
strategy.connect(document.createElement('div'));
|
||||
});
|
||||
|
||||
@ -95,7 +99,7 @@ describe('ComponentFactoryNgElementStrategy', () => {
|
||||
it('should not detect changes', fakeAsync(() => {
|
||||
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
||||
tick(16); // scheduler waits 16ms if RAF is unavailable
|
||||
expect(componentRef.changeDetectorRef.detectChanges).toHaveBeenCalledTimes(0);
|
||||
expect(componentRef.changeDetectorRef.detectChanges).not.toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
|
@ -51,7 +51,7 @@ if (typeof customElements !== 'undefined') {
|
||||
});
|
||||
|
||||
it('should send input values from attributes when connected', () => {
|
||||
const element = new NgElementCtor();
|
||||
const element = new NgElementCtor(injector);
|
||||
element.setAttribute('foo-foo', 'value-foo-foo');
|
||||
element.setAttribute('barbar', 'value-barbar');
|
||||
element.connectedCallback();
|
||||
@ -62,7 +62,7 @@ if (typeof customElements !== 'undefined') {
|
||||
});
|
||||
|
||||
it('should listen to output events after connected', () => {
|
||||
const element = new NgElementCtor();
|
||||
const element = new NgElementCtor(injector);
|
||||
element.connectedCallback();
|
||||
|
||||
let eventValue: any = null;
|
||||
@ -73,7 +73,7 @@ if (typeof customElements !== 'undefined') {
|
||||
});
|
||||
|
||||
it('should not listen to output events after disconnected', () => {
|
||||
const element = new NgElementCtor();
|
||||
const element = new NgElementCtor(injector);
|
||||
element.connectedCallback();
|
||||
element.disconnectedCallback();
|
||||
expect(strategy.disconnectCalled).toBe(true);
|
||||
@ -86,7 +86,7 @@ if (typeof customElements !== 'undefined') {
|
||||
});
|
||||
|
||||
it('should properly set getters/setters on the element', () => {
|
||||
const element = new NgElementCtor();
|
||||
const element = new NgElementCtor(injector);
|
||||
element.fooFoo = 'foo-foo-value';
|
||||
element.barBar = 'barBar-value';
|
||||
|
||||
@ -104,29 +104,26 @@ if (typeof customElements !== 'undefined') {
|
||||
NgElementCtorWithChangedAttr = createNgElementConstructor(TestComponent, {
|
||||
injector,
|
||||
strategyFactory,
|
||||
propertyInputs: ['prop1', 'prop2'],
|
||||
attributeToPropertyInputs: {'attr-1': 'prop1', 'attr-2': 'prop2'}
|
||||
attributeToPropertyInputs: {'attr-1': 'fooFoo', 'attr-2': 'barbar'}
|
||||
});
|
||||
|
||||
customElements.define('test-element-with-changed-attributes', NgElementCtorWithChangedAttr);
|
||||
});
|
||||
|
||||
beforeEach(() => { element = new NgElementCtorWithChangedAttr(); });
|
||||
beforeEach(() => { element = new NgElementCtorWithChangedAttr(injector); });
|
||||
|
||||
it('should affect which attributes are watched', () => {
|
||||
expect(NgElementCtorWithChangedAttr.observedAttributes).toEqual(['attr-1', 'attr-2']);
|
||||
});
|
||||
|
||||
it('should send attribute values as inputs when connected', () => {
|
||||
const element = new NgElementCtorWithChangedAttr();
|
||||
const element = new NgElementCtorWithChangedAttr(injector);
|
||||
element.setAttribute('attr-1', 'value-1');
|
||||
element.setAttribute('attr-2', 'value-2');
|
||||
element.setAttribute('attr-3', 'value-3'); // Made-up attribute
|
||||
element.connectedCallback();
|
||||
|
||||
expect(strategy.getInputValue('prop1')).toBe('value-1');
|
||||
expect(strategy.getInputValue('prop2')).toBe('value-2');
|
||||
expect(strategy.getInputValue('prop3')).not.toBe('value-3');
|
||||
expect(strategy.getInputValue('fooFoo')).toBe('value-1');
|
||||
expect(strategy.getInputValue('barbar')).toBe('value-2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user