From c4a88cdd0dfed3ef46a8aa9be448c01781fdc4f0 Mon Sep 17 00:00:00 2001 From: Alex Snezhko Date: Wed, 24 Sep 2025 02:42:11 -0700 Subject: [PATCH] fix(custom-element): set prop runs pending mutations before disconnect (#13897) close #13315 --- .../__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 9db0de9cf..8b3b440f2 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 d99a9341b..6d6dccc6e 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -346,6 +346,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) */ @@ -360,11 +366,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 }) @@ -514,7 +516,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') { -- 2.47.3