Skip to content

Commit e864132

Browse files
fix(runtime): add root scope id to the user provided nested children as classname (#5750)
* fix(runtime): add root scope id to the user provided nested children as classname #5749 * fix(runtime): add missing insertBefore patches --------- Co-authored-by: Christian Bromann <[email protected]>
1 parent e7f8129 commit e864132

File tree

8 files changed

+92
-37
lines changed

8 files changed

+92
-37
lines changed

src/runtime/connected-callback.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createTime } from './profile';
99
import { HYDRATE_ID, NODE_TYPE, PLATFORM_FLAGS } from './runtime-constants';
1010
import { addStyle } from './styles';
1111
import { attachToAncestor } from './update-component';
12+
import { insertBefore } from './vdom/vdom-render';
1213

1314
export const connectedCallback = (elm: d.HostElement) => {
1415
if ((plt.$flags$ & PLATFORM_FLAGS.isTmpDisconnected) === 0) {
@@ -128,5 +129,5 @@ const setContentReference = (elm: d.HostElement) => {
128129
BUILD.isDebug ? `content-ref (host=${elm.localName})` : '',
129130
) as any);
130131
contentRefElm['s-cn'] = true;
131-
elm.insertBefore(contentRefElm, elm.firstChild);
132+
insertBefore(elm, contentRefElm, elm.firstChild);
132133
};

src/runtime/dom-extras.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { CMP_FLAGS, HOST_FLAGS, NODE_TYPES } from '@utils/constants';
44

55
import type * as d from '../declarations';
66
import { PLATFORM_FLAGS } from './runtime-constants';
7-
import { updateFallbackSlotVisibility } from './vdom/vdom-render';
7+
import { insertBefore, updateFallbackSlotVisibility } from './vdom/vdom-render';
88

