Skip to content

Commit b64a333

Browse files
committed
fix(core): slot controller
1 parent 966a03b commit b64a333

File tree

2 files changed

+113
-82
lines changed

2 files changed

+113
-82
lines changed

core/pfe-core/controllers/slot-controller.ts

Lines changed: 63 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,6 @@ function isContent(node: Node) {
4949
}
5050
}
5151

52-
/**
53-
* If it's a named slot, return its children,
54-
* for the default slot, look for direct children not assigned to a slot
55-
* @param n slot name
56-
*/
57-
const isSlot =
58-
<T extends Element = Element>(n: string | typeof SlotController.default) =>
59-
(child: Element): child is T =>
60-
n === SlotController.default ? !child.hasAttribute('slot')
61-
: child.getAttribute('slot') === n;
62-
6352
export declare class SlotControllerPublicAPI implements ReactiveController {
6453
static default: symbol;
6554

@@ -109,45 +98,67 @@ export declare class SlotControllerPublicAPI implements ReactiveController {
10998
isEmpty(...names: (string | null | undefined)[]): boolean;
11099
}
111100

101+
class SlotRecord {
102+
constructor(
103+
public slot: HTMLSlotElement,
104+
public name: string | symbol,
105+
private host: ReactiveElement,
106+
) {}
107+
108+
get elements() {
109+
return this.slot?.assignedElements?.();
110+
}
111+
112+
get hasContent() {
113+
if (this.name === SlotController.default) {
114+
return !!this.elements.length
115+
|| !![...this.host.childNodes]
116+
.some(node => {
117+
if (node instanceof Element) {
118+
return !node.hasAttribute('slot');
119+
} else {
120+
return isContent(node);
121+
}
122+
});
123+
} else {
124+
return !!this.slot.assignedNodes()
125+
.some(isContent);
126+
}
127+
}
128+
}
129+
112130
export class SlotController implements SlotControllerPublicAPI {
113131
public static default = Symbol('default slot') satisfies symbol as symbol;
114132

115133
/** @deprecated use `default` */
116134
public static anonymous: symbol = this.default;
117135

118-
#nodes = new Map<string | typeof SlotController.default, Slot>();
136+
#slotRecords = new Map<string | typeof SlotController.default, SlotRecord>();
119137

120-
#slotMapInitialized = false;
121-
122-
#slotNames: (string | null)[] = [];
138+
#slotNames: (string | symbol | null)[] = [];
123139

124140
#deprecations: Record<string, string> = {};
125141

126142
#initSlotMap = async () => {
127143
const { host } = this;
128144
await host.updateComplete;
129-
const nodes = this.#nodes;
145+
const slotRecords = this.#slotRecords;
130146
// Loop over the properties provided by the schema
131-
for (const slotName of this.#slotNames
132-
.concat(Object.values(this.#deprecations))) {
133-
const slotId = slotName || SlotController.default;
134-
const name = slotName ?? '';
135-
const elements = this.#getChildrenForSlot(slotId);
136-
const slot = this.#getSlotElement(slotId);
137-
const hasContent =
138-
slotId === SlotController.default ? !![...host.childNodes].some(isContent)
139-
: !!slot?.assignedNodes?.().some(isContent);
140-
nodes.set(slotId, { elements, name, hasContent, slot });
147+
for (let slotName of this.#slotNames.concat(Object.values(this.#deprecations))) {
148+
slotName ||= SlotController.default;
149+
const slot = this.#getSlotElement(slotName);
150+
if (slot) {
151+
slotRecords.set(slotName, new SlotRecord(slot, slotName, host));
152+
}
141153
}
142154
host.requestUpdate();
143-
this.#slotMapInitialized = true;
144155
};
145156

146157
#mo = new MutationObserver(this.#initSlotMap);
147158

148159
constructor(public host: ReactiveElement, ...args: SlotControllerArgs) {
149-
this.#initialize(...args);
150160
host.addController(this);
161+
this.#initialize(...args);
151162
if (!this.#slotNames.length) {
152163
this.#slotNames = [null];
153164
}
@@ -164,43 +175,27 @@ export class SlotController implements SlotControllerPublicAPI {
164175
}
165176
}
166177

178+
#getSlotElement(slotId: string | symbol) {
179+
const selector =
180+
slotId === SlotController.default ? 'slot:not([name])' : `slot[name="${slotId as string}"]`;
181+
return this.host.shadowRoot?.querySelector?.<HTMLSlotElement>(selector) ?? null;
182+
}
183+
167184
async hostConnected(): Promise<void> {
168185
this.#mo.observe(this.host, { childList: true });
169186
// Map the defined slots into an object that is easier to query
170-
this.#nodes.clear();
187+
this.#slotRecords.clear();
188+
await this.host.updateComplete;
171189
this.#initSlotMap();
172190
// insurance for framework integrations
173191
await this.host.updateComplete;
174192
this.host.requestUpdate();
175193
}
176194

177-
hostUpdated(): void {
178-
if (!this.#slotMapInitialized) {
179-
this.#initSlotMap();
180-
}
181-
}
182-
183195
hostDisconnected(): void {
184196
this.#mo.disconnect();
185197
}
186198

187-
#getSlotElement(slotId: string | symbol) {
188-
const selector =
189-
slotId === SlotController.default ? 'slot:not([name])' : `slot[name="${slotId as string}"]`;
190-
return this.host.shadowRoot?.querySelector?.<HTMLSlotElement>(selector) ?? null;
191-
}
192-
193-
#getChildrenForSlot<T extends Element = Element>(
194-
name: string | typeof SlotController.default,
195-
): T[] {
196-
if (this.#nodes.has(name)) {
197-
return (this.#nodes.get(name)!.slot?.assignedElements?.() ?? []) as T[];
198-
} else {
199-
const children = Array.from(this.host.children) as T[];
200-
return children.filter(isSlot(name));
201-
}
202-
}
203-
204199
/**
205200
* Given a slot name or slot names, returns elements assigned to the requested slots as an array.
206201
* If no value is provided, it returns all children not assigned to a slot (without a slot attribute).
@@ -218,12 +213,12 @@ export class SlotController implements SlotControllerPublicAPI {
218213
* this.getSlotted();
219214
* ```
220215
*/
221-
getSlotted<T extends Element = Element>(...slotNames: string[]): T[] {
222-
if (!slotNames.length) {
223-
return (this.#nodes.get(SlotController.default)?.elements ?? []) as T[];
216+
public getSlotted<T extends Element = Element>(...slotNames: string[] | [null]): T[] {
217+
if (!slotNames.length || slotNames.length === 1 && slotNames.at(0) === null) {
218+
return (this.#slotRecords.get(SlotController.default)?.elements ?? []) as T[];
224219
} else {
225220
return slotNames.flatMap(slotName =>
226-
this.#nodes.get(slotName)?.elements ?? []) as T[];
221+
this.#slotRecords.get(slotName ?? SlotController.default)?.elements ?? []) as T[];
227222
}
228223
}
229224

@@ -232,12 +227,20 @@ export class SlotController implements SlotControllerPublicAPI {
232227
* @param names The slot names to check.
233228
* @example this.hasSlotted('header');
234229
*/
235-
hasSlotted(...names: (string | null | undefined)[]): boolean {
236-
const slotNames = Array.from(names, x => x == null ? SlotController.default : x);
230+
public hasSlotted(...names: (string | null | undefined)[]): boolean {
231+
const slotNames = Array.from(names, x =>
232+
x == null ? SlotController.default : x);
237233
if (!slotNames.length) {
238234
slotNames.push(SlotController.default);
239235
}
240-
return slotNames.some(x => this.#nodes.get(x)?.hasContent ?? false);
236+
return slotNames.some(slotName => {
237+
const slot = this.#slotRecords.get(slotName);
238+
if (!slot) {
239+
return false;
240+
} else {
241+
return slot.hasContent;
242+
}
243+
});
241244
}
242245

243246
/**
@@ -247,7 +250,7 @@ export class SlotController implements SlotControllerPublicAPI {
247250
* @example this.isEmpty();
248251
* @returns
249252
*/
250-
isEmpty(...names: (string | null | undefined)[]): boolean {
253+
public isEmpty(...names: (string | null | undefined)[]): boolean {
251254
return !this.hasSlotted(...names);
252255
}
253256
}

core/pfe-core/controllers/test/slot-controller.spec.ts

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,24 @@
11
import { expect, fixture, nextFrame } from '@open-wc/testing';
22

33
import { customElement } from 'lit/decorators/custom-element.js';
4-
import {
5-
ReactiveElement,
6-
html,
7-
render,
8-
type PropertyValues,
9-
type TemplateResult,
10-
} from 'lit';
4+
import { LitElement, html, type TemplateResult } from 'lit';
115

126
import { SlotController } from '../slot-controller.js';
137

8+
@customElement('test-slot-controller-with-named-and-anonymous')
9+
class TestSlotControllerWithNamedAndAnonymous extends LitElement {
10+
controller = new SlotController(this, 'a', null);
11+
render(): TemplateResult {
12+
return html`
13+
<slot name="a"></slot>
14+
<slot name="b"></slot>
15+
<slot></slot>
16+
`;
17+
}
18+
}
19+
1420
describe('SlotController', function() {
1521
describe('with named and anonymous slots', function() {
16-
@customElement('test-slot-controller-with-named-and-anonymous')
17-
class TestSlotControllerWithNamedAndAnonymous extends ReactiveElement {
18-
declare static template: TemplateResult;
19-
20-
controller = new SlotController(this, 'a', null);
21-
22-
render(): TemplateResult {
23-
return html`
24-
<slot name="a"></slot>
25-
<slot name="b"></slot>
26-
<slot></slot>
27-
`;
28-
}
29-
}
3022
describe('with no content', function() {
3123
let element: TestSlotControllerWithNamedAndAnonymous;
3224
beforeEach(async function() {
@@ -46,6 +38,15 @@ describe('SlotController', function() {
4638
expect(element.controller.hasSlotted()).to.be.false;
4739
expect(element.controller.isEmpty()).to.be.true;
4840
});
41+
it('returns empty list for getSlotted("a")', function() {
42+
expect(element.controller.getSlotted('a')).to.be.empty;
43+
});
44+
it('returns empty list for getSlotted(null)', function() {
45+
expect(element.controller.getSlotted(null)).to.be.empty;
46+
});
47+
it('returns empty list for getSlotted()', function() {
48+
expect(element.controller.getSlotted()).to.be.empty;
49+
});
4950
});
5051

5152
describe('with element content in default slot', function() {
@@ -69,6 +70,15 @@ describe('SlotController', function() {
6970
expect(element.controller.hasSlotted()).to.be.true;
7071
expect(element.controller.isEmpty()).to.be.false;
7172
});
73+
it('returns empty list for getSlotted("a")', function() {
74+
expect(element.controller.getSlotted('a')).to.be.empty;
75+
});
76+
it('returns lengthy list for getSlotted(null)', function() {
77+
expect(element.controller.getSlotted(null)).to.not.be.empty;
78+
});
79+
it('returns lengthy list for getSlotted()', function() {
80+
expect(element.controller.getSlotted()).to.not.be.empty;
81+
});
7282
});
7383

7484
describe('with element content in named slot', function() {
@@ -92,6 +102,15 @@ describe('SlotController', function() {
92102
expect(element.controller.hasSlotted()).to.be.false;
93103
expect(element.controller.isEmpty()).to.be.true;
94104
});
105+
it('returns lengthy list for getSlotted("a")', function() {
106+
expect(element.controller.getSlotted('a')).to.not.be.empty;
107+
});
108+
it('returns empty list for getSlotted(null)', function() {
109+
expect(element.controller.getSlotted(null)).to.be.empty;
110+
});
111+
it('returns empty list for getSlotted()', function() {
112+
expect(element.controller.getSlotted()).to.be.empty;
113+
});
95114
});
96115

97116
describe('with text content in default slot', function() {
@@ -107,14 +126,23 @@ describe('SlotController', function() {
107126
expect(element.controller.hasSlotted('a')).to.be.false;
108127
expect(element.controller.isEmpty('a')).to.be.true;
109128
});
110-
it('reports nonjgempty default slot', function() {
129+
it('reports non-empty default slot', function() {
111130
expect(element.controller.hasSlotted(null)).to.be.true;
112131
expect(element.controller.isEmpty(null)).to.be.false;
113132
});
114133
it('reports non-empty default slot with no arguments', function() {
115134
expect(element.controller.hasSlotted()).to.be.true;
116135
expect(element.controller.isEmpty()).to.be.false;
117136
});
137+
it('returns empty list for getSlotted("a")', function() {
138+
expect(element.controller.getSlotted('a')).to.be.empty;
139+
});
140+
it('returns lengthy list for getSlotted(null)', function() {
141+
expect(element.controller.getSlotted(null)).to.be.empty;
142+
});
143+
it('returns lengthy list for getSlotted()', function() {
144+
expect(element.controller.getSlotted()).to.be.empty;
145+
});
118146
});
119147
});
120148
});

0 commit comments

Comments
 (0)