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( + ``, + ) + 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') +})