Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d5c27c2
chore: nvm use lts
bennypowers Jan 13, 2025
ba4408c
chore: bump lit-labs/ssr
bennypowers Jan 13, 2025
cd54b8b
chore: bump lit/context
bennypowers Jan 13, 2025
1bddaa9
fix: update context usage for ssr
bennypowers Jan 13, 2025
ed2a4c9
fix: use lit-ssr html function
bennypowers Jan 13, 2025
0088148
fix(core): rely on lit's dom-shim
bennypowers Jan 13, 2025
837f66a
chore: update and align lit version
bennypowers Jan 13, 2025
a127114
fix(core): ssr connected callback
bennypowers Jan 13, 2025
1fffb2c
fix(elements): ssr connected callbacks
bennypowers Jan 13, 2025
9bd0874
fix(elements): table th role from context instead of dom
bennypowers Jan 13, 2025
d605a41
feat(core): wip slots decorator
bennypowers Jan 21, 2025
49c3202
fix(core): empty array check
bennypowers Jan 27, 2025
0b3dffa
fix(core): remove need for static decorator for ssr slot hints
bennypowers Jan 30, 2025
98313ce
fix(core): client-side slot controller doens't use isServer
bennypowers Jan 30, 2025
9f5ecaa
fix(core): ssr-hint-has-default-slotted attr name
bennypowers Jan 30, 2025
35a459e
Merge branch 'main' into feat/slot-controller-ssr
bennypowers Apr 6, 2025
444d4e8
test: ssr demos
bennypowers Apr 6, 2025
cf9eec6
chore: ssr dev packages
bennypowers Apr 6, 2025
09ca48b
Merge branch 'main' into feat/slot-controller-ssr
bennypowers Apr 6, 2025
7e61848
chore: deps
bennypowers Apr 6, 2025
0d9f978
docs: build elements
bennypowers Apr 6, 2025
f4a6dd9
docs: format
bennypowers Apr 6, 2025
848a2d6
chore: revert ssr
bennypowers Apr 6, 2025
81ed882
style: lint
bennypowers Apr 6, 2025
cdacbf0
docs: remove preloads
bennypowers Apr 7, 2025
eca6a80
docs: changeset
bennypowers Apr 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changeset/all-tigers-wear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
"@patternfly/elements": patch
---
**SSR**: enable more elements and more features to be rendered server-side
6 changes: 2 additions & 4 deletions .changeset/clear-pugs-make.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
"@patternfly/pfe-core": major
"@patternfly/elements": patch
---
Enable `connectedCallback()` and context protocol in SSR scenarios.

Expand All @@ -15,7 +14,7 @@ Before:
```js
connectedCallback() {
super.connectedCallback();
this.items = this.querySelectorAll('my-item');
this.items = [...this.querySelectorAll('my-item')];
}
```

