Skip to content

Commit e893bd1

Browse files
johnjenkinsJohn Jenkins
andauthored
fix(runtime): allow cloneNode patch even without <slot> (#6513)
* fix(runtime): allow `cloneNode` patch even without <slot> * chore: --------- Co-authored-by: John Jenkins <john.jenkins@nanoporetech.com>
1 parent 5c17422 commit e893bd1

File tree

7 files changed

+116
-2
lines changed

7 files changed

+116
-2
lines changed

src/runtime/bootstrap-custom-element.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export const proxyCustomElement = (Cstr: any, compactMeta: d.ComponentRuntimeMet
7373
patchTextContent(Cstr.prototype);
7474
}
7575
}
76+
} else if (BUILD.cloneNodeFix) {
77+
patchCloneNode(Cstr.prototype);
7678
}
7779

7880
if (BUILD.hydrateClientSide && BUILD.shadowDom) {

src/runtime/bootstrap-lazy.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d.
215215
patchTextContent(HostElement.prototype);
216216
}
217217
}
218+
} else if (BUILD.cloneNodeFix) {
219+
patchCloneNode(HostElement.prototype);
218220
}
219221

220222
// if the component is formAssociated we need to set that on the host

src/runtime/dom-extras.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ export const patchPseudoShadowDom = (hostElementPrototype: HTMLElement) => {
3434
*
3535
* @param HostElementPrototype The Stencil component to be patched
3636
*/
37-
export const patchCloneNode = (HostElementPrototype: HTMLElement) => {
38-
const orgCloneNode = HostElementPrototype.cloneNode;
37+
export const patchCloneNode = (HostElementPrototype: any) => {
38+
if (HostElementPrototype.__cloneNode) return;
39+
const orgCloneNode = (HostElementPrototype.__cloneNode = HostElementPrototype.cloneNode);
3940

4041
HostElementPrototype.cloneNode = function (deep?: boolean) {
4142
const srcNode = this;
@@ -91,6 +92,7 @@ export const patchCloneNode = (HostElementPrototype: HTMLElement) => {
9192
* @param HostElementPrototype The Stencil component to be patched
9293
*/
9394
export const patchSlotAppendChild = (HostElementPrototype: any) => {
95+
if (HostElementPrototype.__appendChild) return;
9496
HostElementPrototype.__appendChild = HostElementPrototype.appendChild;
9597

9698
HostElementPrototype.appendChild = function (this: d.RenderNode, newChild: d.RenderNode) {
@@ -122,6 +124,7 @@ export const patchSlotAppendChild = (HostElementPrototype: any) => {
122124
* @param ElementPrototype The Stencil component to be patched
123125
*/
124126
const patchSlotRemoveChild = (ElementPrototype: any) => {
127+
if (ElementPrototype.__removeChild) return;
125128
ElementPrototype.__removeChild = ElementPrototype.removeChild;
126129

127130
ElementPrototype.removeChild = function (this: d.RenderNode, toRemove: d.RenderNode) {
@@ -146,6 +149,7 @@ const patchSlotRemoveChild = (ElementPrototype: any) => {
146149
* @param HostElementPrototype the `Element` to be patched
147150
*/
148151
export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => {
152+
if ((HostElementPrototype as any).__prepend) return;
149153
(HostElementPrototype as any).__prepend = HostElementPrototype.prepend;
150154

151155
HostElementPrototype.prepend = function (this: d.HostElement, ...newChildren: (d.RenderNode | string)[]) {
@@ -183,6 +187,7 @@ export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => {
183187
* @param HostElementPrototype the `Element` to be patched
184188
*/
185189
export const patchSlotAppend = (HostElementPrototype: HTMLElement) => {
190+
if ((HostElementPrototype as any).__append) return;
186191
(HostElementPrototype as any).__append = HostElementPrototype.append;
187192
HostElementPrototype.append = function (this: d.HostElement, ...newChildren: (d.RenderNode | string)[]) {
188193
newChildren.forEach((newChild: d.RenderNode | string) => {
@@ -202,6 +207,7 @@ export const patchSlotAppend = (HostElementPrototype: HTMLElement) => {
202207
* @param HostElementPrototype the `Element` to be patched
203208
*/
204209
export const patchSlotInsertAdjacentHTML = (HostElementPrototype: HTMLElement) => {
210+
if ((HostElementPrototype as any).__insertAdjacentHTML) return;
205211
const originalInsertAdjacentHtml = HostElementPrototype.insertAdjacentHTML;
206212

207213
HostElementPrototype.insertAdjacentHTML = function (this: d.HostElement, position: InsertPosition, text: string) {
@@ -249,6 +255,7 @@ export const patchSlotInsertAdjacentText = (HostElementPrototype: HTMLElement) =
249255
* @param HostElementPrototype the custom element prototype to patch
250256
*/
251257
const patchInsertBefore = (HostElementPrototype: HTMLElement) => {
258+
if ((HostElementPrototype as any).__insertBefore) return;
252259
const eleProto: d.RenderNode = HostElementPrototype;
253260
if (eleProto.__insertBefore) return;
254261

@@ -318,6 +325,7 @@ const patchInsertBefore = (HostElementPrototype: HTMLElement) => {
318325
* @param HostElementPrototype the `Element` to be patched
319326
*/
320327
export const patchSlotInsertAdjacentElement = (HostElementPrototype: HTMLElement) => {
328+
if ((HostElementPrototype as any).__insertAdjacentElement) return;
321329
const originalInsertAdjacentElement = HostElementPrototype.insertAdjacentElement;
322330

323331
HostElementPrototype.insertAdjacentElement = function (

test/wdio/clone-node/cmp-root.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Component, Element, h } from '@stencil/core';
2+
3+
@Component({
4+
tag: 'clone-node-root',
5+
})
6+
export class CloneNodeRoot {
7+
@Element() el!: HTMLElement;
8+
9+
cloneSlides() {
10+
this.el.querySelectorAll('clone-node-slide').forEach((slide) => {
11+
const clonedSlide = slide.cloneNode(true) as HTMLElement;
12+
this.el.appendChild(clonedSlide);
13+
});
14+
}
15+
16+
render() {
17+
return (
18+
<div>
19+
<button onClick={this.cloneSlides.bind(this)}>Clone Slides</button>
20+
<clone-node-slide>
21+
<clone-node-text></clone-node-text>
22+
</clone-node-slide>
23+
</div>
24+
);
25+
}
26+
}

test/wdio/clone-node/cmp-slide.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Component, h, Host } from '@stencil/core';
2+
3+
@Component({
4+
tag: 'clone-node-slide',
5+
})
6+
export class CloneNodeSlide {
7+
render() {
8+
return (
9+
<Host>
10+
<p class="slide-content">Slide Content</p>
11+
<slot></slot>
12+
</Host>
13+
);
14+
}
15+
}

test/wdio/clone-node/cmp-text.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Component, h, Host } from '@stencil/core';
2+
3+
@Component({
4+
tag: 'clone-node-text',
5+
})
6+
export class CloneNodeText {
7+
render() {
8+
return (
9+
<Host>
10+
<p class="text-content">Clone Node Text</p>
11+
</Host>
12+
);
13+
}
14+
}

test/wdio/clone-node/cmp.test.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { h } from '@stencil/core';
2+
import { render } from '@wdio/browser-runner/stencil';
3+
import { $, $$, browser, expect } from '@wdio/globals';
4+
5+
describe('clone-node', () => {
6+
before(async () => {
7+
render({
8+
components: [],
9+
template: () => <clone-node-root></clone-node-root>,
10+
});
11+
});
12+
13+
it('should not duplicate nested component content when cloning', async () => {
14+
await $('clone-node-root.hydrated').waitForExist();
15+
16+
// Initially should have 1 slide with 1 text component containing 1 paragraph
17+
const initialTextElements = await $$('.text-content');
18+
expect(initialTextElements).toHaveLength(1);
19+
20+
const initialSlideElements = await $$('.slide-content');
21+
expect(initialSlideElements).toHaveLength(1);
22+
23+
// Click the button to clone the slide
24+
const button = await $('button');
25+
await button.click();
26+
27+
// Wait for the clone to be added
28+
await browser.pause(100);
29+
30+
// After cloning, should have 2 slides with 2 text components
31+
// Each text component should have exactly 1 paragraph (not duplicated)
32+
const clonedTextElements = await $$('.text-content');
33+
expect(clonedTextElements).toHaveLength(2);
34+
35+
const clonedSlideElements = await $$('.slide-content');
36+
expect(clonedSlideElements).toHaveLength(2);
37+
38+
// Verify each text element has the correct content
39+
for (const textEl of clonedTextElements) {
40+
await expect(textEl).toHaveText('Clone Node Text');
41+
}
42+
43+
for (const slideEl of clonedSlideElements) {
44+
await expect(slideEl).toHaveText('Slide Content');
45+
}
46+
});
47+
});

0 commit comments

Comments
 (0)