diff --git a/packages/runtime-dom/__tests__/.customElement.spec.ts.swp b/packages/runtime-dom/__tests__/.customElement.spec.ts.swp new file mode 100644 index 00000000000..073ed14222b Binary files /dev/null and b/packages/runtime-dom/__tests__/.customElement.spec.ts.swp differ diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index 07ea091486e..0e86b33263d 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -474,6 +474,28 @@ describe('defineCustomElement', () => { '
1 is numbertrue is boolean
', ) }) + + test('prop overrides', async () => { + const EBase = defineCustomElement({ + props: { + value: Number, + }, + render() { + return h('span', this.value) + }, + }) + class E extends EBase { + set value(newValue: number) { + super.value = newValue + 1 + } + } + customElements.define('my-element-with-prop-overrides', E) + const el = document.createElement('my-element-with-prop-overrides') as any + container.appendChild(el) + el.value = 999 + await nextTick() + expect(el.shadowRoot.firstChild.innerHTML).toBe('1000') + }) }) describe('attrs', () => { diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index edf7c431353..8528c91a19b 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -176,11 +176,13 @@ export function defineCustomElement( if (isPlainObject(Comp)) extend(Comp, extraOptions) class VueCustomElement extends VueElement { static def = Comp + static asyncDef: any | null = null constructor(initialProps?: Record) { - super(Comp, initialProps, _createApp) + super(VueCustomElement.def, initialProps, _createApp) } } + _defineProps(VueCustomElement.prototype, Comp) return VueCustomElement } @@ -406,7 +408,16 @@ export class VueElement if (asyncDef) { this._pendingResolve = asyncDef().then((def: InnerComponentDef) => { def.configureApp = this._def.configureApp - resolve((this._def = def), true) + this._def = def + const ctor = this.constructor as any + if (!ctor.asyncDef) { + const proto = Object.getPrototypeOf(this) + _defineProps(proto, def) + ctor.asyncDef = def + } else if (ctor.asyncDef !== def) { + warn('async loader returned inconsistent value') + } + resolve(def, true) }) } else { resolve(this._def) @@ -431,7 +442,7 @@ export class VueElement const exposed = this._instance && this._instance.exposed if (!exposed) return for (const key in exposed) { - if (!hasOwn(this, key)) { + if (!hasOwn(this, key) && !hasOwn(Object.getPrototypeOf(this), key)) { // exposed properties are readonly Object.defineProperty(this, key, { // unwrap ref to be consistent with public instance behavior @@ -451,20 +462,9 @@ export class VueElement for (const key of Object.keys(this)) { if (key[0] !== '_' && declaredPropKeys.includes(key)) { this._setProp(key, this[key as keyof this]) + delete this[key as keyof this] } } - - // defining getter/setters on prototype - for (const key of declaredPropKeys.map(camelize)) { - Object.defineProperty(this, key, { - get() { - return this._getProp(key) - }, - set(val) { - this._setProp(key, val, true, true) - }, - }) - } } protected _setAttr(key: string): void { @@ -688,6 +688,28 @@ export class VueElement } } +function _defineProps(proto: object, def: InnerComponentDef) { + const { props } = def + const declaredPropKeys = isArray(props) ? props : Object.keys(props || {}) + // defining getter/setters on prototype + for (const key of declaredPropKeys.map(camelize)) { + Object.defineProperty(proto, key, { + get() { + return this._getProp(key) + }, + set(val) { + if (this._resolved) { + this._setProp(key, val, true, true) + } else { + // when pre-upgrade or connect, set values directly on the instance + Reflect.set({}, key, val, this) + } + }, + enumerable: true, + }) + } +} + export function useHost(caller?: string): VueElement | null { const instance = getCurrentInstance() const el = instance && (instance.ce as VueElement)