Skip to content

Commit 0277045

Browse files
fix(core): ssr events (#2891)
* chore: nvm use lts * chore: bump lit-labs/ssr * chore: bump lit/context * fix: update context usage for ssr * fix: use lit-ssr html function * fix(core): rely on lit's dom-shim * chore: update and align lit version * fix(core): ssr connected callback * fix(elements): ssr connected callbacks * fix(elements): table th role from context instead of dom * feat(core): wip slots decorator * fix(core): empty array check * fix(core): remove need for static decorator for ssr slot hints * fix: revert slot controller changes This will be addressed in a later PR * fix: keep isServer in client-side slot controller pending later PR * refactor(tools): readability * docs: major changeset * fix(core): add isServer to scrollspy controller * Revert "fix(core): add isServer to scrollspy controller" This reverts commit 701f33f. --------- Co-authored-by: Steven Spriggs <[email protected]>
1 parent 57a5c57 commit 0277045

File tree

22 files changed

+217
-165
lines changed

22 files changed

+217
-165
lines changed

.changeset/clear-pugs-make.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
"@patternfly/pfe-core": major
3+
"@patternfly/elements": patch
4+
---
5+
Enable `connectedCallback()` and context protocol in SSR scenarios.
6+
7+
BREAKING CHANGE
8+
This change affects any element which is expected to execute in node JS when
9+
lit-ssr shims are present. By enabling the `connectedCallback()` to execute
10+
server side. Elements must ensure that their connectedCallbacks do not try to
11+
access the DOM.
12+
13+
Before:
14+
15+
```js
16+
connectedCallback() {
17+
super.connectedCallback();
18+
this.items = this.querySelectorAll('my-item');
19+
}
20+
```
21+
22+
After:
23+
```js
24+
connectedCallback() {
25+
super.connectedCallback();
26+
if (!isServer) {
27+
this.items = this.querySelectorAll('my-item');
28+
}
29+
}
30+
```
31+

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v20.10.0
1+
v22.13.0

core/pfe-core/controllers/light-dom-controller.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ReactiveController, ReactiveElement } from 'lit';
1+
import { isServer, type ReactiveController, type ReactiveElement } from 'lit';
22

33
import { Logger } from './logger.js';
44

@@ -52,9 +52,13 @@ export class LightDOMController implements ReactiveController {
5252
* Returns a boolean statement of whether or not this component contains any light DOM.
5353
*/
5454
hasLightDOM(): boolean {
55-
return !!(
56-
this.host.children.length > 0
57-
|| (this.host.textContent ?? '').trim().length > 0
58-
);
55+
if (isServer) {
56+
return false;
57+
} else {
58+
return !!(
59+
this.host.children.length > 0
60+
|| (this.host.textContent ?? '').trim().length > 0
61+
);
62+
}
5963
}
6064
}

