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

fix(scoped): fixes for <slot /> and slotted nodes #6082

Merged
Merged
Show file tree
Hide file tree
Changes from 5 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
11 changes: 8 additions & 3 deletions src/declarations/stencil-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,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 @@ -1389,9 +1393,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
Loading