Skip to content

Commit

Permalink
Add queryAssignedElements decorator (#2327)
Browse files Browse the repository at this point in the history
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
AndrewJakubowicz committed Dec 7, 2021
1 parent 93d8751 commit 49ecf62
Show file tree
Hide file tree
Showing 16 changed files with 887 additions and 10 deletions.
8 changes: 8 additions & 0 deletions .changeset/famous-feet-kneel.md
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.
1 change: 1 addition & 0 deletions packages/labs/ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"lit": "^2.0.0",
"lit-element": "^3.0.0",
"lit-html": "^2.0.0",
"@lit/reactive-element": "1.0.2",
"node-fetch": "^2.6.0",
"parse5": "^6.0.1",
"resolve": "^1.10.1"
Expand Down
4 changes: 4 additions & 0 deletions packages/lit-element/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
"development": "./development/decorators/query-all.js",
"default": "./decorators/query-all.js"
},
"./decorators/query-assigned-elements.js": {
"development": "./development/decorators/query-assigned-elements.js",
"default": "./decorators/query-assigned-elements.js"
},
"./decorators/query-assigned-nodes.js": {
"development": "./development/decorators/query-assigned-nodes.js",
"default": "./decorators/query-assigned-nodes.js"
Expand Down
1 change: 1 addition & 0 deletions packages/lit-element/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export * from '@lit/reactive-element/decorators/event-options.js';
export * from '@lit/reactive-element/decorators/query.js';
export * from '@lit/reactive-element/decorators/query-all.js';
export * from '@lit/reactive-element/decorators/query-async.js';
export * from '@lit/reactive-element/decorators/query-assigned-elements.js';
export * from '@lit/reactive-element/decorators/query-assigned-nodes.js';
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';
3 changes: 3 additions & 0 deletions packages/lit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
"./decorators/query-all.js": {
"default": "./decorators/query-all.js"
},
"./decorators/query-assigned-elements.js": {
"default": "./decorators/query-assigned-elements.js"
},
"./decorators/query-assigned-nodes.js": {
"default": "./decorators/query-assigned-nodes.js"
},
Expand Down
1 change: 1 addition & 0 deletions packages/lit/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export * from '@lit/reactive-element/decorators/event-options.js';
export * from '@lit/reactive-element/decorators/query.js';
export * from '@lit/reactive-element/decorators/query-all.js';
export * from '@lit/reactive-element/decorators/query-async.js';
export * from '@lit/reactive-element/decorators/query-assigned-elements.js';
export * from '@lit/reactive-element/decorators/query-assigned-nodes.js';
7 changes: 7 additions & 0 deletions packages/lit/src/decorators/query-assigned-elements.ts
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';
4 changes: 4 additions & 0 deletions packages/reactive-element/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
"development": "./development/decorators/query-all.js",
"default": "./decorators/query-all.js"
},
"./decorators/query-assigned-elements.js": {
"development": "./development/decorators/query-assigned-elements.js",
"default": "./decorators/query-assigned-elements.js"
},
"./decorators/query-assigned-nodes.js": {
"development": "./development/decorators/query-assigned-nodes.js",
"default": "./decorators/query-assigned-nodes.js"
Expand Down
1 change: 1 addition & 0 deletions packages/reactive-element/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export * from './decorators/event-options.js';
export * from './decorators/query.js';
export * from './decorators/query-all.js';
export * from './decorators/query-async.js';
export * from './decorators/query-assigned-elements.js';
export * from './decorators/query-assigned-nodes.js';
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,
}),
});
}
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, []);
});
});
21 changes: 11 additions & 10 deletions packages/ts-transformers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,17 @@ customElements.define('simple-greeting', SimpleGreeting);

#### Supported decorators

| Decorator | Transformer behavior |
| --------------------- | ------------------------------------------------------------------------------------- |
| `@customElement` | Adds a `customElements.define` call |
| `@property` | Adds an entry to `static properties`, and moves initializers to the `constructor` |
| `@state` | Same as `@property` with `{state: true}` |
| `@query` | Defines a getter that calls `querySelector` |
| `@querySelectorAll` | Defines a getter that calls `querySelectorAll` |
| `@queryAsync` | Defines an `async` getter that awaits `updateComplete` and then calls `querySelector` |
| `@queryAssignedNodes` | Defines a getter that calls `querySelector('slot[name=foo]').assignedNodes` |
| `@localized` | Adds an `updateWhenLocaleChanges` call to the constructor |
| Decorator | Transformer behavior |
| ------------------------ | ------------------------------------------------------------------------------------- |
| `@customElement` | Adds a `customElements.define` call |
| `@property` | Adds an entry to `static properties`, and moves initializers to the `constructor` |
| `@state` | Same as `@property` with `{state: true}` |
| `@query` | Defines a getter that calls `querySelector` |
| `@querySelectorAll` | Defines a getter that calls `querySelectorAll` |
| `@queryAsync` | Defines an `async` getter that awaits `updateComplete` and then calls `querySelector` |
| `@queryAssignedElements` | Defines a getter that calls `querySelector('slot[name=foo]').assignedElements` |
| `@queryAssignedNodes` | Defines a getter that calls `querySelector('slot[name=foo]').assignedNodes` |
| `@localized` | Adds an `updateWhenLocaleChanges` call to the constructor |

### preserveBlankLinesTransformer

Expand Down
2 changes: 2 additions & 0 deletions packages/ts-transformers/src/idiomatic-decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {StateVisitor} from './internal/decorators/state.js';
import {QueryVisitor} from './internal/decorators/query.js';
import {QueryAllVisitor} from './internal/decorators/query-all.js';
import {QueryAsyncVisitor} from './internal/decorators/query-async.js';
import {QueryAssignedElementsVisitor} from './internal/decorators/query-assigned-elements.js';
import {QueryAssignedNodesVisitor} from './internal/decorators/query-assigned-nodes.js';
import {EventOptionsVisitor} from './internal/decorators/event-options.js';
import {LocalizedVisitor} from './internal/decorators/localized.js';
Expand Down Expand Up @@ -65,6 +66,7 @@ export function idiomaticDecoratorsTransformer(
new QueryVisitor(context),
new QueryAllVisitor(context),
new QueryAsyncVisitor(context),
new QueryAssignedElementsVisitor(context),
new QueryAssignedNodesVisitor(context),
new EventOptionsVisitor(context, program),
new LocalizedVisitor(context),
Expand Down

0 comments on commit 49ecf62

Please sign in to comment.