From a4a2f090eb604716b805324730738cdf6a2985f0 Mon Sep 17 00:00:00 2001 From: Steven Orvell Date: Tue, 11 Feb 2020 15:34:43 -0800 Subject: [PATCH 1/9] Adds `asyncQuery(selector)` decorator Returns a promise this which resolves to the result of `querySelector` the given `selector` performed after the element's `updateComplete` promise is resolved. --- src/lib/decorators.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/lib/decorators.ts b/src/lib/decorators.ts index cc62160a..38cc0153 100644 --- a/src/lib/decorators.ts +++ b/src/lib/decorators.ts @@ -225,6 +225,48 @@ export function query(selector: string) { }; } +/** + * A property decorator that converts a class property into a getter that + * returns a promise that resolves to the result of a querySelector on the + * element's renderRoot done after the element's `updateComplete` promise + * resolves. + * + * @param selector A DOMString containing one or more selectors to match. + * + * See: https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector + * + * @example + * + * class MyElement { + * @asyncQuery('#first') + * first; + * + * render() { + * return html` + *
+ *
+ * `; + * } + * } + */ +export function asyncQuery(selector: string) { + return (protoOrDescriptor: Object|ClassElement, + // tslint:disable-next-line:no-any decorator + name?: PropertyKey): any => { + const descriptor = { + async get(this: LitElement) { + await this.updateComplete; + return this.renderRoot.querySelector(selector); + }, + enumerable: true, + configurable: true, + }; + return (name !== undefined) ? + legacyQuery(descriptor, protoOrDescriptor as Object, name) : + standardQuery(descriptor, protoOrDescriptor as ClassElement); + }; +} + /** * A property decorator that converts a class property into a getter * that executes a querySelectorAll on the element's renderRoot. From 62214717aaede26870a8dd2028c34ff745023fe4 Mon Sep 17 00:00:00 2001 From: Steven Orvell Date: Fri, 21 Feb 2020 16:27:49 -0800 Subject: [PATCH 2/9] asyncQuery resolves only when the queried element's updateComplete resolves. --- src/lib/decorators.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/decorators.ts b/src/lib/decorators.ts index 38cc0153..c619a1fc 100644 --- a/src/lib/decorators.ts +++ b/src/lib/decorators.ts @@ -229,7 +229,8 @@ export function query(selector: string) { * A property decorator that converts a class property into a getter that * returns a promise that resolves to the result of a querySelector on the * element's renderRoot done after the element's `updateComplete` promise - * resolves. + * resolves and after the queried element's `updateComplete` promise resolves + * (if it is a LitElement). * * @param selector A DOMString containing one or more selectors to match. * @@ -256,7 +257,10 @@ export function asyncQuery(selector: string) { const descriptor = { async get(this: LitElement) { await this.updateComplete; - return this.renderRoot.querySelector(selector); + const el = this.renderRoot.querySelector(selector); + // if this is a LitElement, then await updateComplete + await (el && (el as LitElement).updateComplete); + return el; }, enumerable: true, configurable: true, From 899a5395a417ee188399af0baab738cc1d75bc4a Mon Sep 17 00:00:00 2001 From: Steven Orvell Date: Mon, 24 Feb 2020 15:20:13 -0800 Subject: [PATCH 3/9] Tests for asyncQuery --- src/test/lib/decorators_test.ts | 73 ++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/src/test/lib/decorators_test.ts b/src/test/lib/decorators_test.ts index 8640102f..a66ba1a0 100644 --- a/src/test/lib/decorators_test.ts +++ b/src/test/lib/decorators_test.ts @@ -13,7 +13,7 @@ */ import {eventOptions, property} from '../../lib/decorators.js'; -import {customElement, html, LitElement, PropertyValues, query, queryAll, queryAssignedNodes} from '../../lit-element.js'; +import {customElement, html, LitElement, PropertyValues, query, queryAll, queryAssignedNodes, asyncQuery} from '../../lit-element.js'; import {generateElementName} from '../test-helpers.js'; const flush = @@ -460,6 +460,77 @@ suite('decorators', () => { }); }); + suite('@asyncQuery', () => { + @customElement('dep-el' as keyof HTMLElementTagNameMap) + class D extends LitElement { + + wasUpdated = false; + + render() { + return html`dep`; + } + + firstUpdated() { + this.wasUpdated = true; + } + } + + + @customElement(generateElementName() as keyof HTMLElementTagNameMap) + class C extends LitElement { + @asyncQuery('#blah') blah!: Promise; + + @asyncQuery('dep-el') dep!: Promise; + + @asyncQuery('span') nope!: Promise; + + @property() + foo = false; + + render() { + return html` +
Not this one
+ ${this.foo ? + html`
This one
` : + html`
This one
` } + `; + } + } + + test('returns an element when it exists after update', async () => { + const c = new C(); + container.appendChild(c); + let div = await c.blah; + assert.instanceOf(div, HTMLDivElement); + assert.isFalse(div.hasAttribute('foo')); + c.foo = true; + div = await c.blah; + assert.instanceOf(div, HTMLDivElement); + assert.isTrue(div.hasAttribute('foo')); + }); + + test('returns an element when it exists after update that is itself updated', async () => { + const c = new C(); + container.appendChild(c); + let dep = await c.dep; + assert.instanceOf(dep, D); + assert.isFalse(dep.hasAttribute('foo')); + assert.isTrue(dep.wasUpdated); + c.foo = true; + dep = await c.dep; + assert.instanceOf(dep, D); + assert.isTrue(dep.hasAttribute('foo')); + assert.isTrue(dep.wasUpdated); + }); + + test('returns null when no match', async () => { + const c = new C(); + container.appendChild(c); + const span = await c.nope; + assert.isNull(span); + }); + }); + suite('@eventOptions', () => { test('allows capturing listeners', async function() { if (!supportsOptions) { From 9120ac3c043ee1c80a59f0ed327cc0d968fd0c1f Mon Sep 17 00:00:00 2001 From: Steven Orvell Date: Mon, 24 Feb 2020 16:07:36 -0800 Subject: [PATCH 4/9] Avoid awaiting a null result --- src/lib/decorators.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/decorators.ts b/src/lib/decorators.ts index c619a1fc..5970f166 100644 --- a/src/lib/decorators.ts +++ b/src/lib/decorators.ts @@ -259,7 +259,9 @@ export function asyncQuery(selector: string) { await this.updateComplete; const el = this.renderRoot.querySelector(selector); // if this is a LitElement, then await updateComplete - await (el && (el as LitElement).updateComplete); + if (el) { + await (el as LitElement).updateComplete; + } return el; }, enumerable: true, From 6451733b59bb57d836451e48d70a73e41d98f8b2 Mon Sep 17 00:00:00 2001 From: Steven Orvell Date: Thu, 27 Feb 2020 14:07:45 -0800 Subject: [PATCH 5/9] Address review feedback. --- CHANGELOG.md | 1 + src/lib/decorators.ts | 17 +++++++++-------- src/test/lib/decorators_test.ts | 34 ++------------------------------- 3 files changed, 12 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9ad5ee5..f7810b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). * The value returned by `render` is always rendered, even if it isn't a `TemplateResult`. ([#712](https://github.com/Polymer/lit-element/issues/712) ### Added +* Added `@asyncQuery(selector)` decorator which returns a Promise resolves to the result of querying for the given selector after the element's `updateComplete` Promise resolves ([#903](https://github.com/Polymer/lit-element/issues/903)). * Added `enableUpdating()` to `UpdatingElement` to enable customizing when updating is enabled [#860](https://github.com/Polymer/lit-element/pull/860). * Added `queryAssignedNodes(slotName, flatten)` to enable querying assignedNodes for a given slot [#860](https://github.com/Polymer/lit-element/pull/860). * Added `getStyles()` to `LitElement` to allow hooks into style gathering for component sets [#866](https://github.com/Polymer/lit-element/pull/866). diff --git a/src/lib/decorators.ts b/src/lib/decorators.ts index 5970f166..fcdfdd5e 100644 --- a/src/lib/decorators.ts +++ b/src/lib/decorators.ts @@ -207,6 +207,11 @@ export function internalProperty(options?: InternalPropertyDeclaration) { * `; * } * } + * + * // external usage + * async doSomethingWithFirst() { + * (await aMyElement.first).doSomething(); + * } */ export function query(selector: string) { return (protoOrDescriptor: Object|ClassElement, @@ -229,8 +234,9 @@ export function query(selector: string) { * A property decorator that converts a class property into a getter that * returns a promise that resolves to the result of a querySelector on the * element's renderRoot done after the element's `updateComplete` promise - * resolves and after the queried element's `updateComplete` promise resolves - * (if it is a LitElement). + * resolves. When queried property may change with element state, this decorator + * can be used instead of requiring users to await the `updateComplete` before + * accessing the property. * * @param selector A DOMString containing one or more selectors to match. * @@ -257,12 +263,7 @@ export function asyncQuery(selector: string) { const descriptor = { async get(this: LitElement) { await this.updateComplete; - const el = this.renderRoot.querySelector(selector); - // if this is a LitElement, then await updateComplete - if (el) { - await (el as LitElement).updateComplete; - } - return el; + return this.renderRoot.querySelector(selector); }, enumerable: true, configurable: true, diff --git a/src/test/lib/decorators_test.ts b/src/test/lib/decorators_test.ts index a66ba1a0..e257ebad 100644 --- a/src/test/lib/decorators_test.ts +++ b/src/test/lib/decorators_test.ts @@ -461,27 +461,11 @@ suite('decorators', () => { }); suite('@asyncQuery', () => { - @customElement('dep-el' as keyof HTMLElementTagNameMap) - class D extends LitElement { - - wasUpdated = false; - - render() { - return html`dep`; - } - - firstUpdated() { - this.wasUpdated = true; - } - } - @customElement(generateElementName() as keyof HTMLElementTagNameMap) class C extends LitElement { @asyncQuery('#blah') blah!: Promise; - @asyncQuery('dep-el') dep!: Promise; - @asyncQuery('span') nope!: Promise; @property() @@ -491,8 +475,8 @@ suite('decorators', () => { return html`
Not this one
${this.foo ? - html`
This one
` : - html`
This one
` } + html`
This one
` : + html`
This one
` } `; } } @@ -509,20 +493,6 @@ suite('decorators', () => { assert.isTrue(div.hasAttribute('foo')); }); - test('returns an element when it exists after update that is itself updated', async () => { - const c = new C(); - container.appendChild(c); - let dep = await c.dep; - assert.instanceOf(dep, D); - assert.isFalse(dep.hasAttribute('foo')); - assert.isTrue(dep.wasUpdated); - c.foo = true; - dep = await c.dep; - assert.instanceOf(dep, D); - assert.isTrue(dep.hasAttribute('foo')); - assert.isTrue(dep.wasUpdated); - }); - test('returns null when no match', async () => { const c = new C(); container.appendChild(c); From b396c933dcfac3d44226b434282e6946b3cebbe6 Mon Sep 17 00:00:00 2001 From: Steven Orvell Date: Thu, 27 Feb 2020 17:20:56 -0800 Subject: [PATCH 6/9] Adds comment about an `@asyncQuery` use case we may support in the future. --- src/lib/decorators.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/decorators.ts b/src/lib/decorators.ts index fcdfdd5e..6a6b591f 100644 --- a/src/lib/decorators.ts +++ b/src/lib/decorators.ts @@ -230,6 +230,11 @@ export function query(selector: string) { }; } +// Note, in the future, we may extend this decorator to support the use case +// where the queried element may need to do work to become ready to interact +// with (e.g. load some implementation code). If so, we might elect to +// add a second argument defining a function that can be run to make the +// queried element loaded/updated/ready. /** * A property decorator that converts a class property into a getter that * returns a promise that resolves to the result of a querySelector on the From 9071b908aad88bce3da10c7e1f2f006798e9bc41 Mon Sep 17 00:00:00 2001 From: Steven Orvell Date: Fri, 28 Feb 2020 09:38:38 -0800 Subject: [PATCH 7/9] Renamed decorator to `queryAsync` based on review feedback. --- CHANGELOG.md | 2 +- src/lib/decorators.ts | 4 ++-- src/test/lib/decorators_test.ts | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7810b41..7465630e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). * The value returned by `render` is always rendered, even if it isn't a `TemplateResult`. ([#712](https://github.com/Polymer/lit-element/issues/712) ### Added -* Added `@asyncQuery(selector)` decorator which returns a Promise resolves to the result of querying for the given selector after the element's `updateComplete` Promise resolves ([#903](https://github.com/Polymer/lit-element/issues/903)). +* Added `@queryAsync(selector)` decorator which returns a Promise that resolves to the result of querying for the given selector after the element's `updateComplete` Promise resolves ([#903](https://github.com/Polymer/lit-element/issues/903)). * Added `enableUpdating()` to `UpdatingElement` to enable customizing when updating is enabled [#860](https://github.com/Polymer/lit-element/pull/860). * Added `queryAssignedNodes(slotName, flatten)` to enable querying assignedNodes for a given slot [#860](https://github.com/Polymer/lit-element/pull/860). * Added `getStyles()` to `LitElement` to allow hooks into style gathering for component sets [#866](https://github.com/Polymer/lit-element/pull/866). diff --git a/src/lib/decorators.ts b/src/lib/decorators.ts index 6a6b591f..f18a9644 100644 --- a/src/lib/decorators.ts +++ b/src/lib/decorators.ts @@ -250,7 +250,7 @@ export function query(selector: string) { * @example * * class MyElement { - * @asyncQuery('#first') + * @queryAsync('#first') * first; * * render() { @@ -261,7 +261,7 @@ export function query(selector: string) { * } * } */ -export function asyncQuery(selector: string) { +export function queryAsync(selector: string) { return (protoOrDescriptor: Object|ClassElement, // tslint:disable-next-line:no-any decorator name?: PropertyKey): any => { diff --git a/src/test/lib/decorators_test.ts b/src/test/lib/decorators_test.ts index e257ebad..cd79eff4 100644 --- a/src/test/lib/decorators_test.ts +++ b/src/test/lib/decorators_test.ts @@ -13,7 +13,7 @@ */ import {eventOptions, property} from '../../lib/decorators.js'; -import {customElement, html, LitElement, PropertyValues, query, queryAll, queryAssignedNodes, asyncQuery} from '../../lit-element.js'; +import {customElement, html, LitElement, PropertyValues, query, queryAll, queryAssignedNodes, queryAsync} from '../../lit-element.js'; import {generateElementName} from '../test-helpers.js'; const flush = @@ -460,13 +460,13 @@ suite('decorators', () => { }); }); - suite('@asyncQuery', () => { + suite('@queryAsync', () => { @customElement(generateElementName() as keyof HTMLElementTagNameMap) class C extends LitElement { - @asyncQuery('#blah') blah!: Promise; + @queryAsync('#blah') blah!: Promise; - @asyncQuery('span') nope!: Promise; + @queryAsync('span') nope!: Promise; @property() foo = false; From 8a6dc474ed1168313de760ea8cdaacfddd6cbe28 Mon Sep 17 00:00:00 2001 From: Steven Orvell Date: Fri, 28 Feb 2020 09:42:39 -0800 Subject: [PATCH 8/9] Fix typo. --- src/lib/decorators.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/decorators.ts b/src/lib/decorators.ts index f18a9644..a096f707 100644 --- a/src/lib/decorators.ts +++ b/src/lib/decorators.ts @@ -239,9 +239,9 @@ export function query(selector: string) { * A property decorator that converts a class property into a getter that * returns a promise that resolves to the result of a querySelector on the * element's renderRoot done after the element's `updateComplete` promise - * resolves. When queried property may change with element state, this decorator - * can be used instead of requiring users to await the `updateComplete` before - * accessing the property. + * resolves. When the queried property may change with element state, this + * decorator can be used instead of requiring users to await the + * `updateComplete` before accessing the property. * * @param selector A DOMString containing one or more selectors to match. * From ec0f2dc166d6d7a2c50c71d272dfeb86162cdbac Mon Sep 17 00:00:00 2001 From: Steven Orvell Date: Fri, 28 Feb 2020 09:52:35 -0800 Subject: [PATCH 9/9] Correct docs --- src/lib/decorators.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lib/decorators.ts b/src/lib/decorators.ts index a096f707..001036e6 100644 --- a/src/lib/decorators.ts +++ b/src/lib/decorators.ts @@ -208,10 +208,6 @@ export function internalProperty(options?: InternalPropertyDeclaration) { * } * } * - * // external usage - * async doSomethingWithFirst() { - * (await aMyElement.first).doSomething(); - * } */ export function query(selector: string) { return (protoOrDescriptor: Object|ClassElement, @@ -260,6 +256,11 @@ export function query(selector: string) { * `; * } * } + * + * // external usage + * async doSomethingWithFirst() { + * (await aMyElement.first).doSomething(); + * } */ export function queryAsync(selector: string) { return (protoOrDescriptor: Object|ClassElement,