Skip to content

Commit 9e38aa7

Browse files
johnjenkinsJohn Jenkins
andauthored
fix(runtime): delay non-shadow onConnectedCallback; make sure slotted content is available (#6519)
* fix(runtime): delay non-shadow onConnectedCallback to make sure slotted content is available * chore: * chore: --------- Co-authored-by: John Jenkins <john.jenkins@nanoporetech.com>
1 parent dfeeaec commit 9e38aa7

File tree

8 files changed

+182
-1
lines changed

8 files changed

+182
-1
lines changed

src/declarations/stencil-private.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,6 +1812,10 @@ export interface HostRef {
18121812
$rmListeners$?: (() => void)[];
18131813
$modeName$?: string;
18141814
$renderCount$?: number;
1815+
/**
1816+
* Defer connectedCallback until after first render for components with slot relocation.
1817+
*/
1818+
$deferredConnectedCallback$?: boolean;
18151819
}
18161820

18171821
export interface PlatformRuntime {

src/runtime/initialize-component.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,15 @@ export const initializeComponent = async (
9090
hostRef.$flags$ |= HOST_FLAGS.isWatchReady;
9191
}
9292
endNewInstance();
93-
fireConnectedCallback(hostRef.$lazyInstance$, elm);
93+
94+
// For components that relocate slots, defer connectedCallback until after first render
95+
// so that slotted content is available
96+
const needsDeferredCallback = BUILD.slotRelocation && cmpMeta.$flags$ & CMP_FLAGS.hasSlotRelocation;
97+
if (!needsDeferredCallback) {
98+
fireConnectedCallback(hostRef.$lazyInstance$, elm);
99+
} else {
100+
hostRef.$deferredConnectedCallback$ = true;
101+
}
94102
} else {
95103
// sync constructor component
96104
Cstr = elm.constructor as any;

src/runtime/update-component.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,21 @@ const dispatchHooks = (hostRef: d.HostRef, isInitialLoad: boolean): Promise<void
9898

9999
if (isInitialLoad) {
100100
if (BUILD.lazyLoad) {
101+
// Fire deferred connectedCallback before componentWillLoad
102+
if (BUILD.slotRelocation && hostRef.$deferredConnectedCallback$) {
103+
hostRef.$deferredConnectedCallback$ = false;
104+
safeCall(instance, 'connectedCallback', undefined, elm);
105+
}
106+
101107
if (BUILD.hostListener) {
102108
hostRef.$flags$ |= HOST_FLAGS.isListenReady;
103109
if (hostRef.$queuedListeners$) {
104110
hostRef.$queuedListeners$.map(([methodName, event]) => safeCall(instance, methodName, event, elm));
105111
hostRef.$queuedListeners$ = undefined;
106112
}
107113
}
114+
115+
// Fire any pending fetch callbacks
108116
if (hostRef.$fetchedCbList$.length) {
109117
hostRef.$fetchedCbList$.forEach((cb) => cb(elm));
110118
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Component, Element, h } from '@stencil/core';
2+
3+
@Component({
4+
tag: 'scoped-slot-connectedcallback-child',
5+
shadow: false,
6+
})
7+
export class ScopedSlotConnectedCallbackChild {
8+
@Element() el: HTMLElement;
9+
10+
connectedCallback() {
11+
// Check if slotted content is available in connectedCallback
12+
const slottedContent = this.el.querySelector('#slotted-content');
13+
if (slottedContent) {
14+
this.el.setAttribute('data-connected-slot-available', 'true');
15+
} else {
16+
this.el.setAttribute('data-connected-slot-available', 'false');
17+
}
18+
}
19+
20+
componentWillLoad() {
21+
// Also check in componentWillLoad for comparison
22+
const slottedContent = this.el.querySelector('#slotted-content');
23+
if (slottedContent) {
24+
this.el.setAttribute('data-willload-slot-available', 'true');
25+
} else {
26+
this.el.setAttribute('data-willload-slot-available', 'false');
27+
}
28+
}
29+
30+
render() {
31+
return (
32+
<div class="wrapper">
33+
Before slot | <slot /> | After slot
34+
</div>
35+
);
36+
}
37+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Component, h } from '@stencil/core';
2+
3+
@Component({
4+
tag: 'scoped-slot-connectedcallback-middle',
5+
shadow: false,
6+
})
7+
export class ScopedSlotConnectedCallbackMiddle {
8+
render() {
9+
return (
10+
<scoped-slot-connectedcallback-child>
11+
<span id="slotted-content">Slotted Content</span>
12+
</scoped-slot-connectedcallback-child>
13+
);
14+
}
15+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Component, h } from '@stencil/core';
2+
3+
@Component({
4+
tag: 'scoped-slot-connectedcallback-parent',
5+
shadow: false,
6+
})
7+
export class ScopedSlotConnectedCallbackParent {
8+
render() {
9+
return <scoped-slot-connectedcallback-middle />;
10+
}
11+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { h } from '@stencil/core';
2+
import { render } from '@wdio/browser-runner/stencil';
3+
import { $, browser, expect } from '@wdio/globals';
4+
5+
import { setupIFrameTest } from '../util.js';
6+
7+
describe('scoped-slot-connectedcallback', function () {
8+
describe('lazy load (dist output)', () => {
9+
beforeEach(() => {
10+
render({
11+
components: [],
12+
template: () => <scoped-slot-connectedcallback-parent />,
13+
});
14+
});
15+
16+
it('should have slotted content available in connectedCallback', async () => {
17+
await $('scoped-slot-connectedcallback-child').waitForExist();
18+
19+
const child = await $('scoped-slot-connectedcallback-child');
20+
const connectedAttr = await child.getAttribute('data-connected-slot-available');
21+
const willLoadAttr = await child.getAttribute('data-willload-slot-available');
22+
23+
// Both connectedCallback and componentWillLoad should have access to slotted content
24+
expect(connectedAttr).toBe('true');
25+
expect(willLoadAttr).toBe('true');
26+
});
27+
28+
it('should render slotted content correctly', async () => {
29+
await $('scoped-slot-connectedcallback-child').waitForExist();
30+
31+
const slottedContent = await $('#slotted-content');
32+
await expect(slottedContent).toBeExisting();
33+
await expect(slottedContent).toHaveText('Slotted Content');
34+
35+
const wrapper = await $('.wrapper');
36+
const text = await wrapper.getText();
37+
expect(text).toContain('Before slot');
38+
expect(text).toContain('Slotted Content');
39+
expect(text).toContain('After slot');
40+
});
41+
});
42+
43+
describe('dist-custom-elements output', () => {
44+
let doc: Document;
45+
46+
beforeEach(async () => {
47+
await setupIFrameTest('/scoped-slot-connectedcallback/custom-element.html', 'custom-elements-iframe');
48+
const frameEle: HTMLIFrameElement = document.querySelector('iframe#custom-elements-iframe');
49+
doc = frameEle.contentDocument;
50+
51+
// Render the component inside the iframe
52+
const parent = doc.createElement('scoped-slot-connectedcallback-parent');
53+
doc.body.appendChild(parent);
54+
55+
// Wait for component to be ready
56+
await browser.waitUntil(() => Boolean(doc.querySelector('scoped-slot-connectedcallback-child')));
57+
});
58+
59+
it('should have slotted content available in connectedCallback', async () => {
60+
const child = doc.querySelector('scoped-slot-connectedcallback-child');
61+
const connectedAttr = child.getAttribute('data-connected-slot-available');
62+
const willLoadAttr = child.getAttribute('data-willload-slot-available');
63+
64+
// Both connectedCallback and componentWillLoad should have access to slotted content
65+
expect(connectedAttr).toBe('true');
66+
expect(willLoadAttr).toBe('true');
67+
});
68+
69+
it('should render slotted content correctly', async () => {
70+
const slottedContent = doc.querySelector('#slotted-content');
71+
expect(slottedContent).toBeTruthy();
72+
expect(slottedContent.textContent).toBe('Slotted Content');
73+
74+
const wrapper = doc.querySelector('.wrapper');
75+
const text = wrapper.textContent;
76+
expect(text).toContain('Before slot');
77+
expect(text).toContain('Slotted Content');
78+
expect(text).toContain('After slot');
79+
});
80+
});
81+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<html>
2+
<head>
3+
<title>Scoped Slot ConnectedCallback - dist-custom-elements output</title>
4+
<script type="module">
5+
import { defineCustomElement as childDefine } from '/test-components/scoped-slot-connectedcallback-child.js';
6+
import { defineCustomElement as middleDefine } from '/test-components/scoped-slot-connectedcallback-middle.js';
7+
import { defineCustomElement as parentDefine } from '/test-components/scoped-slot-connectedcallback-parent.js';
8+
9+
childDefine();
10+
middleDefine();
11+
parentDefine();
12+
</script>
13+
</head>
14+
<body>
15+
16+
</body>
17+
</html>

0 commit comments

Comments
 (0)