diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index 1762f0cec..91596b67f 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -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 + + 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 = `` + const e = container.childNodes[0] as VueElement & { + value: string + setValue: MockedFunction + } + expect(e.shadowRoot!.innerHTML).toBe(`
hello
`) + 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(`
world
`) + }) + + 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 = `` + + expect( + `[Vue warn]: Exposed property "value" already exists on custom element.`, + ).toHaveBeenWarned() + }) + }) }) diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 7c4d8793b..a92248dd5 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -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