Skip to content

Commit

Permalink
fix(scoped): fixes for <slot /> and slotted nodes (#6082)
Browse files Browse the repository at this point in the history
* chore: wip. Working proto

* chore: rc

* chore: comment

* chore: revert if

* chore: fixup incorrect test

---------

Co-authored-by: John Jenkins <[email protected]>
  • Loading branch information
johnjenkins and John Jenkins authored Jan 4, 2025
1 parent 9e6483a commit 13ee704
Show file tree
Hide file tree
Showing 31 changed files with 749 additions and 433 deletions.
11 changes: 8 additions & 3 deletions src/declarations/stencil-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,10 @@ export interface HostElement extends HTMLElement {
*/
['s-hmr']?: (versionId: string) => void;

/**
* A list of nested nested hydration promises that
* must be resolved for the top, ancestor component to be fully hydrated
*/
['s-p']?: Promise<void>[];

componentOnReady?: () => Promise<this>;
Expand Down Expand Up @@ -1392,9 +1396,10 @@ export interface RenderNode extends HostElement {
['s-cn']?: boolean;

/**
* Is a slot reference node:
* This is a node that represents where a slot
* was originally located.
* Is a `slot` node when `shadow: false` (or `scoped: true`).
*
* This is a node (either empty text-node or `<slot-fb>` element)
* that represents where a `<slot>` is located in the original JSX.
*/
['s-sr']?: boolean;

Expand Down
3 changes: 3 additions & 0 deletions src/mock-doc/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ export function createElement(ownerDocument: any, tagName: string): any {

case 'ul':
return new MockUListElement(ownerDocument);

case 'slot-fb':
return new MockHTMLElement(ownerDocument, tagName);
}

if (ownerDocument != null && tagName.includes('-')) {
Expand Down
85 changes: 67 additions & 18 deletions src/runtime/client-hydrate.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { BUILD } from '@app-data';
import { doc, plt } from '@platform';
import { CMP_FLAGS } from '@utils';

import type * as d from '../declarations';
import { addSlotRelocateNode, patchNextPrev } from './dom-extras';
import { patchNextPrev } from './dom-extras';
import { createTime } from './profile';
import {
COMMENT_NODE_ID,
Expand All @@ -13,7 +14,9 @@ import {
ORG_LOCATION_ID,
SLOT_NODE_ID,
TEXT_NODE_ID,
VNODE_FLAGS,
} from './runtime-constants';
import { addSlotRelocateNode } from './slot-polyfill-utils';
import { newVNode } from './vdom/h';

/**
Expand Down Expand Up @@ -50,6 +53,17 @@ export const initializeClientHydrate = (
const vnode: d.VNode = newVNode(tagName, null);
vnode.$elm$ = hostElm;

let scopeId: string;
if (BUILD.scoped) {
const cmpMeta = hostRef.$cmpMeta$;
if (cmpMeta && cmpMeta.$flags$ & CMP_FLAGS.needsScopedEncapsulation && hostElm['s-sc']) {
scopeId = hostElm['s-sc'];
hostElm.classList.add(scopeId + '-h');
} else if (hostElm['s-sc']) {
delete hostElm['s-sc'];
}
}

if (!plt.$orgLocNodes$) {
// This is the first pass over of this whole document;
// does a scrape to construct a 'bare-bones' tree of what elements we have and where content has been moved from
Expand Down Expand Up @@ -94,20 +108,36 @@ export const initializeClientHydrate = (
}
}

if (childRenderNode.$tag$ === 'slot') {
if (childRenderNode.$children$) {
childRenderNode.$flags$ |= VNODE_FLAGS.isSlotFallback;

if (!childRenderNode.$elm$.childNodes.length) {
// idiosyncrasy with slot fallback nodes during SSR + `serializeShadowRoot: false`:
// the slot node is created here (in `addSlot()`) via a comment node,
// but the children aren't moved into it. Let's do that now
childRenderNode.$children$.forEach((c) => {
childRenderNode.$elm$.appendChild(c.$elm$);
});
}
} else {
childRenderNode.$flags$ |= VNODE_FLAGS.isSlotReference;
}
}

if (orgLocationNode && orgLocationNode.isConnected) {
if (shadowRoot && orgLocationNode['s-en'] === '') {
// if this node is within a shadowDOM, with an original location home
// we're safe to move it now
orgLocationNode.parentNode.insertBefore(node, orgLocationNode.nextSibling);
}
// Remove original location / slot reference comment now regardless:
// 1) Stops SSR frameworks complaining about mismatches
// 2) is un-required for non-shadow, slotted nodes as we'll add all the meta nodes we need when we deal with *all* slotted nodes ↓↓↓
// Remove original location / slot reference comment now.
// we'll handle it via `addSlotRelocateNode` later
orgLocationNode.parentNode.removeChild(orgLocationNode);

if (!shadowRoot) {
// Add the Original Order of this node.
// We'll use it later to make sure slotted nodes get added in the correct order
// We'll use it to make sure slotted nodes get added in the correct order
node['s-oo'] = parseInt(childRenderNode.$nodeId$);
}
}
Expand All @@ -116,14 +146,15 @@ export const initializeClientHydrate = (
}

const hosts: d.HostElement[] = [];
let snIndex = 0;
const snLen = slottedNodes.length;
let snIndex = 0;
let slotGroup: SlottedNodes;
let snGroupIdx: number;
let snGroupLen: number;
let slottedItem: SlottedNodes[0];

// Loops through all the slotted nodes we found while stepping through this component
// Loops through all the slotted nodes we found while stepping through this component.
// creates slot relocation nodes (non-shadow) or moves nodes to their new home (shadow)
for (snIndex; snIndex < snLen; snIndex++) {
slotGroup = slottedNodes[snIndex];

Expand Down Expand Up @@ -157,8 +188,7 @@ export const initializeClientHydrate = (
} else {
// If all else fails - just set the CR as the first child
// (9/10 if node['s-cr'] hasn't been set, the node will be at the element root)
const hostChildren = (hostEle as any).__childNodes || hostEle.childNodes;
slottedItem.slot['s-cr'] = hostChildren[0] as d.RenderNode;
slottedItem.slot['s-cr'] = ((hostEle as any).__childNodes || hostEle.childNodes)[0];
}
// Create our 'Original Location' node
addSlotRelocateNode(slottedItem.node, slottedItem.slot, false, slottedItem.node['s-oo']);
Expand All @@ -176,6 +206,13 @@ export const initializeClientHydrate = (
}
}

if (BUILD.scoped && scopeId && slotNodes.length) {
slotNodes.forEach((slot) => {
// Host is `scoped: true` - add the slotted scoped class to the slot parent
slot.$elm$.parentElement.classList.add(scopeId + '-s');
});
}

if (BUILD.shadowDom && shadowRoot) {
// Add all the root nodes in the shadowDOM (a root node can have a whole nested DOM tree)
let rnIdex = 0;
Expand Down Expand Up @@ -247,9 +284,9 @@ const clientHydrate = (
$index$: childIdSplt[3],
$tag$: node.tagName.toLowerCase(),
$elm$: node,
// If we don't add the initial classes to the VNode, the first `vdom-render.ts` reconciliation will fail:
// client side changes before componentDidLoad will be ignored, `set-accessor.ts` will just take the element's initial classes
$attrs$: { class: node.className },
// If we don't add the initial classes to the VNode, the first `vdom-render.ts` patch
// won't try to reconcile them. Classes set on the node will be blown away.
$attrs$: { class: node.className || '' },
});

childRenderNodes.push(childVNode);
Expand All @@ -260,6 +297,13 @@ const clientHydrate = (
parentVNode.$children$ = [];
}

if (BUILD.scoped && scopeId) {
// Host is `scoped: true` - add that flag to the child.
// It's used in 'set-accessor.ts' to make sure our scoped class is present
node['s-si'] = scopeId;
childVNode.$attrs$.class += ' ' + scopeId;
}

// Test if this element was 'slotted' or is a 'slot' (with fallback). Recreate node attributes
const slotName = childVNode.$elm$.getAttribute('s-sn');
if (typeof slotName === 'string') {
Expand All @@ -276,6 +320,12 @@ const clientHydrate = (
shadowRootNodes,
slottedNodes,
);

if (BUILD.scoped && scopeId) {
// Host is `scoped: true` - a slot-fb node
// never goes through 'set-accessor.ts' so add the class now
node.classList.add(scopeId);
}
}
childVNode.$elm$['s-sn'] = slotName;
childVNode.$elm$.removeAttribute('s-sn');
Expand All @@ -285,10 +335,6 @@ const clientHydrate = (
parentVNode.$children$[childVNode.$index$ as any] = childVNode;
}

// Host is `scoped: true` - add that flag to the child.
// It's used in 'set-accessor.ts' to make sure our scoped class is present
if (scopeId) node['s-si'] = scopeId;

// This is now the new parent VNode for all the next child checks
parentVNode = childVNode;

Expand Down Expand Up @@ -390,10 +436,10 @@ const clientHydrate = (
if (childNodeType === SLOT_NODE_ID) {
// Comment refers to a slot node:
// `${SLOT_NODE_ID}.${hostId}.${nodeId}.${depth}.${index}.${slotName}`;
childVNode.$tag$ = 'slot';

// Add the slot name
const slotName = (node['s-sn'] = childVNode.$name$ = childIdSplt[5] || '');
const slotName = (node['s-sn'] = childIdSplt[5] || '');

// add the `<slot>` node to the VNode tree and prepare any slotted any child nodes
addSlot(
slotName,
Expand Down Expand Up @@ -502,6 +548,8 @@ function addSlot(
slottedNodes: SlottedNodes[],
) {
node['s-sr'] = true;
childVNode.$name$ = slotName || null;
childVNode.$tag$ = 'slot';

// Find this slots' current host parent (as dictated by the VDOM tree).
// Important because where it is now in the constructed SSR markup might be different to where to *should* be
Expand Down Expand Up @@ -604,4 +652,5 @@ interface RenderNodeData extends d.VNode {
$nodeId$: string;
$depth$: string;
$index$: string;
$elm$: d.RenderNode;
}
2 changes: 1 addition & 1 deletion src/runtime/connected-callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,5 @@ const setContentReference = (elm: d.HostElement) => {
BUILD.isDebug ? `content-ref (host=${elm.localName})` : '',
) as any);
contentRefElm['s-cn'] = true;
insertBefore(elm, contentRefElm, elm.firstChild);
insertBefore(elm, contentRefElm, elm.firstChild as d.RenderNode);
};
Loading

0 comments on commit 13ee704

Please sign in to comment.