Skip to content

Commit 996e089

Browse files
authored
feat(core): slot controller ssr hint attributes (#2893)
* 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(core): client-side slot controller doens't use isServer * fix(core): ssr-hint-has-default-slotted attr name * test: ssr demos backported from #2505 * chore: ssr dev packages * chore: deps * docs: build elements * docs: format * chore: revert ssr we'll have to either patch or reimplement like we did in rhds * style: lint * docs: remove preloads * docs: changeset
1 parent a2f3254 commit 996e089

File tree

12 files changed

+249
-128
lines changed

12 files changed

+249
-128
lines changed

.changeset/all-tigers-wear.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
"@patternfly/elements": patch
3+
---
4+
**SSR**: enable more elements and more features to be rendered server-side

.changeset/clear-pugs-make.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
---
22
"@patternfly/pfe-core": major
3-
"@patternfly/elements": patch
43
---
54
Enable `connectedCallback()` and context protocol in SSR scenarios.
65

@@ -15,7 +14,7 @@ Before:
1514
```js
1615
connectedCallback() {
1716
super.connectedCallback();
18-
this.items = this.querySelectorAll('my-item');
17+
this.items = [...this.querySelectorAll('my-item')];
1918
}
2019
```
2120

@@ -24,8 +23,7 @@ After:
2423
connectedCallback() {
2524
super.connectedCallback();
2625
if (!isServer) {
27-
this.items = this.querySelectorAll('my-item');
26+
this.items = isServer ? [] : [...this.querySelectorAll('my-item')];
2827
}
2928
}
3029
```
31-
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { ReactiveElement } from 'lit';
2+
import {
3+
type SlotControllerArgs,
4+
type SlotControllerPublicAPI,
5+
} from './slot-controller.js';
6+
7+
export class SlotController implements SlotControllerPublicAPI {
8+
public static default = Symbol('default slot') satisfies symbol as symbol;
9+
10+
/** @deprecated use `default` */
11+
public static anonymous: symbol = this.default;
12+
13+
static attribute = 'ssr-hint-has-slotted' as const;
14+
15+
static anonymousAttribute = 'ssr-hint-has-slotted-default' as const;
16+
17+
constructor(public host: ReactiveElement, ..._: SlotControllerArgs) {
18+
host.addController(this);
19+
}
20+
21+
hostConnected?(): Promise<void>;
22+
23+
private fromAttribute(slots: string | null) {
24+
return (slots ?? '')
25+
.split(/[, ]/)
26+
.map(x => x.trim());
27+
}
28+
29+
getSlotted<T extends Element = Element>(..._: string[]): T[] {
30+
return [];
31+
}
32+
33+
hasSlotted(...names: (string | null)[]): boolean {
34+
const attr = this.host.getAttribute(SlotController.attribute);
35+
const anon = this.host.hasAttribute(SlotController.anonymousAttribute);
36+
const hints = new Set(this.fromAttribute(attr));
37+
return names.every(x => x === null ? anon : hints.has(x));
38+
}
39+
40+
isEmpty(...names: (string | null)[]): boolean {
41+
return !this.hasSlotted(...names);
42+
}
43+
}

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

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

33
interface AnonymousSlot {
44
hasContent: boolean;
@@ -49,7 +49,56 @@ const isSlot =
4949
n === SlotController.default ? !child.hasAttribute('slot')
5050
: child.getAttribute('slot') === n;
5151

52-
export class SlotController implements ReactiveController {
52+
export declare class SlotControllerPublicAPI implements ReactiveController {
53+
static default: symbol;
54+
55+
public host: ReactiveElement;
56+
57+
constructor(host: ReactiveElement, ...args: SlotControllerArgs);
58+
59+
hostConnected?(): Promise<void>;
60+
61+
hostDisconnected?(): void;
62+
63+
hostUpdated?(): void;
64+
65+
/**
66+
* Given a slot name or slot names, returns elements assigned to the requested slots as an array.
67+
* If no value is provided, it returns all children not assigned to a slot (without a slot attribute).
68+
* @param slotNames slots to query
69+
* @example Get header-slotted elements
70+
* ```js
71+
* this.getSlotted('header')
72+
* ```
73+
* @example Get header- and footer-slotted elements
74+
* ```js
75+
* this.getSlotted('header', 'footer')
76+
* ```
77+
* @example Get default-slotted elements
78+
* ```js
79+
* this.getSlotted();
80+
* ```
81+
*/
82+
getSlotted<T extends Element = Element>(...slotNames: string[]): T[];
83+
84+
/**
85+
* Returns a boolean statement of whether or not any of those slots exists in the light DOM.
86+
* @param names The slot names to check.
87+
* @example this.hasSlotted('header');
88+
*/
89+
hasSlotted(...names: (string | null | undefined)[]): boolean;
90+
91+
/**
92+
* Whether or not all the requested slots are empty.
93+
* @param names The slot names to query. If no value is provided, it returns the default slot.
94+
* @example this.isEmpty('header', 'footer');
95+
* @example this.isEmpty();
96+
* @returns
97+
*/
98+
isEmpty(...names: (string | null | undefined)[]): boolean;
99+
}
100+
101+
export class SlotController implements SlotControllerPublicAPI {
53102
public static default = Symbol('default slot') satisfies symbol as symbol;
54103

55104
/** @deprecated use `default` */
@@ -94,16 +143,16 @@ export class SlotController implements ReactiveController {
94143
this.host.requestUpdate();
95144
}
96145

97-
hostDisconnected(): void {
98-
this.#mo.disconnect();
99-
}
100-
101146
hostUpdated(): void {
102147
if (!this.#slotMapInitialized) {
103148
this.#initSlotMap();
104149
}
105150
}
106151

152+
hostDisconnected(): void {
153+
this.#mo.disconnect();
154+
}
155+
107156
#initSlotMap() {
108157
// Loop over the properties provided by the schema
109158
for (const slotName of this.#slotNames
@@ -113,31 +162,23 @@ export class SlotController implements ReactiveController {
113162
const elements = this.#getChildrenForSlot(slotId);
114163
const slot = this.#getSlotElement(slotId);
115164
const hasContent =
116-
!isServer
117-
&& !!elements.length
118-
|| !!slot?.assignedNodes?.()?.filter(x => x.textContent?.trim()).length;
165+
!!elements.length || !!slot?.assignedNodes?.()?.filter(x => x.textContent?.trim()).length;
119166
this.#nodes.set(slotId, { elements, name, hasContent, slot });
120167
}
121168
this.host.requestUpdate();
122169
this.#slotMapInitialized = true;
123170
}
124171

125172
#getSlotElement(slotId: string | symbol) {
126-
if (isServer) {
127-
return null;
128-
} else {
129-
const selector =
173+
const selector =
130174
slotId === SlotController.default ? 'slot:not([name])' : `slot[name="${slotId as string}"]`;
131-
return this.host.shadowRoot?.querySelector?.<HTMLSlotElement>(selector) ?? null;
132-
}
175+
return this.host.shadowRoot?.querySelector?.<HTMLSlotElement>(selector) ?? null;
133176
}
134177

135178
#getChildrenForSlot<T extends Element = Element>(
136179
name: string | typeof SlotController.default,
137180
): T[] {
138-
if (isServer) {
139-
return [];
140-
} else if (this.#nodes.has(name)) {
181+
if (this.#nodes.has(name)) {
141182
return (this.#nodes.get(name)!.slot?.assignedElements?.() ?? []) as T[];
142183
} else {
143184
const children = Array.from(this.host.children) as T[];

core/pfe-core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"./controllers/roving-tabindex-controller.js": "./controllers/roving-tabindex-controller.js",
3434
"./controllers/scroll-spy-controller.js": "./controllers/scroll-spy-controller.js",
3535
"./controllers/slot-controller.js": {
36+
"node": "./controllers/slot-controller-server.js",
3637
"import": "./controllers/slot-controller.js",
3738
"default": "./controllers/slot-controller.js"
3839
},

docs/components/demos.11tydata.cjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = {
2+
templateEngineOverride: 'njk',
3+
permalink: '{{ demo.permalink }}',
4+
pagination: {
5+
data: 'demos',
6+
alias: 'demo',
7+
size: 1,
8+
before: xs => xs.filter(x => x.permalink),
9+
},
10+
};

docs/components/demos.html

Lines changed: 50 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,52 @@
1-
---js
2-
{
3-
templateEngineOverride: 'njk',
4-
permalink: '{{ demo.permalink }}',
5-
pagination: {
6-
data: 'demos',
7-
alias: 'demo',
8-
size: 1,
9-
before: xs => xs.filter(x => x.permalink),
10-
},
11-
preloads: [
12-
'@lit/[email protected]/development/css-tag.js',
13-
'@lit/[email protected]/development/decorators/base.js',
14-
'@lit/[email protected]/development/decorators/custom-element.js',
15-
'@lit/[email protected]/development/decorators/event-options.js',
16-
'@lit/[email protected]/development/decorators/property.js',
17-
'@lit/[email protected]/development/decorators/query-all.js',
18-
'@lit/[email protected]/development/decorators/query-assigned-nodes.js',
19-
'@lit/[email protected]/development/decorators/query-async.js',
20-
'@lit/[email protected]/development/decorators/query.js',
21-
'@lit/[email protected]/development/decorators/state.js',
22-
'@lit/[email protected]/development/reactive-element.js',
23-
'[email protected]/development/experimental-hydrate-support.js',
24-
'[email protected]/development/lit-element.js',
25-
'[email protected]/_/a39ea7cf.js',
26-
'[email protected]/development/async-directive.js',
27-
'[email protected]/development/directive-helpers.js',
28-
'[email protected]/development/directive.js',
29-
'[email protected]/development/directives/guard.js',
30-
'[email protected]/development/directives/if-defined.js',
31-
'[email protected]/development/directives/live.js',
32-
'[email protected]/development/directives/ref.js',
33-
'[email protected]/development/directives/repeat.js',
34-
'[email protected]/development/directives/style-map.js',
35-
'[email protected]/development/directives/template-content.js',
36-
'[email protected]/development/directives/unsafe-html.js',
37-
'[email protected]/development/directives/unsafe-svg.js',
38-
'[email protected]/development/directives/until.js',
39-
'[email protected]/development/experimental-hydrate.js',
40-
'[email protected]/development/lit-html.js',
41-
'[email protected]/development/static.js',
42-
'[email protected]/async-directive.js',
43-
'[email protected]/decorators.js',
44-
'[email protected]/decorators/query-all.js',
45-
'[email protected]/decorators/query-assigned-nodes.js',
46-
'[email protected]/decorators/query-async.js',
47-
'[email protected]/decorators/query.js',
48-
'[email protected]/directives/guard.js',
49-
'[email protected]/directives/if-defined.js',
50-
'[email protected]/directives/live.js',
51-
'[email protected]/directives/ref.js',
52-
'[email protected]/directives/repeat.js',
53-
'[email protected]/directives/style-map.js',
54-
'[email protected]/directives/template-content.js',
55-
'[email protected]/directives/unsafe-html.js',
56-
'[email protected]/directives/unsafe-svg.js',
57-
'[email protected]/directives/until.js',
58-
'[email protected]/experimental-hydrate-support.js',
59-
'[email protected]/experimental-hydrate.js',
60-
'[email protected]/html.js',
61-
'[email protected]/index.js',
62-
'[email protected]/polyfill-support.js',
63-
'[email protected]/static-html.js',
64-
]
65-
}
66-
---
671
<!DOCTYPE html>
68-
<html lang="en" dir="ltr" style="height: 100%">
69-
<head>
70-
<meta charset="utf-8">
71-
<meta name="viewport" content="width=device-width, initial-scale=1">
72-
<meta name="description" content="PatternFly Elements: A set of community-created web components based on PatternFly design.">
73-
<link href="{{ '/brand/logo/svg/pfe-icon-blue.svg' | url }}" rel="shortcut icon">
74-
<title>{{ demo.title or (demo.tagName) }} | PatternFly Elements</title>
75-
<link rel="preconnect" href="https://ga.jspm.io">
76-
<link rel="preconnect" href="https://fonts.gstatic.com">
77-
<script type="importmap">{{ importMap | dump | safe }}</script>
78-
{%- for path in preloads -%}
79-
<link rel="modulepreload" href="https://ga.jspm.io/npm:{{ path }}">
80-
{%- endfor -%}
81-
<link rel="stylesheet" href="{{ '/main.css' | url }}">
82-
<noscript><link href="{{ '/core/styles/pf--noscript.min.css' | url }}" rel="stylesheet"></noscript>
83-
<script async src="https://ga.jspm.io/npm:[email protected]/dist/es-module-shims.js"></script>
84-
<script type="module">import 'element-internals-polyfill';</script>
85-
</head>
86-
<body style="height: 100%">
87-
<main style="height: 100%">
88-
<div data-demo="{{demo.tagName}}">{% if demo.filePath %}
89-
{%- include demo.filePath -%}{% endif %}
90-
</div>
91-
</main>
92-
</body>
2+
<html lang="en"
3+
dir="ltr">
4+
5+
<head>
6+
<meta charset="utf-8">
7+
<meta name="viewport"
8+
content="width=device-width, initial-scale=1">
9+
<meta name="description"
10+
content="PatternFly Elements: A set of community-created web components based on PatternFly design.">
11+
<link href="/brand/logo/svg/pfe-icon-blue.svg"
12+
rel="shortcut icon">
13+
<title>{{ demo.title or (demo.tagName) }} | PatternFly Elements</title>
14+
<link rel="preconnect"
15+
href="https://ga.jspm.io">
16+
<link rel="preconnect"
17+
href="https://fonts.gstatic.com">
18+
<script type="importmap">{{ importMap | dump | safe }}</script>
19+
<link rel="stylesheet"
20+
href="/main.css">
21+
<noscript>
22+
<link href="/core/styles/pf--noscript.min.css"
23+
rel="stylesheet">
24+
</noscript>
25+
<script async
26+
src="https://ga.jspm.io/npm:[email protected]/dist/es-module-shims.js"></script>
27+
<script type="module">import 'element-internals-polyfill';</script>
28+
<style>
29+
html,
30+
body,
31+
main {
32+
min-height: 100%;
33+
}
34+
</style>
35+
<noscript>
36+
<style>
37+
:not(:defined) {
38+
opacity: 1;
39+
}
40+
</style>
41+
</noscript>
42+
</head>
43+
44+
<body>
45+
<main>
46+
<div data-demo="{{demo.tagName}}">{% if demo.filePath %}
47+
{%- include demo.filePath -%}{% endif %}
48+
</div>
49+
</main>
50+
</body>
51+
9352
</html>

0 commit comments

Comments
 (0)