2018-02-28 12:45:11 -05:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
*/
|
|
|
|
|
2018-03-06 17:02:25 -05:00
|
|
|
import {Injector, Type} from '@angular/core';
|
2018-02-28 12:45:11 -05:00
|
|
|
import {Subscription} from 'rxjs/Subscription';
|
|
|
|
|
2018-03-06 17:02:25 -05:00
|
|
|
import {ComponentNgElementStrategyFactory} from './component-factory-strategy';
|
2018-02-28 12:45:11 -05:00
|
|
|
import {NgElementStrategy, NgElementStrategyFactory} from './element-strategy';
|
2018-03-06 17:02:25 -05:00
|
|
|
import {createCustomEvent, getComponentInputs, getDefaultAttributeToPropertyInputs} from './utils';
|
2018-02-28 12:45:11 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Class constructor based on an Angular Component to be used for custom element registration.
|
|
|
|
*
|
|
|
|
* @experimental
|
|
|
|
*/
|
|
|
|
export interface NgElementConstructor<P> {
|
|
|
|
readonly observedAttributes: string[];
|
|
|
|
|
2018-03-06 17:02:25 -05:00
|
|
|
new (injector: Injector): NgElement&WithProperties<P>;
|
2018-02-28 12:45:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Class that extends HTMLElement and implements the functionality needed for a custom element.
|
|
|
|
*
|
|
|
|
* @experimental
|
|
|
|
*/
|
|
|
|
export abstract class NgElement extends HTMLElement {
|
|
|
|
protected ngElementStrategy: NgElementStrategy;
|
|
|
|
protected ngElementEventsSubscription: Subscription|null = null;
|
|
|
|
|
|
|
|
abstract attributeChangedCallback(
|
|
|
|
attrName: string, oldValue: string|null, newValue: string, namespace?: string): void;
|
|
|
|
abstract connectedCallback(): void;
|
|
|
|
abstract disconnectedCallback(): void;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Additional type information that can be added to the NgElement class for properties added based
|
|
|
|
* on the inputs and methods of the underlying component.
|
2018-03-02 13:08:16 -05:00
|
|
|
*
|
|
|
|
* @experimental
|
2018-02-28 12:45:11 -05:00
|
|
|
*/
|
|
|
|
export type WithProperties<P> = {
|
|
|
|
[property in keyof P]: P[property]
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2018-03-06 17:02:25 -05:00
|
|
|
* 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
|
2018-03-15 20:18:40 -04:00
|
|
|
* provide a custom strategy factory to be used instead of the default.
|
2018-02-28 12:45:11 -05:00
|
|
|
*
|
|
|
|
* @experimental
|
|
|
|
*/
|
|
|
|
export interface NgElementConfig {
|
2018-03-02 01:34:21 -05:00
|
|
|
injector: Injector;
|
|
|
|
strategyFactory?: NgElementStrategyFactory;
|
|
|
|
}
|
|
|
|
|
2018-02-28 12:45:11 -05:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
* will be the initial injector set on the class which will be used for each created instance.
|
|
|
|
*
|
|
|
|
* @description Builds a class that encapsulates the functionality of the provided component and
|
|
|
|
* uses the config's information to provide more context to the class. Takes the component factory's
|
|
|
|
* inputs and outputs to convert them to the proper custom element API and add hooks to input
|
2018-03-14 16:24:27 -04:00
|
|
|
* changes. Passes the config's injector to each created instance (may be overridden with the
|
2018-02-28 12:45:11 -05:00
|
|
|
* static property to affect all newly created instances, or as a constructor argument for
|
|
|
|
* one-off creations).
|
|
|
|
*
|
|
|
|
* @experimental
|
|
|
|
*/
|
2018-03-14 16:24:27 -04:00
|
|
|
export function createCustomElement<P>(
|
2018-03-02 01:34:21 -05:00
|
|
|
component: Type<any>, config: NgElementConfig): NgElementConstructor<P> {
|
2018-03-06 17:02:25 -05:00
|
|
|
const inputs = getComponentInputs(component, config.injector);
|
2018-03-02 01:34:21 -05:00
|
|
|
|
2018-03-06 17:02:25 -05:00
|
|
|
const strategyFactory =
|
|
|
|
config.strategyFactory || new ComponentNgElementStrategyFactory(component, config.injector);
|
2018-03-02 01:34:21 -05:00
|
|
|
|
2018-03-15 20:18:40 -04:00
|
|
|
const attributeToPropertyInputs = getDefaultAttributeToPropertyInputs(inputs);
|
2018-03-02 01:34:21 -05:00
|
|
|
|
2018-02-28 12:45:11 -05:00
|
|
|
class NgElementImpl extends NgElement {
|
2018-03-02 13:08:16 -05:00
|
|
|
static readonly observedAttributes = Object.keys(attributeToPropertyInputs);
|
2018-02-28 12:45:11 -05:00
|
|
|
|
2018-03-06 17:02:25 -05:00
|
|
|
constructor(injector?: Injector) {
|
2018-02-28 12:45:11 -05:00
|
|
|
super();
|
2018-03-06 17:02:25 -05:00
|
|
|
this.ngElementStrategy = strategyFactory.create(injector || config.injector);
|
2018-02-28 12:45:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
attributeChangedCallback(
|
|
|
|
attrName: string, oldValue: string|null, newValue: string, namespace?: string): void {
|
2018-03-02 13:08:16 -05:00
|
|
|
const propName = attributeToPropertyInputs[attrName] !;
|
|
|
|
this.ngElementStrategy.setInputValue(propName, newValue);
|
2018-02-28 12:45:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
connectedCallback(): void {
|
|
|
|
this.ngElementStrategy.connect(this);
|
|
|
|
|
|
|
|
// Listen for events from the strategy and dispatch them as custom events
|
|
|
|
this.ngElementEventsSubscription = this.ngElementStrategy.events.subscribe(e => {
|
|
|
|
const customEvent = createCustomEvent(this.ownerDocument, e.name, e.value);
|
|
|
|
this.dispatchEvent(customEvent);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
disconnectedCallback(): void {
|
|
|
|
this.ngElementStrategy.disconnect();
|
|
|
|
|
|
|
|
if (this.ngElementEventsSubscription) {
|
|
|
|
this.ngElementEventsSubscription.unsubscribe();
|
|
|
|
this.ngElementEventsSubscription = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-02 01:34:21 -05:00
|
|
|
// Add getters and setters to the prototype for each property input. If the config does not
|
|
|
|
// contain property inputs, use all inputs by default.
|
2018-03-06 17:02:25 -05:00
|
|
|
inputs.map(({propName}) => propName).forEach(property => {
|
2018-02-28 12:45:11 -05:00
|
|
|
Object.defineProperty(NgElementImpl.prototype, property, {
|
2018-03-02 13:08:16 -05:00
|
|
|
get: function() { return this.ngElementStrategy.getInputValue(property); },
|
|
|
|
set: function(newValue: any) { this.ngElementStrategy.setInputValue(property, newValue); },
|
2018-02-28 12:45:11 -05:00
|
|
|
configurable: true,
|
|
|
|
enumerable: true,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
return (NgElementImpl as any) as NgElementConstructor<P>;
|
|
|
|
}
|