99
export const patchPseudoShadowDom = (
1010
hostElementPrototype: HTMLElement,
@@ -47,6 +47,7 @@ export const patchCloneNode = (HostElementPrototype: HTMLElement) => {
4747
's-nr',
4848
's-si',
4949
's-rf',
50+
's-rsc',
5051
];
5152

5253
for (; i < srcNode.childNodes.length; i++) {
@@ -84,7 +85,7 @@ export const patchSlotAppendChild = (HostElementPrototype: any) => {
8485
if (slotNode) {
8586
const slotChildNodes = getHostSlotChildNodes(slotNode, slotName);
8687
const appendAfter = slotChildNodes[slotChildNodes.length - 1];
87-
const insertedNode = appendAfter.parentNode.insertBefore(newChild, appendAfter.nextSibling);
88+
const insertedNode = insertBefore(appendAfter.parentNode, newChild, appendAfter.nextSibling);
8889

8990
// Check if there is fallback content that should be hidden
9091
updateFallbackSlotVisibility(this);
@@ -149,7 +150,7 @@ export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => {
149150

150151
const slotChildNodes = getHostSlotChildNodes(slotNode, slotName);
151152
const appendAfter = slotChildNodes[0];
152-
return appendAfter.parentNode.insertBefore(newChild, appendAfter.nextSibling);
153+
return insertBefore(appendAfter.parentNode, newChild, appendAfter.nextSibling);
153154
}
154155

155156
if (newChild.nodeType === 1 && !!newChild.getAttribute('slot')) {
@@ -309,7 +310,7 @@ export const patchTextContent = (hostElementPrototype: HTMLElement): void => {
309310
if (node['s-sn'] === '') {
310311
const textNode = this.ownerDocument.createTextNode(value);
311312
textNode['s-sn'] = '';
312-
node.parentElement.insertBefore(textNode, node.nextSibling);
313+
insertBefore(node.parentElement, textNode, node.nextSibling);
313314
} else {
314315
node.remove();
315316
}
@@ -352,7 +353,7 @@ export const patchTextContent = (hostElementPrototype: HTMLElement): void => {
352353
this.__textContent = value;
353354
const contentRefElm = this['s-cr'];
354355
if (contentRefElm) {
355-
this.insertBefore(contentRefElm, this.firstChild);
356+
insertBefore(this, contentRefElm, this.firstChild);
356357
}
357358
}
358359
},

src/runtime/test/shadow.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,14 @@ describe('shadow', () => {
8787
<cmp-a class="hydrated sc-cmp-a-h">
8888
<!---->
8989
<div class="sc-cmp-a sc-cmp-a-s">
90-
<span slot=\"start\">
90+
<span class="sc-cmp-a" slot=\"start\">
9191
Start
9292
</span>
9393
<span class='sc-cmp-a sc-cmp-a-s'>
9494
Text
9595
</span>
9696
<div class='end sc-cmp-a sc-cmp-a-s'>
97-
<span slot=\"end\">
97+
<span class="sc-cmp-a" slot=\"end\">
9898
End
9999
</span>
100100
</div>

src/runtime/vdom/vdom-annotations.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
SLOT_NODE_ID,
1111
TEXT_NODE_ID,
1212
} from '../runtime-constants';
13+
import { insertBefore } from './vdom-render';
1314

1415
/**
1516
* Updates the DOM generated on the server with annotations such as node attributes and
@@ -58,7 +59,7 @@ export const insertVdomAnnotations = (doc: Document, staticComponents: string[])
5859
}
5960
const commentBeforeTextNode = doc.createComment(childId);
6061
commentBeforeTextNode.nodeValue = `${TEXT_NODE_ID}.${childId}`;
61-
nodeRef.parentNode?.insertBefore(commentBeforeTextNode, nodeRef);
62+
insertBefore(nodeRef.parentNode, commentBeforeTextNode, nodeRef);
6263
}
6364
}
6465

@@ -220,7 +221,7 @@ const insertChildVNodeAnnotations = (
220221
const textNodeId = `${TEXT_NODE_ID}.${childId}`;
221222

222223
const commentBeforeTextNode = doc.createComment(textNodeId);
223-
parentNode?.insertBefore(commentBeforeTextNode, childElm);
224+
insertBefore(parentNode, commentBeforeTextNode, childElm);
224225
}
225226
} else if (childElm.nodeType === NODE_TYPE.CommentNode) {
226227
if (childElm['s-sr']) {

src/runtime/vdom/vdom-render.ts

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -111,19 +111,6 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex:
111111
elm.classList.add((elm['s-si'] = scopeId));
112112
}
113113

114-
if (BUILD.scoped) {
115-
// to be able to style the deep nested scoped component from the root component,
116-
// root component's scope id needs to be added to the child nodes since sass compiler
117-
// adds scope id to the nested selectors during compilation phase
118-
const rootScopeId =
119-
newParentVNode.$elm$?.['s-rsc'] || newParentVNode.$elm$?.['s-si'] || newParentVNode.$elm$?.['s-sc'];
120-
121-
if (rootScopeId) {
122-
elm['s-rsc'] = rootScopeId;
123-
!elm.classList.contains(rootScopeId) && elm.classList.add(rootScopeId);
124-
}
125-
}
126-
127114
if (newVNode.$children$) {
128115
for (i = 0; i < newVNode.$children$.length; ++i) {
129116
// create the node
@@ -206,7 +193,7 @@ const relocateToHostRoot = (parentElm: Element) => {
206193
for (const childNode of contentRefNode ? childNodeArray.reverse() : childNodeArray) {
207194
// Only relocate nodes that were slotted in
208195
if (childNode['s-sh'] != null) {
209-
host.insertBefore(childNode, contentRefNode ?? null);
196+
insertBefore(host, childNode, contentRefNode ?? null);
210197

211198
// Reset so we can correctly move the node around again.
212199
childNode['s-sh'] = undefined;
@@ -237,7 +224,7 @@ const putBackInOriginalLocation = (parentElm: d.RenderNode, recursive: boolean)
237224
const childNode = oldSlotChildNodes[i] as any;
238225
if (childNode['s-hn'] !== hostTagName && childNode['s-ol']) {
239226
// and relocate it back to it's original location
240-
parentReferenceNode(childNode).insertBefore(childNode, referenceNode(childNode));
227+
insertBefore(parentReferenceNode(childNode), childNode, referenceNode(childNode));
241228

242229
// remove the old original location comment entirely
243230
// later on the patch function will know what to do
@@ -293,7 +280,7 @@ const addVnodes = (
293280
childNode = createElm(null, parentVNode, startIdx, parentElm);
294281
if (childNode) {
295282
vnodes[startIdx].$elm$ = childNode as any;
296-
containerElm.insertBefore(childNode, BUILD.slotRelocation ? referenceNode(before) : before);
283+
insertBefore(containerElm, childNode, BUILD.slotRelocation ? referenceNode(before) : before);
297284
}
298285
}
299286
}
@@ -490,7 +477,7 @@ const updateChildren = (
490477
// `parentElm`. Luckily, `Node.nextSibling` will return `null` if there
491478
// aren't any siblings, and passing `null` to `Node.insertBefore` will
492479
// append it to the children of the parent element.
493-
parentElm.insertBefore(oldStartVnode.$elm$, oldEndVnode.$elm$.nextSibling as any);
480+
insertBefore(parentElm, oldStartVnode.$elm$, oldEndVnode.$elm$.nextSibling as any);
494481
oldStartVnode = oldCh[++oldStartIdx];
495482
newEndVnode = newCh[--newEndIdx];
496483
} else if (isSameVnode(oldEndVnode, newStartVnode, isInitialRender)) {
@@ -518,7 +505,7 @@ const updateChildren = (
518505
// can move the element for `oldEndVnode` _before_ the element for
519506
// `oldStartVnode`, leaving `oldStartVnode` to be reconciled in the
520507
// future.
521-
parentElm.insertBefore(oldEndVnode.$elm$, oldStartVnode.$elm$);
508+
insertBefore(parentElm, oldEndVnode.$elm$, oldStartVnode.$elm$);
522509
oldEndVnode = oldCh[--oldEndIdx];
523510
newStartVnode = newCh[++newStartIdx];
524511
} else {
@@ -569,9 +556,9 @@ const updateChildren = (
569556
if (node) {
570557
// if we created a new node then handle inserting it to the DOM
571558
if (BUILD.slotRelocation) {
572-
parentReferenceNode(oldStartVnode.$elm$).insertBefore(node, referenceNode(oldStartVnode.$elm$));
559+
insertBefore(parentReferenceNode(oldStartVnode.$elm$), node, referenceNode(oldStartVnode.$elm$));
573560
} else {
574-
oldStartVnode.$elm$.parentNode.insertBefore(node, oldStartVnode.$elm$);
561+
insertBefore(oldStartVnode.$elm$.parentNode, node, oldStartVnode.$elm$);
575562
}
576563
}
577564
}
@@ -924,6 +911,52 @@ export const nullifyVNodeRefs = (vNode: d.VNode) => {
924911
}
925912
};
926913

914+
/**
915+
* Inserts a node before a reference node as a child of a specified parent node.
916+
* Additionally, adds parent element's scope id as class to the new node.
917+
*
918+
* @param parent parent node
919+
* @param newNode element to be inserted
920+
* @param reference anchor element
921+
* @returns inserted node
922+
*/
923+
export const insertBefore = (parent: Node, newNode: Node, reference?: Node): Node => {
924+
const inserted = parent?.insertBefore(newNode, reference);
925+
926+
if (BUILD.scoped) {
927+
setParentScopeIdAsClassName(newNode as d.RenderNode, parent as d.RenderNode);
928+
}
929+
930+
return inserted;
931+
};
932+
933+
const findParentScopeId = (element: d.RenderNode): string | undefined => {
934+
return element
935+
? element['s-rsc'] || element['s-si'] || element['s-sc'] || findParentScopeId(element.parentElement)
936+
: undefined;
937+
};
938+
939+
/**
940+
* to be able to style the deep nested scoped component from the root component,
941+
* root component's scope id needs to be added to the child nodes since sass compiler
942+
* adds scope id to the nested selectors during compilation phase
943+
*
944+
* @param element an element to be updated
945+
* @param parent a parent element that scope id is retrieved
946+
*/
947+
export const setParentScopeIdAsClassName = (element: d.RenderNode, parent: d.RenderNode) => {
948+
if (element && parent) {
949+
const oldRootScopeId = element['s-rsc'];
950+
const newRootScopeId = findParentScopeId(parent);
951+
952+
oldRootScopeId && element.classList?.contains(oldRootScopeId) && element.classList.remove(oldRootScopeId);
953+
if (newRootScopeId) {
954+
element['s-rsc'] = newRootScopeId;
955+
!element.classList?.contains(newRootScopeId) && element.classList?.add(newRootScopeId);
956+
}
957+
}
958+
};
959+
927960
/**
928961
* Information about nodes to be relocated in order to support
929962
* `<slot>` elements in scoped (i.e. non-shadow DOM) components
@@ -1045,7 +1078,7 @@ render() {
10451078
: (doc.createTextNode('') as any);
10461079
orgLocationNode['s-nr'] = nodeToRelocate;
10471080

1048-
nodeToRelocate.parentNode.insertBefore((nodeToRelocate['s-ol'] = orgLocationNode), nodeToRelocate);
1081+
insertBefore(nodeToRelocate.parentNode, (nodeToRelocate['s-ol'] = orgLocationNode), nodeToRelocate);
10491082
}
10501083
}
10511084

@@ -1115,7 +1148,7 @@ render() {
11151148
// If we get to this point and `insertBeforeNode` is `null`, that means
11161149
// we're just going to append the node as the last child of the parent. Passing
11171150
// `null` as the second arg here will trigger that behavior.
1118-
parentNodeRef.insertBefore(nodeToRelocate, insertBeforeNode);
1151+
insertBefore(parentNodeRef, nodeToRelocate, insertBeforeNode);
11191152

11201153
// Reset the `hidden` value back to what it was defined as originally
11211154
// This solves a problem where a `slot` is dynamically rendered and `hidden` may have

test/wdio/scoped-id-in-nested-classname/cmp-level-1.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
cmp-level-2 {
33
cmp-level-3 {
44
padding: 32px;
5+
6+
#test-element {
7+
padding: 24px;
8+
}
59
}
610
}
711
}

test/wdio/scoped-id-in-nested-classname/cmp-level-1.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import { Component, h } from '@stencil/core';
88
})
99
export class CmpLevel1 {
1010
render() {
11-
return <cmp-level-2></cmp-level-2>;
11+
return (
12+
<cmp-level-2>
13+
<slot />
14+
</cmp-level-2>
15+
);
1216
}
1317
}

test/wdio/scoped-id-in-nested-classname/cmp.test.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,27 @@ import { h } from '@stencil/core';
22
import { render } from '@wdio/browser-runner/stencil';
33

44
describe('scope-id-in-nested-classname', function () {
5-
beforeEach(() => {
5+
it('should have root scope id in the nested element as classname', async () => {
66
render({
77
template: () => <cmp-level-1></cmp-level-1>,
88
});
9-
});
10-
11-
it('should have root scope id in the nested element as classname', async () => {
129
await expect($('cmp-level-3')).toHaveElementClass('sc-cmp-level-1');
1310

1411
const appliedCss = await (await $('cmp-level-3')).getCSSProperty('padding');
1512
await expect(appliedCss.parsed.value).toBe(32);
1613
});
14+
15+
it('should have root scope id in the user provided nested element as classname', async () => {
16+
render({
17+
template: () => (
18+
<cmp-level-1>
19+
<span id="test-element">Test</span>
20+
</cmp-level-1>
21+
),
22+
});
23+
await expect($('#test-element')).toHaveElementClass('sc-cmp-level-1');
24+
25+
const appliedCss = await (await $('#test-element')).getCSSProperty('padding');
26+
await expect(appliedCss.parsed.value).toBe(24);
27+
});
1728
});

0 commit comments

Comments
 (0)