core/pfe-core/controllers/scroll-spy-controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export class ScrollSpyController implements ReactiveController {
6161
#threshold: number | number[];
6262
#intersectingElements: Element[] = [];
6363

64-
#getRootNode: () => Node;
64+
#getRootNode: () => Node | null;
6565
#getHash: (el: Element) => string | null;
6666
#onIntersection?: () => void;
6767

@@ -110,7 +110,7 @@ export class ScrollSpyController implements ReactiveController {
110110
this.#rootMargin = options.rootMargin;
111111
this.#activeAttribute = options.activeAttribute ?? 'active';
112112
this.#threshold = options.threshold ?? 0.85;
113-
this.#getRootNode = () => options.rootNode ?? host.getRootNode();
113+
this.#getRootNode = () => options.rootNode ?? host.getRootNode?.() ?? null;
114114
this.#getHash = options?.getHash ?? ((el: Element) => el.getAttribute('href'));
115115
this.#onIntersection = options?.onIntersection;
116116
}
Lines changed: 69 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { isServer, type ReactiveController, type ReactiveElement } from 'lit';
22

3-
import { Logger } from './logger.js';
4-
53
interface AnonymousSlot {
64
hasContent: boolean;
75
elements: Element[];
@@ -15,8 +13,10 @@ interface NamedSlot extends AnonymousSlot {
1513

1614
export type Slot = NamedSlot | AnonymousSlot;
1715

16+
export type SlotName = string | null;
17+
1818
export interface SlotsConfig {
19-
slots: (string | null)[];
19+
slots: SlotName[];
2020
/**
2121
* Object mapping new slot name keys to deprecated slot name values
2222
* @example `pf-modal--header` is deprecated in favour of `header`
@@ -32,9 +32,9 @@ export interface SlotsConfig {
3232
deprecations?: Record<string, string>;
3333
}
3434

35-
function isObjectConfigSpread(
36-
config: ([SlotsConfig] | (string | null)[]),
37-
): config is [SlotsConfig] {
35+
export type SlotControllerArgs = [SlotsConfig] | SlotName[];
36+
37+
export function isObjectSpread(config: SlotControllerArgs): config is [SlotsConfig] {
3838
return config.length === 1 && typeof config[0] === 'object' && config[0] !== null;
3939
}
4040

@@ -57,58 +57,92 @@ export class SlotController implements ReactiveController {
5757

5858
#nodes = new Map<string | typeof SlotController.default, Slot>();
5959

60-
#logger: Logger;
61-
62-
#firstUpdated = false;
60+
#slotMapInitialized = false;
6361

64-
#mo = new MutationObserver(records => this.#onMutation(records));
65-
66-
#slotNames: (string | null)[];
62+
#slotNames: (string | null)[] = [];
6763

6864
#deprecations: Record<string, string> = {};
6965

70-
constructor(public host: ReactiveElement, ...config: ([SlotsConfig] | (string | null)[])) {
71-
this.#logger = new Logger(this.host);
66+
#mo = new MutationObserver(this.#initSlotMap.bind(this));
7267

73-
if (isObjectConfigSpread(config)) {
68+
constructor(public host: ReactiveElement, ...args: SlotControllerArgs) {
69+
this.#initialize(...args);
70+
host.addController(this);
71+
if (!this.#slotNames.length) {
72+
this.#slotNames = [null];
73+
}
74+
}
75+
76+
#initialize(...config: SlotControllerArgs) {
77+
if (isObjectSpread(config)) {
7478
const [{ slots, deprecations }] = config;
7579
this.#slotNames = slots;
7680
this.#deprecations = deprecations ?? {};
7781
} else if (config.length >= 1) {
7882
this.#slotNames = config;
7983
this.#deprecations = {};
80-
} else {
81-
this.#slotNames = [null];
8284
}
83-
84-
85-
host.addController(this);
8685
}
8786

8887
async hostConnected(): Promise<void> {
89-
this.host.addEventListener('slotchange', this.#onSlotChange as EventListener);
90-
this.#firstUpdated = false;
9188
this.#mo.observe(this.host, { childList: true });
9289
// Map the defined slots into an object that is easier to query
9390
this.#nodes.clear();
94-
// Loop over the properties provided by the schema
95-
this.#slotNames.forEach(this.#initSlot);
96-
Object.values(this.#deprecations).forEach(this.#initSlot);
97-
this.host.requestUpdate();
91+
this.#initSlotMap();
9892
// insurance for framework integrations
9993
await this.host.updateComplete;
10094
this.host.requestUpdate();
10195
}
10296

97+
hostDisconnected(): void {
98+
this.#mo.disconnect();
99+
}
100+
103101
hostUpdated(): void {
104-
if (!this.#firstUpdated) {
105-
this.#slotNames.forEach(this.#initSlot);
106-
this.#firstUpdated = true;
102+
if (!this.#slotMapInitialized) {
103+
this.#initSlotMap();
107104
}
108105
}
109106

110-
hostDisconnected(): void {
111-
this.#mo.disconnect();
107+
#initSlotMap() {
108+
// Loop over the properties provided by the schema
109+
for (const slotName of this.#slotNames
110+
.concat(Object.values(this.#deprecations))) {
111+
const slotId = slotName || SlotController.default;
112+
const name = slotName ?? '';
113+
const elements = this.#getChildrenForSlot(slotId);
114+
const slot = this.#getSlotElement(slotId);
115+
const hasContent =
116+
!isServer
117+
&& !!elements.length
118+
|| !!slot?.assignedNodes?.()?.filter(x => x.textContent?.trim()).length;
119+
this.#nodes.set(slotId, { elements, name, hasContent, slot });
120+
}
121+
this.host.requestUpdate();
122+
this.#slotMapInitialized = true;
123+
}
124+
125+
#getSlotElement(slotId: string | symbol) {
126+
if (isServer) {
127+
return null;
128+
} else {
129+
const selector =
130+
slotId === SlotController.default ? 'slot:not([name])' : `slot[name="${slotId as string}"]`;
131+
return this.host.shadowRoot?.querySelector?.<HTMLSlotElement>(selector) ?? null;
132+
}
133+
}
134+
135+
#getChildrenForSlot<T extends Element = Element>(
136+
name: string | typeof SlotController.default,
137+
): T[] {
138+
if (isServer) {
139+
return [];
140+
} else if (this.#nodes.has(name)) {
141+
return (this.#nodes.get(name)!.slot?.assignedElements?.() ?? []) as T[];
142+
} else {
143+
const children = Array.from(this.host.children) as T[];
144+
return children.filter(isSlot(name));
145+
}
112146
}
113147

114148
/**
@@ -143,19 +177,11 @@ export class SlotController implements ReactiveController {
143177
* @example this.hasSlotted('header');
144178
*/
145179
hasSlotted(...names: (string | null | undefined)[]): boolean {
146-
if (isServer) {
147-
return this.host
148-
.getAttribute('ssr-hint-has-slotted')
149-
?.split(',')
150-
.map(name => name.trim())
151-
.some(name => names.includes(name === 'default' ? null : name)) ?? false;
152-
} else {
153-
const slotNames = Array.from(names, x => x == null ? SlotController.default : x);
154-
if (!slotNames.length) {
155-
slotNames.push(SlotController.default);
156-
}
157-
return slotNames.some(x => this.#nodes.get(x)?.hasContent ?? false);
180+
const slotNames = Array.from(names, x => x == null ? SlotController.default : x);
181+
if (!slotNames.length) {
182+
slotNames.push(SlotController.default);
158183
}
184+
return slotNames.some(x => this.#nodes.get(x)?.hasContent ?? false);
159185
}
160186

161187
/**
@@ -168,42 +194,4 @@ export class SlotController implements ReactiveController {
168194
isEmpty(...names: (string | null | undefined)[]): boolean {
169195
return !this.hasSlotted(...names);
170196
}
171-
172-
#onSlotChange = (event: Event & { target: HTMLSlotElement }) => {
173-
const slotName = event.target.name;
174-
this.#initSlot(slotName);
175-
this.host.requestUpdate();
176-
};
177-
178-
#onMutation = async (records: MutationRecord[]) => {
179-
const changed = [];
180-
for (const { addedNodes, removedNodes } of records) {
181-
for (const node of [...addedNodes, ...removedNodes]) {
182-
if (node instanceof HTMLElement && node.slot) {
183-
this.#initSlot(node.slot);
184-
changed.push(node.slot);
185-
}
186-
}
187-
}
188-
this.host.requestUpdate();
189-
};
190-
191-
#getChildrenForSlot<T extends Element = Element>(
192-
name: string | typeof SlotController.default,
193-
): T[] {
194-
const children = Array.from(this.host.children) as T[];
195-
return children.filter(isSlot(name));
196-
}
197-
198-
#initSlot = (slotName: string | null) => {
199-
const name = slotName || SlotController.default;
200-
const elements = this.#nodes.get(name)?.slot?.assignedElements?.()
201-
?? this.#getChildrenForSlot(name);
202-
const selector = slotName ? `slot[name="${slotName}"]` : 'slot:not([name])';
203-
const slot = this.host.shadowRoot?.querySelector?.<HTMLSlotElement>(selector) ?? null;
204-
const nodes = slot?.assignedNodes?.();
205-
const hasContent = !!elements.length || !!nodes?.filter(x => x.textContent?.trim()).length;
206-
this.#nodes.set(name, { elements, name: slotName ?? '', hasContent, slot });
207-
this.#logger.debug(slotName, hasContent);
208-
};
209197
}

core/pfe-core/functions/context.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ function makeContextRoot() {
77
const root = new ContextRoot();
88
if (!isServer) {
99
root.attach(document.body);
10+
} else {
11+
root.attach(
12+
// @ts-expect-error: enable context root in ssr
13+
globalThis.litServerRoot,
14+
);
1015
}
1116
return root;
1217
}

core/pfe-core/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@
3232
"./controllers/property-observer-controller.js": "./controllers/property-observer-controller.js",
3333
"./controllers/roving-tabindex-controller.js": "./controllers/roving-tabindex-controller.js",
3434
"./controllers/scroll-spy-controller.js": "./controllers/scroll-spy-controller.js",
35-
"./controllers/slot-controller.js": "./controllers/slot-controller.js",
35+
"./controllers/slot-controller.js": {
36+
"import": "./controllers/slot-controller.js",
37+
"default": "./controllers/slot-controller.js"
38+
},
3639
"./controllers/style-controller.js": "./controllers/style-controller.js",
3740
"./controllers/timestamp-controller.js": "./controllers/timestamp-controller.js",
3841
"./controllers/tabs-controller.js": "./controllers/tabs-controller.js",
@@ -62,8 +65,8 @@
6265
},
6366
"dependencies": {
6467
"@floating-ui/dom": "^1.6.10",
65-
"@lit/context": "^1.1.2",
66-
"lit": "^3.2.0"
68+
"@lit/context": "^1.1.3",
69+
"lit": "^3.2.1"
6770
},
6871
"repository": {
6972
"type": "git",

0 commit comments

Comments
 (0)