From 2b828246b5fdf6ce4e81c8d964d5d4e78ca791ab Mon Sep 17 00:00:00 2001 From: Alex Snezhko Date: Mon, 15 Sep 2025 21:56:49 -0700 Subject: [PATCH] fix(custom-element): set prop runs pending mutations before disconnect --- .../__tests__/customElement.spec.ts | 25 +++++++++++++++++++ packages/runtime-dom/src/apiCustomElement.ts | 17 ++++++++----- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index cb09cf4d9e7..0052857b887 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -223,6 +223,31 @@ describe('defineCustomElement', () => { expect(e.getAttribute('baz-qux')).toBe('four') }) + test('props via attributes and properties changed together', async () => { + const e = new E() + e.foo = 'foo1' + e.bar = { x: 'bar1' } + container.appendChild(e) + await nextTick() + expect(e.shadowRoot!.innerHTML).toBe('
foo1
bar1
') + + // change attr then property + e.setAttribute('foo', 'foo2') + e.bar = { x: 'bar2' } + await nextTick() + expect(e.shadowRoot!.innerHTML).toBe('
foo2
bar2
') + expect(e.getAttribute('foo')).toBe('foo2') + expect(e.hasAttribute('bar')).toBe(false) + + // change prop then attr + e.bar = { x: 'bar3' } + e.setAttribute('foo', 'foo3') + await nextTick() + expect(e.shadowRoot!.innerHTML).toBe('
foo3
bar3
') + expect(e.getAttribute('foo')).toBe('foo3') + expect(e.hasAttribute('bar')).toBe(false) + }) + test('props via hyphen property', async () => { const Comp = defineCustomElement({ props: { diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index d1f10777f73..2bbd6617207 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -342,6 +342,12 @@ export class VueElement }) } + private _processMutations(mutations: MutationRecord[]) { + for (const m of mutations) { + this._setAttr(m.attributeName!) + } + } + /** * resolve inner component definition (handle possible async component) */ @@ -356,11 +362,7 @@ export class VueElement } // watch future attr changes - this._ob = new MutationObserver(mutations => { - for (const m of mutations) { - this._setAttr(m.attributeName!) - } - }) + this._ob = new MutationObserver(this._processMutations.bind(this)) this._ob.observe(this, { attributes: true }) @@ -510,7 +512,10 @@ export class VueElement // reflect if (shouldReflect) { const ob = this._ob - ob && ob.disconnect() + if (ob) { + this._processMutations(ob.takeRecords()) + ob.disconnect() + } if (val === true) { this.setAttribute(hyphenate(key), '') } else if (typeof val === 'string' || typeof val === 'number') {