Skip to content

Commit 13ee704

Browse files
johnjenkinsJohn Jenkins
andauthored
fix(scoped): fixes for <slot /> and slotted nodes (#6082)
* chore: wip. Working proto * chore: rc * chore: comment * chore: revert if * chore: fixup incorrect test --------- Co-authored-by: John Jenkins <[email protected]>
1 parent 9e6483a commit 13ee704

31 files changed

+749
-433
lines changed

src/declarations/stencil-private.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,10 @@ export interface HostElement extends HTMLElement {
11471147
*/
11481148
['s-hmr']?: (versionId: string) => void;
11491149

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

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

13941398
/**
1395-
* Is a slot reference node:
1396-
* This is a node that represents where a slot
1397-
* was originally located.
1399+
* Is a `slot` node when `shadow: false` (or `scoped: true`).
1400+
*
1401+
* This is a node (either empty text-node or `<slot-fb>` element)
1402+
* that represents where a `<slot>` is located in the original JSX.
13981403
*/
13991404
['s-sr']?: boolean;
14001405

src/mock-doc/element.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ export function createElement(ownerDocument: any, tagName: string): any {
5252

5353
case 'ul':
5454
return new MockUListElement(ownerDocument);
55+
56+
case 'slot-fb':
57+
return new MockHTMLElement(ownerDocument, tagName);
5558
}
5659

5760
if (ownerDocument != null && tagName.includes('-')) {

src/runtime/client-hydrate.ts

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { BUILD } from '@app-data';
22
import { doc, plt } from '@platform';
3+
import { CMP_FLAGS } from '@utils';
34

45
import type * as d from '../declarations';
5-
import { addSlotRelocateNode, patchNextPrev } from './dom-extras';
6+
import { patchNextPrev } from './dom-extras';
67
import { createTime } from './profile';
78
import {
89
COMMENT_NODE_ID,
@@ -13,7 +14,9 @@ import {
1314
ORG_LOCATION_ID,
1415
SLOT_NODE_ID,
1516
TEXT_NODE_ID,
17+
VNODE_FLAGS,
1618
} from './runtime-constants';
19+
import { addSlotRelocateNode } from './slot-polyfill-utils';
1720
import { newVNode } from './vdom/h';
1821

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

56+
let scopeId: string;
57+
if (BUILD.scoped) {
58+
const cmpMeta = hostRef.$cmpMeta$;
59+
if (cmpMeta && cmpMeta.$flags$ & CMP_FLAGS.needsScopedEncapsulation && hostElm['s-sc']) {
60+
scopeId = hostElm['s-sc'];
61+
hostElm.classList.add(scopeId + '-h');
62+
} else if (hostElm['s-sc']) {
63+
delete hostElm['s-sc'];
64+
}
65+
}
66+
5367
if (!plt.$orgLocNodes$) {
5468
// This is the first pass over of this whole document;
5569
// does a scrape to construct a 'bare-bones' tree of what elements we have and where content has been moved from
@@ -94,20 +108,36 @@ export const initializeClientHydrate = (
94108
}
95109
}
96110

111+
if (childRenderNode.$tag$ === 'slot') {
112+
if (childRenderNode.$children$) {
113+
childRenderNode.$flags$ |= VNODE_FLAGS.isSlotFallback;
114+
115+
if (!childRenderNode.$elm$.childNodes.length) {
116+
// idiosyncrasy with slot fallback nodes during SSR + `serializeShadowRoot: false`:
117+
// the slot node is created here (in `addSlot()`) via a comment node,
118+
// but the children aren't moved into it. Let's do that now
119+
childRenderNode.$children$.forEach((c) => {
120+
childRenderNode.$elm$.appendChild(c.$elm$);
121+
});
122+
}
123+
} else {
124+
childRenderNode.$flags$ |= VNODE_FLAGS.isSlotReference;
125+
}
126+
}
127+
97128
if (orgLocationNode && orgLocationNode.isConnected) {
98129
if (shadowRoot && orgLocationNode['s-en'] === '') {
99130
// if this node is within a shadowDOM, with an original location home
100131
// we're safe to move it now
101132
orgLocationNode.parentNode.insertBefore(node, orgLocationNode.nextSibling);
102133
}
103-
// Remove original location / slot reference comment now regardless:
104-
// 1) Stops SSR frameworks complaining about mismatches
105-
// 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 ↓↓↓
134+
// Remove original location / slot reference comment now.
135+
// we'll handle it via `addSlotRelocateNode` later
106136
orgLocationNode.parentNode.removeChild(orgLocationNode);
107137

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

118148
const hosts: d.HostElement[] = [];
119-
let snIndex = 0;
120149
const snLen = slottedNodes.length;
150+
let snIndex = 0;
121151
let slotGroup: SlottedNodes;
122152
let snGroupIdx: number;
123153
let snGroupLen: number;
124154
let slottedItem: SlottedNodes[0];
125155

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

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

209+
if (BUILD.scoped && scopeId && slotNodes.length) {
210+
slotNodes.forEach((slot) => {
211+
// Host is `scoped: true` - add the slotted scoped class to the slot parent
212+
slot.$elm$.parentElement.classList.add(scopeId + '-s');
213+
});
214+
}
215+
179216
if (BUILD.shadowDom && shadowRoot) {
180217
// Add all the root nodes in the shadowDOM (a root node can have a whole nested DOM tree)
181218
let rnIdex = 0;
@@ -247,9 +284,9 @@ const clientHydrate = (
247284
$index$: childIdSplt[3],
248285
$tag$: node.tagName.toLowerCase(),
249286
$elm$: node,
250-
// If we don't add the initial classes to the VNode, the first `vdom-render.ts` reconciliation will fail:
251-
// client side changes before componentDidLoad will be ignored, `set-accessor.ts` will just take the element's initial classes
252-
$attrs$: { class: node.className },
287+
// If we don't add the initial classes to the VNode, the first `vdom-render.ts` patch
288+
// won't try to reconcile them. Classes set on the node will be blown away.
289+
$attrs$: { class: node.className || '' },
253290
});
254291

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

300+
if (BUILD.scoped && scopeId) {
301+
// Host is `scoped: true` - add that flag to the child.
302+
// It's used in 'set-accessor.ts' to make sure our scoped class is present
303+
node['s-si'] = scopeId;
304+
childVNode.$attrs$.class += ' ' + scopeId;
305+
}
306+
263307
// Test if this element was 'slotted' or is a 'slot' (with fallback). Recreate node attributes
264308
const slotName = childVNode.$elm$.getAttribute('s-sn');
265309
if (typeof slotName === 'string') {
@@ -276,6 +320,12 @@ const clientHydrate = (
276320
shadowRootNodes,
277321
slottedNodes,
278322
);
323+
324+
if (BUILD.scoped && scopeId) {
325+
// Host is `scoped: true` - a slot-fb node
326+
// never goes through 'set-accessor.ts' so add the class now
327+
node.classList.add(scopeId);
328+
}
279329
}
280330
childVNode.$elm$['s-sn'] = slotName;
281331
childVNode.$elm$.removeAttribute('s-sn');
@@ -285,10 +335,6 @@ const clientHydrate = (
285335
parentVNode.$children$[childVNode.$index$ as any] = childVNode;
286336
}
287337

288-
// Host is `scoped: true` - add that flag to the child.
289-
// It's used in 'set-accessor.ts' to make sure our scoped class is present
290-
if (scopeId) node['s-si'] = scopeId;
291-
292338
// This is now the new parent VNode for all the next child checks
293339
parentVNode = childVNode;
294340

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

395440
// Add the slot name
396-
const slotName = (node['s-sn'] = childVNode.$name$ = childIdSplt[5] || '');
441+
const slotName = (node['s-sn'] = childIdSplt[5] || '');
442+
397443
// add the `<slot>` node to the VNode tree and prepare any slotted any child nodes
398444
addSlot(
399445
slotName,
@@ -502,6 +548,8 @@ function addSlot(
502548
slottedNodes: SlottedNodes[],
503549
) {
504550
node['s-sr'] = true;
551+
childVNode.$name$ = slotName || null;
552+
childVNode.$tag$ = 'slot';
505553

506554
// Find this slots' current host parent (as dictated by the VDOM tree).
507555
// Important because where it is now in the constructed SSR markup might be different to where to *should* be
@@ -604,4 +652,5 @@ interface RenderNodeData extends d.VNode {
604652
$nodeId$: string;
605653
$depth$: string;
606654
$index$: string;
655+
$elm$: d.RenderNode;
607656
}

src/runtime/connected-callback.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,5 @@ const setContentReference = (elm: d.HostElement) => {
134134
BUILD.isDebug ? `content-ref (host=${elm.localName})` : '',
135135
) as any);
136136
contentRefElm['s-cn'] = true;
137-
insertBefore(elm, contentRefElm, elm.firstChild);
137+
insertBefore(elm, contentRefElm, elm.firstChild as d.RenderNode);
138138
};

0 commit comments

Comments
 (0)