]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(custom-element): support for expose on customElement (#6256)
author郝晨光 <2293885211@qq.com>
Sat, 3 Aug 2024 06:48:21 +0000 (14:48 +0800)
committerGitHub <noreply@github.com>
Sat, 3 Aug 2024 06:48:21 +0000 (14:48 +0800)
close #5540

packages/runtime-dom/__tests__/customElement.spec.ts
packages/runtime-dom/src/apiCustomElement.ts

index 1762f0cec648d6e6695cdea38676fef6ec533bc6..91596b67f1ca14eddf684637ae9620eb4e215211 100644 (file)
@@ -1,3 +1,4 @@
+import type { MockedFunction } from 'vitest'
 import {
   type Ref,
   type VueElement,
@@ -881,4 +882,64 @@ describe('defineCustomElement', () => {
       expect(style.textContent).toBe(`div { color: red; }`)
     })
   })
+
+  describe('expose', () => {
+    test('expose attributes and callback', async () => {
+      type SetValue = (value: string) => void
+      let fn: MockedFunction<SetValue>
+
+      const E = defineCustomElement({
+        setup(_, { expose }) {
+          const value = ref('hello')
+
+          const setValue = (fn = vi.fn((_value: string) => {
+            value.value = _value
+          }))
+
+          expose({
+            setValue,
+            value,
+          })
+
+          return () => h('div', null, [value.value])
+        },
+      })
+      customElements.define('my-el-expose', E)
+
+      container.innerHTML = `<my-el-expose></my-el-expose>`
+      const e = container.childNodes[0] as VueElement & {
+        value: string
+        setValue: MockedFunction<SetValue>
+      }
+      expect(e.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
+      expect(e.value).toBe('hello')
+      expect(e.setValue).toBe(fn!)
+      e.setValue('world')
+      expect(e.value).toBe('world')
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe(`<div>world</div>`)
+    })
+
+    test('warning when exposing an existing property', () => {
+      const E = defineCustomElement({
+        props: {
+          value: String,
+        },
+        setup(props, { expose }) {
+          expose({
+            value: 'hello',
+          })
+
+          return () => h('div', null, [props.value])
+        },
+      })
+      customElements.define('my-el-expose-two', E)
+
+      container.innerHTML = `<my-el-expose-two value="world"></my-el-expose-two>`
+
+      expect(
+        `[Vue warn]: Exposed property "value" already exists on custom element.`,
+      ).toHaveBeenWarned()
+    })
+  })
 })
index 7c4d8793ba12968386268ee074408c3088e9a7c5..a92248dd55db7e33d6c217cf7c107e496bb67245 100644 (file)
@@ -26,11 +26,13 @@ import {
   defineComponent,
   getCurrentInstance,
   nextTick,
+  unref,
   warn,
 } from '@vue/runtime-core'
 import {
   camelize,
   extend,
+  hasOwn,
   hyphenate,
   isArray,
   isPlainObject,
@@ -308,6 +310,9 @@ export class VueElement extends BaseClass {
 
       // initial render
       this._update()
+
+      // apply expose
+      this._applyExpose()
     }
 
     const asyncDef = (this._def as ComponentOptions).__asyncLoader
@@ -342,6 +347,22 @@ export class VueElement extends BaseClass {
     }
   }
 
+  private _applyExpose() {
+    const exposed = this._instance && this._instance.exposed
+    if (!exposed) return
+    for (const key in exposed) {
+      if (!hasOwn(this, key)) {
+        // exposed properties are readonly
+        Object.defineProperty(this, key, {
+          // unwrap ref to be consistent with public instance behavior
+          get: () => unref(exposed[key]),
+        })
+      } else if (__DEV__) {
+        warn(`Exposed property "${key}" already exists on custom element.`)
+      }
+    }
+  }
+
   protected _setAttr(key: string) {
     if (key.startsWith('data-v-')) return
     let value = this.hasAttribute(key) ? this.getAttribute(key) : undefined