Skip to content

Commit 021cf8a

Browse files
author
Steve Orvell
authored
Merge pull request #159 from Polymer/query
Add @query, @QueryAll and @CustomElement decorators
2 parents 0ec5deb + e38b0c3 commit 021cf8a

File tree

4 files changed

+218
-0
lines changed

4 files changed

+218
-0
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Change Log
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](http://keepachangelog.com/)
6+
and this project adheres to [Semantic Versioning](http://semver.org/).
7+
8+
<!--
9+
PRs should document their user-visible changes (if any) in the
10+
Unreleased section, uncommenting the header as necessary.
11+
-->
12+
13+
## Unreleased
14+
15+
### Added
16+
* Added `@query()`, `@queryAll()`, and `@customElement` decorators ([#159](https://github.com/Polymer/lit-element/pull/159))
17+
18+
<!-- ### Changed -->
19+
<!-- ### Removed -->
20+
<!-- ### Fixed -->

src/lib/decorators.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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+
16+
import {LitElement} from '../lit-element.js';
17+
18+
export type Constructor<T> = {
19+
new (...args: unknown[]): T
20+
};
21+
22+
/**
23+
* Class decorator factory that defines the decorated class as a custom element.
24+
*
25+
* @param tagName the name of the custom element to define
26+
*
27+
* In TypeScript, the `tagName` passed to `customElement` must be a key of the
28+
* `HTMLElementTagNameMap` interface. To add your element to the interface,
29+
* declare the interface in this module:
30+
*
31+
* @customElement('my-element')
32+
* export class MyElement extends LitElement {}
33+
*
34+
* declare global {
35+
* interface HTMLElementTagNameMap {
36+
* 'my-element': MyElement;
37+
* }
38+
* }
39+
*
40+
*/
41+
export const customElement = (tagName: keyof HTMLElementTagNameMap) =>
42+
(clazz: Constructor<HTMLElement>) => {
43+
window.customElements.define(tagName, clazz);
44+
// Cast as any because TS doesn't recognize the return type as being a
45+
// subtype of the decorated class when clazz is typed as
46+
// `Constructor<HTMLElement>` for some reason. `Constructor<HTMLElement>`
47+
// is helpful to make sure the decorator is applied to elements however.
48+
return clazz as any;
49+
};
50+
51+
/**
52+
* A property decorator that converts a class property into a getter that
53+
* executes a querySelector on the element's renderRoot.
54+
*/
55+
export const query = _query((target: NodeSelector, selector: string) =>
56+
target.querySelector(selector));
57+
58+
/**
59+
* A property decorator that converts a class property into a getter
60+
* that executes a querySelectorAll on the element's renderRoot.
61+
*/
62+
export const queryAll = _query((target: NodeSelector, selector: string) =>
63+
target.querySelectorAll(selector));
64+
65+
/**
66+
* Base-implementation of `@query` and `@queryAll` decorators.
67+
*
68+
* @param queryFn exectute a `selector` (ie, querySelector or querySelectorAll)
69+
* against `target`.
70+
*/
71+
function _query<T>(queryFn: (target: NodeSelector, selector: string) => T) {
72+
return (selector: string) => (proto: any, propName: string) => {
73+
Object.defineProperty(proto, propName, {
74+
get(this: LitElement) { return queryFn(this.renderRoot!, selector); },
75+
enumerable : true,
76+
configurable : true,
77+
});
78+
};
79+
}

src/test/lib/decorators_test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
4+
* This code may only be used under the BSD style license found at
5+
* http://polymer.github.io/LICENSE.txt
6+
* The complete set of authors may be found at
7+
* http://polymer.github.io/AUTHORS.txt
8+
* The complete set of contributors may be found at
9+
* http://polymer.github.io/CONTRIBUTORS.txt
10+
* Code distributed by Google as part of the polymer project is also
11+
* subject to an additional IP rights grant found at
12+
* http://polymer.github.io/PATENTS.txt
13+
*/
14+
15+
import {customElement, query, queryAll} from '../../lib/decorators.js';
16+
import {html, LitElement} from '../../lit-element.js';
17+
import { generateElementName } from '../test-helpers.js';
18+
19+
const assert = chai.assert;
20+
21+
suite('decorators', () => {
22+
let container: HTMLElement;
23+
24+
setup(() => {
25+
container = document.createElement('div');
26+
container.id = 'test-container';
27+
document.body.appendChild(container);
28+
});
29+
30+
teardown(() => {
31+
if (container !== undefined) {
32+
container.parentElement!.removeChild(container);
33+
(container as any) = undefined;
34+
}
35+
});
36+
37+
suite('@customElement', () => {
38+
test('defines an element', () => {
39+
const tagName = generateElementName();
40+
@customElement(tagName as keyof HTMLElementTagNameMap)
41+
class C0 extends HTMLElement {
42+
}
43+
const DefinedC = customElements.get(tagName);
44+
assert.strictEqual(DefinedC, C0);
45+
});
46+
});
47+
48+
suite('@query', () => {
49+
@customElement(generateElementName() as keyof HTMLElementTagNameMap)
50+
class C extends LitElement {
51+
52+
@query('#blah') blah?: HTMLDivElement;
53+
54+
@query('span') nope?: HTMLSpanElement;
55+
56+
render() {
57+
return html`
58+
<div>Not this one</div>
59+
<div id="blah">This one</div>
60+
`;
61+
}
62+
}
63+
64+
test('returns an element when it exists', async () => {
65+
const c = new C();
66+
container.appendChild(c);
67+
await c.updateComplete;
68+
const div = c.blah;
69+
assert.instanceOf(div, HTMLDivElement);
70+
assert.equal(div!.innerText, 'This one');
71+
});
72+
73+
test('returns null when no match', async () => {
74+
const c = new C();
75+
container.appendChild(c);
76+
await Promise.resolve();
77+
const span = c.nope;
78+
assert.isNull(span);
79+
});
80+
});
81+
82+
suite('@queryAll', () => {
83+
@customElement(generateElementName() as keyof HTMLElementTagNameMap)
84+
class C extends LitElement {
85+
86+
@queryAll('div') divs!: NodeList;
87+
88+
@queryAll('span') spans!: NodeList;
89+
90+
render() {
91+
return html`
92+
<div>Not this one</div>
93+
<div id="blah">This one</div>
94+
`;
95+
}
96+
}
97+
98+
test('returns elements when they exists', async () => {
99+
const c = new C();
100+
container.appendChild(c);
101+
await c.updateComplete;
102+
const divs = c.divs!;
103+
// This is not true in ShadyDOM:
104+
// assert.instanceOf(divs, NodeList);
105+
assert.lengthOf(divs, 2);
106+
});
107+
108+
test('returns empty NodeList when no match', async () => {
109+
const c = new C();
110+
container.appendChild(c);
111+
await c.updateComplete;
112+
const spans = c.spans;
113+
// This is not true in ShadyDOM:
114+
// assert.instanceOf(divs, NodeList);
115+
assert.lengthOf(spans, 0);
116+
});
117+
});
118+
});

test/runner.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@
1616
<body>
1717
<script type="module" src="lit-element_test.js"></script>
1818
<script type="module" src="lit-element_styling_test.js"></script>
19+
<script type="module" src="lib/decorators_test.js"></script>
1920
</body></html>

0 commit comments

Comments
 (0)