diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts
index ccd8989b377..97389c8e6ec 100644
--- a/packages/runtime-dom/src/apiCustomElement.ts
+++ b/packages/runtime-dom/src/apiCustomElement.ts
@@ -487,6 +487,10 @@ export class VueElement
delete this._props[key]
} else {
this._props[key] = val
+ // support set key on ceVNode
+ if (key === 'key' && this._app) {
+ this._app._ceVNode!.key = val
+ }
}
if (shouldUpdate && this._instance) {
this._update()
diff --git a/packages/vue/__tests__/e2e/ssr-custom-element.html b/packages/vue/__tests__/e2e/ssr-custom-element.html
deleted file mode 100644
index 14139c2d52d..00000000000
--- a/packages/vue/__tests__/e2e/ssr-custom-element.html
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
diff --git a/packages/vue/__tests__/e2e/ssr-custom-element.spec.ts b/packages/vue/__tests__/e2e/ssr-custom-element.spec.ts
index 0c8413d1769..c39286d3d12 100644
--- a/packages/vue/__tests__/e2e/ssr-custom-element.spec.ts
+++ b/packages/vue/__tests__/e2e/ssr-custom-element.spec.ts
@@ -3,13 +3,57 @@ import { setupPuppeteer } from './e2eUtils'
const { page, click, text } = setupPuppeteer()
+beforeEach(async () => {
+ await page().addScriptTag({
+ path: path.resolve(__dirname, '../../dist/vue.global.js'),
+ })
+})
+
+async function setContent(html: string) {
+ await page().setContent(`
${html}
`)
+}
+
// this must be tested in actual Chrome because jsdom does not support
// declarative shadow DOM
test('ssr custom element hydration', async () => {
- await page().goto(
- `file://${path.resolve(__dirname, './ssr-custom-element.html')}`,
+ await setContent(
+ ``,
)
+ await page().evaluate(() => {
+ const {
+ h,
+ ref,
+ defineSSRCustomElement,
+ defineAsyncComponent,
+ onMounted,
+ useHost,
+ } = (window as any).Vue
+
+ const def = {
+ setup() {
+ const count = ref(1)
+ const el = useHost()
+ onMounted(() => (el.style.border = '1px solid red'))
+
+ return () => h('button', { onClick: () => count.value++ }, count.value)
+ },
+ }
+
+ customElements.define('my-element', defineSSRCustomElement(def))
+ customElements.define(
+ 'my-element-async',
+ defineSSRCustomElement(
+ defineAsyncComponent(
+ () =>
+ new Promise(r => {
+ ;(window as any).resolve = () => r(def)
+ }),
+ ),
+ ),
+ )
+ })
+
function getColor() {
return page().evaluate(() => {
return [
@@ -33,3 +77,55 @@ test('ssr custom element hydration', async () => {
await assertInteraction('my-element')
await assertInteraction('my-element-async')
})
+
+// #11641
+test('pass key to custom element', async () => {
+ const messages: string[] = []
+ page().on('console', e => messages.push(e.text()))
+
+ await setContent(
+ `1
`,
+ )
+ await page().evaluate(() => {
+ const {
+ h,
+ ref,
+ defineSSRCustomElement,
+ onBeforeUnmount,
+ onMounted,
+ createSSRApp,
+ renderList,
+ } = (window as any).Vue
+
+ const MyElement = defineSSRCustomElement({
+ props: {
+ str: String,
+ },
+ setup(props: any) {
+ onMounted(() => {
+ console.log('child mounted')
+ })
+ onBeforeUnmount(() => {
+ console.log('child unmount')
+ })
+ return () => h('div', props.str)
+ },
+ })
+ customElements.define('my-element', MyElement)
+
+ createSSRApp({
+ setup() {
+ const arr = ref(['1'])
+ // pass key to custom element
+ return () =>
+ renderList(arr.value, (i: string) =>
+ h('my-element', { key: i, str: i }, null),
+ )
+ },
+ }).mount('#app')
+ })
+
+ expect(messages.includes('child mounted')).toBe(true)
+ expect(messages.includes('child unmount')).toBe(false)
+ expect(await text('my-element >>> div')).toBe('1')
+})