Expand All @@ -24,8 +23,7 @@ After:
connectedCallback() {
super.connectedCallback();
if (!isServer) {
this.items = this.querySelectorAll('my-item');
this.items = isServer ? [] : [...this.querySelectorAll('my-item')];
}
}
```

43 changes: 43 additions & 0 deletions core/pfe-core/controllers/slot-controller-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { ReactiveElement } from 'lit';
import {
type SlotControllerArgs,
type SlotControllerPublicAPI,
} from './slot-controller.js';

export class SlotController implements SlotControllerPublicAPI {
public static default = Symbol('default slot') satisfies symbol as symbol;

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

static attribute = 'ssr-hint-has-slotted' as const;

static anonymousAttribute = 'ssr-hint-has-slotted-default' as const;

constructor(public host: ReactiveElement, ..._: SlotControllerArgs) {
host.addController(this);
}

hostConnected?(): Promise<void>;

private fromAttribute(slots: string | null) {
return (slots ?? '')
.split(/[, ]/)
.map(x => x.trim());
}

getSlotted<T extends Element = Element>(..._: string[]): T[] {
return [];
}

hasSlotted(...names: (string | null)[]): boolean {
const attr = this.host.getAttribute(SlotController.attribute);
const anon = this.host.hasAttribute(SlotController.anonymousAttribute);
const hints = new Set(this.fromAttribute(attr));
return names.every(x => x === null ? anon : hints.has(x));
}

isEmpty(...names: (string | null)[]): boolean {
return !this.hasSlotted(...names);
}
}
77 changes: 59 additions & 18 deletions core/pfe-core/controllers/slot-controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isServer, type ReactiveController, type ReactiveElement } from 'lit';
import type { ReactiveController, ReactiveElement } from 'lit';

interface AnonymousSlot {
hasContent: boolean;
Expand Down Expand Up @@ -49,7 +49,56 @@ const isSlot =
n === SlotController.default ? !child.hasAttribute('slot')
: child.getAttribute('slot') === n;

export class SlotController implements ReactiveController {
export declare class SlotControllerPublicAPI implements ReactiveController {
static default: symbol;

public host: ReactiveElement;

constructor(host: ReactiveElement, ...args: SlotControllerArgs);

hostConnected?(): Promise<void>;

hostDisconnected?(): void;

hostUpdated?(): void;

/**
* Given a slot name or slot names, returns elements assigned to the requested slots as an array.
* If no value is provided, it returns all children not assigned to a slot (without a slot attribute).
* @param slotNames slots to query
* @example Get header-slotted elements
* ```js
* this.getSlotted('header')
* ```
* @example Get header- and footer-slotted elements
* ```js
* this.getSlotted('header', 'footer')
* ```
* @example Get default-slotted elements
* ```js
* this.getSlotted();
* ```
*/
getSlotted<T extends Element = Element>(...slotNames: string[]): T[];

/**
* Returns a boolean statement of whether or not any of those slots exists in the light DOM.
* @param names The slot names to check.
* @example this.hasSlotted('header');
*/
hasSlotted(...names: (string | null | undefined)[]): boolean;

/**
* Whether or not all the requested slots are empty.
* @param names The slot names to query. If no value is provided, it returns the default slot.
* @example this.isEmpty('header', 'footer');
* @example this.isEmpty();
* @returns
*/
isEmpty(...names: (string | null | undefined)[]): boolean;
}

export class SlotController implements SlotControllerPublicAPI {
public static default = Symbol('default slot') satisfies symbol as symbol;

/** @deprecated use `default` */
Expand Down Expand Up @@ -94,16 +143,16 @@ export class SlotController implements ReactiveController {
this.host.requestUpdate();
}

hostDisconnected(): void {
this.#mo.disconnect();
}

hostUpdated(): void {
if (!this.#slotMapInitialized) {
this.#initSlotMap();
}
}

hostDisconnected(): void {
this.#mo.disconnect();
}

#initSlotMap() {
// Loop over the properties provided by the schema
for (const slotName of this.#slotNames
Expand All @@ -113,31 +162,23 @@ export class SlotController implements ReactiveController {
const elements = this.#getChildrenForSlot(slotId);
const slot = this.#getSlotElement(slotId);
const hasContent =
!isServer
&& !!elements.length
|| !!slot?.assignedNodes?.()?.filter(x => x.textContent?.trim()).length;
!!elements.length || !!slot?.assignedNodes?.()?.filter(x => x.textContent?.trim()).length;
this.#nodes.set(slotId, { elements, name, hasContent, slot });
}
this.host.requestUpdate();
this.#slotMapInitialized = true;
}

#getSlotElement(slotId: string | symbol) {
if (isServer) {
return null;
} else {
const selector =
const selector =
slotId === SlotController.default ? 'slot:not([name])' : `slot[name="${slotId as string}"]`;
return this.host.shadowRoot?.querySelector?.<HTMLSlotElement>(selector) ?? null;
}
return this.host.shadowRoot?.querySelector?.<HTMLSlotElement>(selector) ?? null;
}

#getChildrenForSlot<T extends Element = Element>(
name: string | typeof SlotController.default,
): T[] {
if (isServer) {
return [];
} else if (this.#nodes.has(name)) {
if (this.#nodes.has(name)) {
return (this.#nodes.get(name)!.slot?.assignedElements?.() ?? []) as T[];
} else {
const children = Array.from(this.host.children) as T[];
Expand Down
1 change: 1 addition & 0 deletions core/pfe-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"./controllers/roving-tabindex-controller.js": "./controllers/roving-tabindex-controller.js",
"./controllers/scroll-spy-controller.js": "./controllers/scroll-spy-controller.js",
"./controllers/slot-controller.js": {
"node": "./controllers/slot-controller-server.js",
"import": "./controllers/slot-controller.js",
"default": "./controllers/slot-controller.js"
},
Expand Down
10 changes: 10 additions & 0 deletions docs/components/demos.11tydata.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
templateEngineOverride: 'njk',
permalink: '{{ demo.permalink }}',
pagination: {
data: 'demos',
alias: 'demo',
size: 1,
before: xs => xs.filter(x => x.permalink),
},
};
141 changes: 50 additions & 91 deletions docs/components/demos.html
Original file line number Diff line number Diff line change
@@ -1,93 +1,52 @@
---js
{
templateEngineOverride: 'njk',
permalink: '{{ demo.permalink }}',
pagination: {
data: 'demos',
alias: 'demo',
size: 1,
before: xs => xs.filter(x => x.permalink),
},
preloads: [
'@lit/[email protected]/development/css-tag.js',
'@lit/[email protected]/development/decorators/base.js',
'@lit/[email protected]/development/decorators/custom-element.js',
'@lit/[email protected]/development/decorators/event-options.js',
'@lit/[email protected]/development/decorators/property.js',
'@lit/[email protected]/development/decorators/query-all.js',
'@lit/[email protected]/development/decorators/query-assigned-nodes.js',
'@lit/[email protected]/development/decorators/query-async.js',
'@lit/[email protected]/development/decorators/query.js',
'@lit/[email protected]/development/decorators/state.js',
'@lit/[email protected]/development/reactive-element.js',
'[email protected]/development/experimental-hydrate-support.js',
'[email protected]/development/lit-element.js',
'[email protected]/_/a39ea7cf.js',
'[email protected]/development/async-directive.js',
'[email protected]/development/directive-helpers.js',
'[email protected]/development/directive.js',
'[email protected]/development/directives/guard.js',
'[email protected]/development/directives/if-defined.js',
'[email protected]/development/directives/live.js',
'[email protected]/development/directives/ref.js',
'[email protected]/development/directives/repeat.js',
'[email protected]/development/directives/style-map.js',
'[email protected]/development/directives/template-content.js',
'[email protected]/development/directives/unsafe-html.js',
'[email protected]/development/directives/unsafe-svg.js',
'[email protected]/development/directives/until.js',
'[email protected]/development/experimental-hydrate.js',
'[email protected]/development/lit-html.js',
'[email protected]/development/static.js',
'[email protected]/async-directive.js',
'[email protected]/decorators.js',
'[email protected]/decorators/query-all.js',
'[email protected]/decorators/query-assigned-nodes.js',
'[email protected]/decorators/query-async.js',
'[email protected]/decorators/query.js',
'[email protected]/directives/guard.js',
'[email protected]/directives/if-defined.js',
'[email protected]/directives/live.js',
'[email protected]/directives/ref.js',
'[email protected]/directives/repeat.js',
'[email protected]/directives/style-map.js',
'[email protected]/directives/template-content.js',
'[email protected]/directives/unsafe-html.js',
'[email protected]/directives/unsafe-svg.js',
'[email protected]/directives/until.js',
'[email protected]/experimental-hydrate-support.js',
'[email protected]/experimental-hydrate.js',
'[email protected]/html.js',
'[email protected]/index.js',
'[email protected]/polyfill-support.js',
'[email protected]/static-html.js',
]
}
---
<!DOCTYPE html>
<html lang="en" dir="ltr" style="height: 100%">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="PatternFly Elements: A set of community-created web components based on PatternFly design.">
<link href="{{ '/brand/logo/svg/pfe-icon-blue.svg' | url }}" rel="shortcut icon">
<title>{{ demo.title or (demo.tagName) }} | PatternFly Elements</title>
<link rel="preconnect" href="https://ga.jspm.io">
<link rel="preconnect" href="https://fonts.gstatic.com">
<script type="importmap">{{ importMap | dump | safe }}</script>
{%- for path in preloads -%}
<link rel="modulepreload" href="https://ga.jspm.io/npm:{{ path }}">
{%- endfor -%}
<link rel="stylesheet" href="{{ '/main.css' | url }}">
<noscript><link href="{{ '/core/styles/pf--noscript.min.css' | url }}" rel="stylesheet"></noscript>
<script async src="https://ga.jspm.io/npm:[email protected]/dist/es-module-shims.js"></script>
<script type="module">import 'element-internals-polyfill';</script>
</head>
<body style="height: 100%">
<main style="height: 100%">
<div data-demo="{{demo.tagName}}">{% if demo.filePath %}
{%- include demo.filePath -%}{% endif %}
</div>
</main>
</body>
<html lang="en"
dir="ltr">

<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1">
<meta name="description"
content="PatternFly Elements: A set of community-created web components based on PatternFly design.">
<link href="/brand/logo/svg/pfe-icon-blue.svg"
rel="shortcut icon">
<title>{{ demo.title or (demo.tagName) }} | PatternFly Elements</title>
<link rel="preconnect"
href="https://ga.jspm.io">
<link rel="preconnect"
href="https://fonts.gstatic.com">
<script type="importmap">{{ importMap | dump | safe }}</script>
<link rel="stylesheet"
href="/main.css">
<noscript>
<link href="/core/styles/pf--noscript.min.css"
rel="stylesheet">
</noscript>
<script async
src="https://ga.jspm.io/npm:[email protected]/dist/es-module-shims.js"></script>
<script type="module">import 'element-internals-polyfill';</script>
<style>
html,
body,
main {
min-height: 100%;
}
</style>
<noscript>
<style>
:not(:defined) {
opacity: 1;
}
</style>
</noscript>
</head>

<body>
<main>
<div data-demo="{{demo.tagName}}">{% if demo.filePath %}
{%- include demo.filePath -%}{% endif %}
</div>
</main>
</body>

</html>
Loading
Loading