UNPKG

8.35 kBPlain TextView Raw
1
2/**
3 * @license
4 * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
5 * This code may only be used under the BSD style license found at
6 * http://polymer.github.io/LICENSE.txt
7 * The complete set of authors may be found at
8 * http://polymer.github.io/AUTHORS.txt
9 * The complete set of contributors may be found at
10 * http://polymer.github.io/CONTRIBUTORS.txt
11 * Code distributed by Google as part of the polymer project is also
12 * subject to an additional IP rights grant found at
13 * http://polymer.github.io/PATENTS.txt
14 */
15
16import {LitElement} from '../lit-element.js';
17
18import {PropertyDeclaration, UpdatingElement} from './updating-element.js';
19
20export type Constructor<T> = {
21 new (...args: any[]): T
22};
23
24// From the TC39 Decorators proposal
25interface ClassDescriptor {
26 kind: 'class';
27 elements: ClassElement[];
28 finisher?: <T>(clazz: Constructor<T>) => undefined | Constructor<T>;
29}
30
31// From the TC39 Decorators proposal
32interface ClassElement {
33 kind: 'field'|'method';
34 key: PropertyKey;
35 placement: 'static'|'prototype'|'own';
36 initializer?: Function;
37 extras?: ClassElement[];
38 finisher?: <T>(clazz: Constructor<T>) => undefined | Constructor<T>;
39 descriptor?: PropertyDescriptor;
40}
41
42const legacyCustomElement =
43 (tagName: string, clazz: Constructor<HTMLElement>) => {
44 window.customElements.define(tagName, clazz);
45 // Cast as any because TS doesn't recognize the return type as being a
46 // subtype of the decorated class when clazz is typed as
47 // `Constructor<HTMLElement>` for some reason.
48 // `Constructor<HTMLElement>` is helpful to make sure the decorator is
49 // applied to elements however.
50 return clazz as any;
51 };
52
53const standardCustomElement =
54 (tagName: string, descriptor: ClassDescriptor) => {
55 const {kind, elements} = descriptor;
56 return {
57 kind,
58 elements,
59 // This callback is called once the class is otherwise fully defined
60 finisher(clazz: Constructor<HTMLElement>) {
61 window.customElements.define(tagName, clazz);
62 }
63 };
64 };
65
66/**
67 * Class decorator factory that defines the decorated class as a custom element.
68 *
69 * @param tagName the name of the custom element to define
70 *
71 * In TypeScript, the `tagName` passed to `customElement` should be a key of the
72 * `HTMLElementTagNameMap` interface. To add your element to the interface,
73 * declare the interface in this module:
74 *
75 * @customElement('my-element')
76 * export class MyElement extends LitElement {}
77 *
78 * declare global {
79 * interface HTMLElementTagNameMap {
80 * 'my-element': MyElement;
81 * }
82 * }
83 *
84 */
85export const customElement = (tagName: string) => (
86 classOrDescriptor: Constructor<HTMLElement>|ClassDescriptor) =>
87 (typeof classOrDescriptor === 'function')
88 ? legacyCustomElement(tagName,
89 classOrDescriptor as Constructor<HTMLElement>)
90 : standardCustomElement(tagName, classOrDescriptor as ClassDescriptor);
91
92const standardProperty =
93 (options: PropertyDeclaration, element: ClassElement) => {
94 // createProperty() takes care of defining the property, but we still must
95 // return some kind of descriptor, so return a descriptor for an unused
96 // prototype field. The finisher calls createProperty().
97 return {
98 kind : 'field',
99 key : Symbol(),
100 placement : 'own',
101 descriptor : {},
102 // When @babel/plugin-proposal-decorators implements initializers,
103 // do this instead of the initializer below. See:
104 // https://github.com/babel/babel/issues/9260 extras: [
105 // {
106 // kind: 'initializer',
107 // placement: 'own',
108 // initializer: descriptor.initializer,
109 // }
110 // ],
111 initializer(this: any) {
112 if (typeof element.initializer === 'function') {
113 this[element.key] = element.initializer!.call(this);
114 }
115 },
116 finisher(clazz: typeof UpdatingElement) {
117 clazz.createProperty(element.key, options);
118 }
119 };
120 };
121
122const legacyProperty = (options: PropertyDeclaration, proto: Object,
123 name: PropertyKey) => {
124 (proto.constructor as typeof UpdatingElement).createProperty(name!, options);
125};
126
127/**
128 * A property decorator which creates a LitElement property which reflects a
129 * corresponding attribute value. A `PropertyDeclaration` may optionally be
130 * supplied to configure property features.
131 */
132export const property = (options?: PropertyDeclaration) =>
133 (protoOrDescriptor: Object|ClassElement, name?: PropertyKey): any =>
134 (name !== undefined)
135 ? legacyProperty(options!, protoOrDescriptor as Object, name)
136 : standardProperty(options!, protoOrDescriptor as ClassElement);
137
138/**
139 * A property decorator that converts a class property into a getter that
140 * executes a querySelector on the element's renderRoot.
141 */
142export const query = _query((target: NodeSelector, selector: string) =>
143 target.querySelector(selector));
144
145/**
146 * A property decorator that converts a class property into a getter
147 * that executes a querySelectorAll on the element's renderRoot.
148 */
149export const queryAll = _query((target: NodeSelector, selector: string) =>
150 target.querySelectorAll(selector));
151
152const legacyQuery =
153 (descriptor: PropertyDescriptor, proto: Object,
154 name: PropertyKey) => { Object.defineProperty(proto, name, descriptor); };
155
156const standardQuery = (descriptor: PropertyDescriptor, element: ClassElement) =>
157 ({
158 kind : 'method',
159 placement : 'prototype',
160 key : element.key,
161 descriptor,
162 });
163
164/**
165 * Base-implementation of `@query` and `@queryAll` decorators.
166 *
167 * @param queryFn exectute a `selector` (ie, querySelector or querySelectorAll)
168 * against `target`.
169 */
170function _query<T>(queryFn: (target: NodeSelector, selector: string) => T) {
171 return (selector: string) => (protoOrDescriptor: Object|ClassElement,
172 name?: PropertyKey): any => {
173 const descriptor = {
174 get(this: LitElement) { return queryFn(this.renderRoot!, selector); },
175 enumerable : true,
176 configurable : true,
177 };
178 return (name !== undefined)
179 ? legacyQuery(descriptor, protoOrDescriptor as Object, name)
180 : standardQuery(descriptor, protoOrDescriptor as ClassElement);
181 };
182}
183
184const standardEventOptions =
185 (options: AddEventListenerOptions, element: ClassElement) => {
186 return {
187 ...element,
188 finisher(clazz: typeof UpdatingElement) {
189 Object.assign(clazz.prototype[element.key as keyof UpdatingElement],
190 options);
191 }
192 };
193 };
194
195const legacyEventOptions =
196 (options: AddEventListenerOptions, proto: any,
197 name: PropertyKey) => { Object.assign(proto[name], options); };
198
199/**
200 * Adds event listener options to a method used as an event listener in a
201 * lit-html template.
202 *
203 * @param options An object that specifis event listener options as accepted by
204 * `EventTarget#addEventListener` and `EventTarget#removeEventListener`.
205 *
206 * Current browsers support the `capture`, `passive`, and `once` options. See:
207 * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Parameters
208 *
209 * @example
210 *
211 * class MyElement {
212 *
213 * clicked = false;
214 *
215 * render() {
216 * return html`<div @click=${this._onClick}`><button></button></div>`;
217 * }
218 *
219 * @eventOptions({capture: true})
220 * _onClick(e) {
221 * this.clicked = true;
222 * }
223 * }
224 */
225export const eventOptions = (options: AddEventListenerOptions) =>
226 // Return value typed as any to prevent TypeScript from complaining that
227 // standard decorator function signature does not match TypeScript decorator
228 // signature
229 // TODO(kschaaf): unclear why it was only failing on this decorator and not
230 // the others
231 ((protoOrDescriptor: Object|ClassElement, name?: string) =>
232 (name !== undefined)
233 ? legacyEventOptions(options, protoOrDescriptor as Object, name)
234 : standardEventOptions(options,
235 protoOrDescriptor as ClassElement)) as any;