Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ssr-friendly slot-controller #2505

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 34 additions & 15 deletions core/pfe-core/controllers/slot-controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ReactiveController, ReactiveElement } from 'lit';
import { isServer } from 'lit';

import { bound } from '../decorators/bound.js';
import { Logger } from './logger.js';
Expand Down Expand Up @@ -33,7 +34,7 @@ export interface SlotsConfig {
deprecations?: Record<string, string>;
}

function isObjectConfigSpread(config: ([SlotsConfig] | (string | null)[])): config is [SlotsConfig] {
function isObjectConfigSpread(config: ([SlotsConfig] | SlotName[])): config is [SlotsConfig] {
return config.length === 1 && typeof config[0] === 'object' && config[0] !== null;
}

Expand All @@ -42,28 +43,34 @@ function isObjectConfigSpread(config: ([SlotsConfig] | (string | null)[])): conf
* for the default slot, look for direct children not assigned to a slot
*/
const isSlot =
<T extends Element = Element>(n: string | typeof SlotController.anonymous) =>
<T extends Element = Element>(n: SlotName) =>
(child: Element): child is T =>
n === SlotController.anonymous ? !child.hasAttribute('slot')
: child.getAttribute('slot') === n;

type SlotName = string | null | symbol;

export class SlotController implements ReactiveController {
public static anonymous = Symbol('anonymous slot');

private nodes = new Map<string | typeof SlotController.anonymous, Slot>();
private nodes = new Map<SlotName, Slot>();

private logger: Logger;

private firstUpdated = false;

private mo = new MutationObserver(this.onMutation);
private mo?: MutationObserver;

private slotNames: (string | null)[];
private slotNames: SlotName[];

private deprecations: Record<string, string> = {};

constructor(public host: ReactiveElement, ...config: ([SlotsConfig] | (string | null)[])) {
constructor(public host: ReactiveElement, ...config: ([SlotsConfig] | SlotName[])) {
this.logger = new Logger(this.host);
// TODO: server export assertion
if (!isServer) {
this.mo = new MutationObserver(this.onMutation);
}

if (isObjectConfigSpread(config)) {
const [{ slots, deprecations }] = config;
Expand All @@ -83,7 +90,7 @@ export class SlotController implements ReactiveController {
hostConnected() {
this.host.addEventListener('slotchange', this.onSlotChange as EventListener);
this.firstUpdated = false;
this.mo.observe(this.host, { childList: true });
this.mo?.observe(this.host, { childList: true });
this.init();
}

Expand All @@ -95,19 +102,31 @@ export class SlotController implements ReactiveController {
}

hostDisconnected() {
this.mo.disconnect();
this.mo?.disconnect();
}

/**
* Returns a boolean statement of whether or not any of those slots exists in the light DOM.
*
* @param {String|Array} name The slot name.
* @example this.hasSlotted("header");
* @example this.hasSlotted('header');
*/
hasSlotted(...names: string[]): boolean {
hasSlotted(...names: SlotName[]): boolean {
if (!names.length) {
this.logger.warn(`Please provide at least one slot name for which to search.`);
return false;
} else if (isServer) {
// TODO: anonymous slot
const attrSlotted: Set<SlotName> =
new Set(this.host.getAttribute('has-slotted')?.split(',').map(x => {
const trimmed = x.trim();
if (trimmed === 'anonymous') {
return SlotController.anonymous;
} else {
return trimmed;
}
}));
return names.some(name =>
attrSlotted.has(name));
} else {
return names.some(x =>
this.nodes.get(x)?.hasContent ?? false);
Expand Down Expand Up @@ -163,18 +182,18 @@ export class SlotController implements ReactiveController {
}
}

private getChildrenForSlot<T extends Element = Element>(name: string | typeof SlotController.anonymous): T[] {
private getChildrenForSlot<T extends Element = Element>(name: SlotName): T[] {
const children = Array.from(this.host.children) as T[];
return children.filter(isSlot(name));
}

@bound private initSlot(slotName: string | null) {
@bound private initSlot(slotName: SlotName) {
const name = slotName || SlotController.anonymous;
const elements = this.nodes.get(name)?.slot?.assignedElements?.() ?? this.getChildrenForSlot(name);
const selector = slotName ? `slot[name="${slotName}"]` : 'slot:not([name])';
const selector = typeof slotName === 'string' ? `slot[name="${slotName}"]` : 'slot:not([name])';
const slot = this.host.shadowRoot?.querySelector?.<HTMLSlotElement>(selector) ?? null;
const hasContent = !!elements.length;
this.nodes.set(name, { elements, name: slotName ?? '', hasContent, slot });
this.nodes.set(name, { elements, name: typeof slotName === 'symbol' || !slotName ? '' : slotName, hasContent, slot });
this.logger.log(slotName, hasContent);
}

Expand Down
14 changes: 11 additions & 3 deletions docs/components/demos.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
}
---
<!DOCTYPE html>
<html lang="en" dir="ltr" style="height: 100%">
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
Expand All @@ -90,9 +90,17 @@
PfIcon.addIconSet('far', get);
PfIcon.addIconSet('fab', get);
</script>
<style>
html, body, main { min-height: 100%; }
</style>
<noscript>
<style>
:not(:defined) { opacity: 1; }
</style>
</noscript>
</head>
<body style="height: 100%">
<main style="height: 100%">
<body>
<main>
<div data-demo="{{demo.tagName}}">{% if demo.filePath %}
{%- include demo.filePath -%}{% endif %}
</div>
Expand Down
9 changes: 7 additions & 2 deletions elements/pf-card/BaseCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ import style from './BaseCard.css';
export abstract class BaseCard extends LitElement {
static readonly styles = [style];

protected slots = new SlotController(this, 'header', null, 'footer');
protected slots = new SlotController(
this,
'header',
SlotController.anonymous,
'footer',
);

render() {
return html`
Expand All @@ -37,7 +42,7 @@ export abstract class BaseCard extends LitElement {
</header>
<div id="body"
part="body"
class="${classMap({ empty: !this.querySelector(':not([slot])') })}">
class="${classMap({ empty: !this.slots.hasSlotted(SlotController.anonymous) })}">
<slot></slot>
</div>
<footer id="footer"
Expand Down
49 changes: 49 additions & 0 deletions elements/pf-card/demo/ssr.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<pf-card has-slotted="header,anonymous,footer">
<h2 slot="header">Header</h2>
<p>Body</p>
<span slot="footer">Footer</span>
</pf-card>

<pf-card has-slotted="anonymous">
<p>Body</p>
</pf-card>

<pf-card has-slotted="header">
<h2 slot="header">Header</h2>
</pf-card>

<pf-card has-slotted="header,anonymous">
<h2 slot="header">Header</h2>
<p>Body</p>
</pf-card>

<pf-card has-slotted="header,footer">
<h2 slot="header">Header</h2>
<span slot="footer">Footer</span>
</pf-card>

<pf-card has-slotted="anonymous,footer">
<p>Body</p>
<span slot="footer">Footer</span>
</pf-card>

<pf-card has-slotted="footer">
<span slot="footer">Footer</span>
</pf-card>

<style>

main {
padding: 2em;
background: #eeeeee;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: max-content 1fr;
gap: 1em;
}

[data-demo] {
display: contents;
}

</style>
8 changes: 8 additions & 0 deletions eleventy.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const { EleventyRenderPlugin } = require('@11ty/eleventy');
const SyntaxHighlightPlugin = require('@11ty/eleventy-plugin-syntaxhighlight');
const DirectoryOutputPlugin = require('@11ty/eleventy-plugin-directory-output');

const LitSSRPlugin = require('@lit-labs/eleventy-plugin-lit');

const PfeAssetsPlugin = require('./docs/_plugins/pfe-assets.cjs');
const EmptyParagraphPlugin = require('./docs/_plugins/empty-p.cjs');

Expand Down Expand Up @@ -41,6 +43,12 @@ module.exports = function(eleventyConfig) {
/** list todos */
eleventyConfig.addPlugin(TodosPlugin);

eleventyConfig.addPlugin(LitSSRPlugin, {
componentModules: [
'elements/pf-card/pf-card.js',
],
});

/** format date strings */
eleventyConfig.addFilter('prettyDate', function(dateStr, options = {}) {
const { dateStyle = 'medium' } = options;
Expand Down
Loading