-
Notifications
You must be signed in to change notification settings - Fork 879
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add queryAssignedElements decorator (#2327)
Fixes #2292 * Add queryAssignedElements decorator behaving as a declarative slot.assignedElements * Add QueryAssignedElementsVisitor to ts-transformers for new queryAssignedElements. * Code review feedback. * Add changeset * Unblock ssr test breakage caused by query-assigned-elements.js not being found.
- Loading branch information
1 parent
93d8751
commit 49ecf62
Showing
16 changed files
with
887 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
--- | ||
'lit': minor | ||
'lit-element': minor | ||
'@lit/reactive-element': minor | ||
'@lit/ts-transformers': minor | ||
--- | ||
|
||
Add `queryAssignedElements` decorator for a declarative API that calls `HTMLSlotElement.assignedElements()` on a specified slot. `selector` option allows filtering returned elements with a CSS selector. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
packages/lit-element/src/decorators/query-assigned-elements.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/** | ||
* @license | ||
* Copyright 2021 Google LLC | ||
* SPDX-License-Identifier: BSD-3-Clause | ||
*/ | ||
|
||
export * from '@lit/reactive-element/decorators/query-assigned-elements.js'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/** | ||
* @license | ||
* Copyright 2021 Google LLC | ||
* SPDX-License-Identifier: BSD-3-Clause | ||
*/ | ||
|
||
export * from '@lit/reactive-element/decorators/query-assigned-elements.js'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
packages/reactive-element/src/decorators/query-assigned-elements.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
/** | ||
* @license | ||
* Copyright 2021 Google LLC | ||
* SPDX-License-Identifier: BSD-3-Clause | ||
*/ | ||
|
||
/* | ||
* IMPORTANT: For compatibility with tsickle and the Closure JS compiler, all | ||
* property decorators (but not class decorators) in this file that have | ||
* an @ExportDecoratedItems annotation must be defined as a regular function, | ||
* not an arrow function. | ||
*/ | ||
|
||
import {ReactiveElement} from '../reactive-element.js'; | ||
import {decorateProperty} from './base.js'; | ||
|
||
export interface QueryAssignedElementsOptions extends AssignedNodesOptions { | ||
/** | ||
* Name of the slot. Leave empty for the default slot. | ||
*/ | ||
slot?: string; | ||
/** | ||
* CSS selector used to filter the elements returned. | ||
*/ | ||
selector?: string; | ||
} | ||
|
||
/** | ||
* A property decorator that converts a class property into a getter that | ||
* returns the `assignedElements` of the given `slot`. Provides a declarative | ||
* way to use | ||
* [`slot.assignedElements`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/assignedElements). | ||
* | ||
* Note, the type of this property should be annotated as `Array<HTMLElement>`. | ||
* | ||
* @param options Object that sets options for nodes to be returned. See | ||
* [MDN parameters section](https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/assignedElements#parameters) | ||
* for available options. Also accepts two more optional properties, | ||
* `slot` and `selector`. | ||
* @param options.slot Name of the slot. Undefined or empty string for the | ||
* default slot. | ||
* @param options.selector Element results are filtered such that they match the | ||
* given CSS selector. | ||
* | ||
* ```ts | ||
* class MyElement { | ||
* @queryAssignedElements({ slot: 'list' }) | ||
* listItems!: Array<HTMLElement>; | ||
* @queryAssignedElements() | ||
* unnamedSlotEls?: Array<HTMLElement>; | ||
* | ||
* render() { | ||
* return html` | ||
* <slot name="list"></slot> | ||
* <slot></slot> | ||
* `; | ||
* } | ||
* } | ||
* ``` | ||
* @category Decorator | ||
*/ | ||
export function queryAssignedElements(options?: QueryAssignedElementsOptions) { | ||
const {slot, selector} = options ?? {}; | ||
return decorateProperty({ | ||
descriptor: (_name: PropertyKey) => ({ | ||
get(this: ReactiveElement) { | ||
const slotSelector = `slot${slot ? `[name=${slot}]` : ':not([name])'}`; | ||
const slotEl = | ||
this.renderRoot?.querySelector<HTMLSlotElement>(slotSelector); | ||
const elements = slotEl?.assignedElements(options) ?? []; | ||
if (selector) { | ||
return elements.filter((node) => node.matches(selector)); | ||
} | ||
return elements; | ||
}, | ||
enumerable: true, | ||
configurable: true, | ||
}), | ||
}); | ||
} |
162 changes: 162 additions & 0 deletions
162
packages/reactive-element/src/test/decorators/queryAssignedElements_test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
/** | ||
* @license | ||
* Copyright 2021 Google LLC | ||
* SPDX-License-Identifier: BSD-3-Clause | ||
*/ | ||
|
||
import {queryAssignedElements} from '../../decorators/query-assigned-elements.js'; | ||
import {customElement} from '../../decorators/custom-element.js'; | ||
import { | ||
canTestReactiveElement, | ||
generateElementName, | ||
RenderingElement, | ||
html, | ||
} from '../test-helpers.js'; | ||
import {assert} from '@esm-bundle/chai'; | ||
|
||
const flush = | ||
window.ShadyDOM && window.ShadyDOM.flush ? window.ShadyDOM.flush : () => {}; | ||
|
||
(canTestReactiveElement ? suite : suite.skip)('@queryAssignedElements', () => { | ||
let container: HTMLElement; | ||
let el: C; | ||
|
||
@customElement('assigned-elements-el') | ||
class D extends RenderingElement { | ||
@queryAssignedElements() defaultAssigned!: Element[]; | ||
|
||
@queryAssignedElements({slot: 'footer', flatten: true}) | ||
footerAssigned!: Element[]; | ||
|
||
@queryAssignedElements({slot: 'footer', flatten: false}) | ||
footerNotFlattenedSlot!: Element[]; | ||
|
||
@queryAssignedElements({ | ||
slot: 'footer', | ||
flatten: true, | ||
selector: '.item', | ||
}) | ||
footerAssignedFiltered!: Element[]; | ||
|
||
override render() { | ||
return html` | ||
<slot></slot> | ||
<slot name="footer"></slot> | ||
`; | ||
} | ||
} | ||
|
||
const defaultSymbol = Symbol('default'); | ||
@customElement('assigned-elements-el-2') | ||
class E extends RenderingElement { | ||
@queryAssignedElements() [defaultSymbol]!: Element[]; | ||
|
||
@queryAssignedElements({slot: 'header'}) headerAssigned!: Element[]; | ||
|
||
override render() { | ||
return html` | ||
<slot name="header"></slot> | ||
<slot></slot> | ||
`; | ||
} | ||
} | ||
|
||
// Note, there are 2 elements here so that the `flatten` option of | ||
// the decorator can be tested. | ||
@customElement(generateElementName()) | ||
class C extends RenderingElement { | ||
div!: HTMLDivElement; | ||
div2!: HTMLDivElement; | ||
div3!: HTMLDivElement; | ||
assignedEls!: D; | ||
assignedEls2!: E; | ||
@queryAssignedElements() missingSlotAssignedElements!: Element[]; | ||
|
||
override render() { | ||
return html` | ||
<assigned-elements-el | ||
><div id="div1">A</div> | ||
<slot slot="footer"></slot | ||
></assigned-elements-el> | ||
<assigned-elements-el-2><div id="div2">B</div></assigned-elements-el-2> | ||
`; | ||
} | ||
|
||
override firstUpdated() { | ||
this.div = this.renderRoot.querySelector('#div1') as HTMLDivElement; | ||
this.div2 = this.renderRoot.querySelector('#div2') as HTMLDivElement; | ||
this.div3 = this.renderRoot.querySelector('#div3') as HTMLDivElement; | ||
this.assignedEls = this.renderRoot.querySelector( | ||
'assigned-elements-el' | ||
) as D; | ||
this.assignedEls2 = this.renderRoot.querySelector( | ||
'assigned-elements-el-2' | ||
) as E; | ||
} | ||
} | ||
|
||
setup(async () => { | ||
container = document.createElement('div'); | ||
document.body.append(container); | ||
el = new C(); | ||
container.append(el); | ||
await new Promise((r) => setTimeout(r, 0)); | ||
}); | ||
|
||
teardown(() => { | ||
container?.remove(); | ||
}); | ||
|
||
test('returns assignedElements for slot', () => { | ||
// Note, `defaultAssigned` does not `flatten` so we test that the property | ||
// reflects current state and state when nodes are added or removed. | ||
assert.deepEqual(el.assignedEls.defaultAssigned, [el.div]); | ||
const child = document.createElement('div'); | ||
const text1 = document.createTextNode(''); | ||
el.assignedEls.append(text1, child); | ||
const text2 = document.createTextNode(''); | ||
el.assignedEls.append(text2); | ||
flush(); | ||
assert.deepEqual(el.assignedEls.defaultAssigned, [el.div, child]); | ||
child.remove(); | ||
flush(); | ||
assert.deepEqual(el.assignedEls.defaultAssigned, [el.div]); | ||
}); | ||
|
||
test('returns assignedElements for unnamed slot that is not first slot', () => { | ||
assert.deepEqual(el.assignedEls2[defaultSymbol], [el.div2]); | ||
}); | ||
|
||
test('returns flattened assignedElements for slot', () => { | ||
assert.deepEqual(el.assignedEls.footerAssigned, []); | ||
const child1 = document.createElement('div'); | ||
const child2 = document.createElement('div'); | ||
el.append(child1, child2); | ||
flush(); | ||
assert.deepEqual(el.assignedEls.footerAssigned, [child1, child2]); | ||
|
||
assert.equal(el.assignedEls.footerNotFlattenedSlot.length, 1); | ||
assert.equal(el.assignedEls.footerNotFlattenedSlot?.[0]?.tagName, 'SLOT'); | ||
|
||
child2.remove(); | ||
flush(); | ||
assert.deepEqual(el.assignedEls.footerAssigned, [child1]); | ||
}); | ||
|
||
test('always returns an array, even if the slot is not rendered', () => { | ||
assert.isArray(el.missingSlotAssignedElements); | ||
}); | ||
|
||
test('returns assignedElements for slot filtered by selector', () => { | ||
assert.deepEqual(el.assignedEls.footerAssignedFiltered, []); | ||
const child1 = document.createElement('div'); | ||
const child2 = document.createElement('div'); | ||
child2.classList.add('item'); | ||
el.append(child1, child2); | ||
flush(); | ||
assert.deepEqual(el.assignedEls.footerAssignedFiltered, [child2]); | ||
child2.remove(); | ||
flush(); | ||
assert.deepEqual(el.assignedEls.footerAssignedFiltered, []); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.