Skip to content

Commit 2c5b7f8

Browse files
fix(hydrate): support server side rendering of components with listener (#5877)
1 parent bb2e04f commit 2c5b7f8

File tree

6 files changed

+96
-4
lines changed

6 files changed

+96
-4
lines changed

src/hydrate/platform/hydrate-app.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export function hydrateApp(
120120

121121
// add it to our Set so we know it's already being connected
122122
connectedElements.add(elm);
123-
return hydrateComponent(win, results, elm.nodeName, elm, waitingElements);
123+
return hydrateComponent.call(elm, win, results, elm.nodeName, elm, waitingElements);
124124
}
125125
}
126126

@@ -163,6 +163,7 @@ export function hydrateApp(
163163
}
164164

165165
async function hydrateComponent(
166+
this: HTMLElement,
166167
win: Window & typeof globalThis,
167168
results: d.HydrateResults,
168169
tagName: string,

test/end-to-end/src/components.d.ts

+13
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export namespace Components {
4949
}
5050
interface DomVisible {
5151
}
52+
interface DsdListenCmp {
53+
}
5254
interface ElementCmp {
5355
}
5456
interface EmptyCmp {
@@ -257,6 +259,12 @@ declare global {
257259
prototype: HTMLDomVisibleElement;
258260
new (): HTMLDomVisibleElement;
259261
};
262+
interface HTMLDsdListenCmpElement extends Components.DsdListenCmp, HTMLStencilElement {
263+
}
264+
var HTMLDsdListenCmpElement: {
265+
prototype: HTMLDsdListenCmpElement;
266+
new (): HTMLDsdListenCmpElement;
267+
};
260268
interface HTMLElementCmpElement extends Components.ElementCmp, HTMLStencilElement {
261269
}
262270
var HTMLElementCmpElement: {
@@ -401,6 +409,7 @@ declare global {
401409
"dom-api": HTMLDomApiElement;
402410
"dom-interaction": HTMLDomInteractionElement;
403411
"dom-visible": HTMLDomVisibleElement;
412+
"dsd-listen-cmp": HTMLDsdListenCmpElement;
404413
"element-cmp": HTMLElementCmpElement;
405414
"empty-cmp": HTMLEmptyCmpElement;
406415
"empty-cmp-shadow": HTMLEmptyCmpShadowElement;
@@ -464,6 +473,8 @@ declare namespace LocalJSX {
464473
}
465474
interface DomVisible {
466475
}
476+
interface DsdListenCmp {
477+
}
467478
interface ElementCmp {
468479
}
469480
interface EmptyCmp {
@@ -532,6 +543,7 @@ declare namespace LocalJSX {
532543
"dom-api": DomApi;
533544
"dom-interaction": DomInteraction;
534545
"dom-visible": DomVisible;
546+
"dsd-listen-cmp": DsdListenCmp;
535547
"element-cmp": ElementCmp;
536548
"empty-cmp": EmptyCmp;
537549
"empty-cmp-shadow": EmptyCmpShadow;
@@ -575,6 +587,7 @@ declare module "@stencil/core" {
575587
"dom-api": LocalJSX.DomApi & JSXBase.HTMLAttributes<HTMLDomApiElement>;
576588
"dom-interaction": LocalJSX.DomInteraction & JSXBase.HTMLAttributes<HTMLDomInteractionElement>;
577589
"dom-visible": LocalJSX.DomVisible & JSXBase.HTMLAttributes<HTMLDomVisibleElement>;
590+
"dsd-listen-cmp": LocalJSX.DsdListenCmp & JSXBase.HTMLAttributes<HTMLDsdListenCmpElement>;
578591
"element-cmp": LocalJSX.ElementCmp & JSXBase.HTMLAttributes<HTMLElementCmpElement>;
579592
"empty-cmp": LocalJSX.EmptyCmp & JSXBase.HTMLAttributes<HTMLEmptyCmpElement>;
580593
"empty-cmp-shadow": LocalJSX.EmptyCmpShadow & JSXBase.HTMLAttributes<HTMLEmptyCmpShadowElement>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:host {
2+
display: block;
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Component, Element, h, Host, Listen } from '@stencil/core';
2+
3+
@Component({
4+
tag: 'dsd-listen-cmp',
5+
styleUrl: 'dsd-listen-cmp.css',
6+
shadow: true,
7+
})
8+
export class MyWhateverComponent {
9+
@Element() hostElement: HTMLSlotElement;
10+
private slotRef: HTMLSlotElement;
11+
12+
@Listen('keydown', { capture: true }) // Crashes, incorrect binding in hydrate index.js
13+
handleKeyPress(e: CustomEvent): void {
14+
e.stopPropagation();
15+
console.log(this.slotRef);
16+
}
17+
18+
render() {
19+
return (
20+
<Host>
21+
<slot ref={(el: HTMLSlotElement) => (this.slotRef = el)}></slot>
22+
</Host>
23+
);
24+
}
25+
}

test/end-to-end/src/declarative-shadow-dom/test.e2e.ts

+48
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,52 @@ describe('renderToString', () => {
205205
const button = await page.find('cmp-server-vs-client');
206206
expect(button.shadowRoot.querySelector('div')).toEqualText('Server vs Client? Winner: Client');
207207
});
208+
209+
it('can hydrate components with event listeners', async () => {
210+
const { html } = await renderToString(
211+
`
212+
<dsd-listen-cmp>Hello World</dsd-listen-cmp>
213+
<car-list cars=${JSON.stringify([vento, beetle])}></car-list>
214+
`,
215+
{
216+
serializeShadowRoot: true,
217+
fullDocument: false,
218+
},
219+
);
220+
221+
/**
222+
* renders the component with listener with proper vdom annotation, e.g.
223+
* ```html
224+
* <dsd-listen-cmp class="sc-dsd-listen-cmp-h" custom-hydrate-flag="" s-id="1">
225+
* <template shadowrootmode="open">
226+
* <style sty-id="sc-dsd-listen-cmp">
227+
* .sc-dsd-listen-cmp-h{display:block}
228+
* </style>
229+
* <slot c-id="1.0.0.0" class="sc-dsd-listen-cmp"></slot>
230+
* </template>
231+
* <!--r.1-->
232+
* Hello World
233+
* </dsd-listen-cmp>
234+
* ```
235+
*/
236+
237+
expect(html).toContain(
238+
`<dsd-listen-cmp class=\"sc-dsd-listen-cmp-h\" custom-hydrate-flag=\"\" s-id=\"1\"><template shadowrootmode=\"open\"><style sty-id=\"sc-dsd-listen-cmp\">/*!@:host*/.sc-dsd-listen-cmp-h{display:block}</style><slot class=\"sc-dsd-listen-cmp\" c-id=\"1.0.0.0\"></slot></template><!--r.1-->Hello World</dsd-listen-cmp>`,
239+
);
240+
241+
/**
242+
* renders second component with proper vdom annotation, e.g.:
243+
* ```html
244+
* <car-detail c-id="2.4.2.0" class="sc-car-list" custom-hydrate-flag="" s-id="4">
245+
* <!--r.4-->
246+
* <section c-id="4.0.0.0" class="sc-car-list">
247+
* <!--t.4.1.1.0-->
248+
* 2023 VW Beetle
249+
* </section>
250+
* </car-detail>
251+
*/
252+
expect(html).toContain(
253+
`<car-detail class=\"sc-car-list\" custom-hydrate-flag=\"\" c-id=\"2.4.2.0\" s-id=\"4\"><!--r.4--><section class=\"sc-car-list\" c-id=\"4.0.0.0\"><!--t.4.1.1.0-->2023 VW Beetle</section></car-detail>`,
254+
);
255+
});
208256
});

test/end-to-end/src/miscellaneous/test.e2e.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { type E2EPage, newE2EPage } from '@stencil/core/testing';
22

33
let page: E2EPage;
44

5+
function checkSorted(arr: string[]) {
6+
return arr.every((value, index, array) => index === 0 || value >= array[index - 1]);
7+
}
8+
59
describe('do not throw page already closed if page was defined in before(All) hook', () => {
610
beforeAll(async () => {
711
page = await newE2EPage();
@@ -38,8 +42,6 @@ describe('sorts hydrated component styles', () => {
3842
.split('\n')
3943
.map((c) => c.slice(0, c.indexOf('{')))
4044
.find((c) => c.includes('app-root'));
41-
expect(classSelector).toBe(
42-
'another-car-detail,another-car-list,app-root,build-data,car-detail,car-list,cmp-a,cmp-b,cmp-c,cmp-dsd,cmp-server-vs-client,dom-api,dom-interaction,dom-visible,element-cmp,empty-cmp,empty-cmp-shadow,env-data,event-cmp,import-assets,listen-cmp,method-cmp,path-alias-cmp,prerender-cmp,prop-cmp,scoped-car-detail,scoped-car-list,slot-cmp,slot-cmp-container,slot-parent-cmp,state-cmp',
43-
);
45+
expect(checkSorted(classSelector.split(','))).toBeTruthy();
4446
});
4547
});

0 commit comments

Comments
 (0)