@polymer/lit-element
Version:
Polymer based lit-html custom element
236 lines (215 loc) • 8.35 kB
text/typescript
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import {LitElement} from '../lit-element.js';
import {PropertyDeclaration, UpdatingElement} from './updating-element.js';
export type Constructor<T> = {
new (...args: any[]): T
};
// From the TC39 Decorators proposal
interface ClassDescriptor {
kind: 'class';
elements: ClassElement[];
finisher?: <T>(clazz: Constructor<T>) => undefined | Constructor<T>;
}
// From the TC39 Decorators proposal
interface ClassElement {
kind: 'field'|'method';
key: PropertyKey;
placement: 'static'|'prototype'|'own';
initializer?: Function;
extras?: ClassElement[];
finisher?: <T>(clazz: Constructor<T>) => undefined | Constructor<T>;
descriptor?: PropertyDescriptor;
}
const legacyCustomElement =
(tagName: string, clazz: Constructor<HTMLElement>) => {
window.customElements.define(tagName, clazz);
// Cast as any because TS doesn't recognize the return type as being a
// subtype of the decorated class when clazz is typed as
// `Constructor<HTMLElement>` for some reason.
// `Constructor<HTMLElement>` is helpful to make sure the decorator is
// applied to elements however.
return clazz as any;
};
const standardCustomElement =
(tagName: string, descriptor: ClassDescriptor) => {
const {kind, elements} = descriptor;
return {
kind,
elements,
// This callback is called once the class is otherwise fully defined
finisher(clazz: Constructor<HTMLElement>) {
window.customElements.define(tagName, clazz);
}
};
};
/**
* Class decorator factory that defines the decorated class as a custom element.
*
* @param tagName the name of the custom element to define
*
* In TypeScript, the `tagName` passed to `customElement` should be a key of the
* `HTMLElementTagNameMap` interface. To add your element to the interface,
* declare the interface in this module:
*
* @customElement('my-element')
* export class MyElement extends LitElement {}
*
* declare global {
* interface HTMLElementTagNameMap {
* 'my-element': MyElement;
* }
* }
*
*/
export const customElement = (tagName: string) => (
classOrDescriptor: Constructor<HTMLElement>|ClassDescriptor) =>
(typeof classOrDescriptor === 'function')
? legacyCustomElement(tagName,
classOrDescriptor as Constructor<HTMLElement>)
: standardCustomElement(tagName, classOrDescriptor as ClassDescriptor);
const standardProperty =
(options: PropertyDeclaration, element: ClassElement) => {
// createProperty() takes care of defining the property, but we still must
// return some kind of descriptor, so return a descriptor for an unused
// prototype field. The finisher calls createProperty().
return {
kind : 'field',
key : Symbol(),
placement : 'own',
descriptor : {},
// When @babel/plugin-proposal-decorators implements initializers,
// do this instead of the initializer below. See:
// https://github.com/babel/babel/issues/9260 extras: [
// {
// kind: 'initializer',
// placement: 'own',
// initializer: descriptor.initializer,
// }
// ],
initializer(this: any) {
if (typeof element.initializer === 'function') {
this[element.key] = element.initializer!.call(this);
}
},
finisher(clazz: typeof UpdatingElement) {
clazz.createProperty(element.key, options);
}
};
};
const legacyProperty = (options: PropertyDeclaration, proto: Object,
name: PropertyKey) => {
(proto.constructor as typeof UpdatingElement).createProperty(name!, options);
};
/**
* A property decorator which creates a LitElement property which reflects a
* corresponding attribute value. A `PropertyDeclaration` may optionally be
* supplied to configure property features.
*/
export const property = (options?: PropertyDeclaration) =>
(protoOrDescriptor: Object|ClassElement, name?: PropertyKey): any =>
(name !== undefined)
? legacyProperty(options!, protoOrDescriptor as Object, name)
: standardProperty(options!, protoOrDescriptor as ClassElement);
/**
* A property decorator that converts a class property into a getter that
* executes a querySelector on the element's renderRoot.
*/
export const query = _query((target: NodeSelector, selector: string) =>
target.querySelector(selector));
/**
* A property decorator that converts a class property into a getter
* that executes a querySelectorAll on the element's renderRoot.
*/
export const queryAll = _query((target: NodeSelector, selector: string) =>
target.querySelectorAll(selector));
const legacyQuery =
(descriptor: PropertyDescriptor, proto: Object,
name: PropertyKey) => { Object.defineProperty(proto, name, descriptor); };
const standardQuery = (descriptor: PropertyDescriptor, element: ClassElement) =>
({
kind : 'method',
placement : 'prototype',
key : element.key,
descriptor,
});
/**
* Base-implementation of `@query` and `@queryAll` decorators.
*
* @param queryFn exectute a `selector` (ie, querySelector or querySelectorAll)
* against `target`.
*/
function _query<T>(queryFn: (target: NodeSelector, selector: string) => T) {
return (selector: string) => (protoOrDescriptor: Object|ClassElement,
name?: PropertyKey): any => {
const descriptor = {
get(this: LitElement) { return queryFn(this.renderRoot!, selector); },
enumerable : true,
configurable : true,
};
return (name !== undefined)
? legacyQuery(descriptor, protoOrDescriptor as Object, name)
: standardQuery(descriptor, protoOrDescriptor as ClassElement);
};
}
const standardEventOptions =
(options: AddEventListenerOptions, element: ClassElement) => {
return {
...element,
finisher(clazz: typeof UpdatingElement) {
Object.assign(clazz.prototype[element.key as keyof UpdatingElement],
options);
}
};
};
const legacyEventOptions =
(options: AddEventListenerOptions, proto: any,
name: PropertyKey) => { Object.assign(proto[name], options); };
/**
* Adds event listener options to a method used as an event listener in a
* lit-html template.
*
* @param options An object that specifis event listener options as accepted by
* `EventTarget#addEventListener` and `EventTarget#removeEventListener`.
*
* Current browsers support the `capture`, `passive`, and `once` options. See:
* https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Parameters
*
* @example
*
* class MyElement {
*
* clicked = false;
*
* render() {
* return html`<div @click=${this._onClick}`><button></button></div>`;
* }
*
* @eventOptions({capture: true})
* _onClick(e) {
* this.clicked = true;
* }
* }
*/
export const eventOptions = (options: AddEventListenerOptions) =>
// Return value typed as any to prevent TypeScript from complaining that
// standard decorator function signature does not match TypeScript decorator
// signature
// TODO(kschaaf): unclear why it was only failing on this decorator and not
// the others
((protoOrDescriptor: Object|ClassElement, name?: string) =>
(name !== undefined)
? legacyEventOptions(options, protoOrDescriptor as Object, name)
: standardEventOptions(options,
protoOrDescriptor as ClassElement)) as any;