Jelajahi Sumber

fix(custom-element): handle keys set on custom elements (#11655)

close #11641
edison 1 tahun lalu
induk
melakukan
f1d1831f07

+ 4 - 0
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()

+ 0 - 44
packages/vue/__tests__/e2e/ssr-custom-element.html

@@ -1,44 +0,0 @@
-<script src="../../dist/vue.global.js"></script>
-
-<my-element
-  ><template shadowrootmode="open"><button>1</button></template></my-element
->
-<my-element-async
-  ><template shadowrootmode="open"
-    ><button>1</button></template
-  ></my-element-async
->
-
-<script>
-  const {
-    h,
-    ref,
-    defineSSRCustomElement,
-    defineAsyncComponent,
-    onMounted,
-    useHost,
-  } = 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.resolve = () => r(def)
-          }),
-      ),
-    ),
-  )
-</script>

+ 98 - 2
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(`<div id="app">${html}</div>`)
+}
+
 // 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(
+    `<my-element><template shadowrootmode="open"><button>1</button></template></my-element><my-element-async><template shadowrootmode="open"><button>1</button></template></my-element-async>`,
   )
 
+  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(
+    `<!--[--><my-element str="1"><template shadowrootmode="open"><div>1</div></template></my-element><!--]-->`,
+  )
+  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')
+})