From 8e49853acf9139e02b6feea87e4874cf43a264a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E9=9B=BE=E4=B8=89=E8=AF=AD?= <32354856+baiwusanyu-c@users.noreply.github.com> Date: Wed, 22 May 2024 00:14:02 +0800 Subject: [PATCH] fix(custom-element): disconnect MutationObserver in nextTick in case that custom elements are moved (#10613) Closes #10610 --- .../__tests__/customElement.spec.ts | 49 +++++++++++++++++++ packages/runtime-dom/src/apiCustomElement.ts | 8 +-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index fb746f72c4a..a8bdec77512 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -1,6 +1,7 @@ import { type Ref, type VueElement, + createApp, defineAsyncComponent, defineComponent, defineCustomElement, @@ -60,6 +61,54 @@ describe('defineCustomElement', () => { expect(e.shadowRoot!.innerHTML).toBe('') }) + // #10610 + test('When elements move, avoid prematurely disconnecting MutationObserver', async () => { + const CustomInput = defineCustomElement({ + props: ['value'], + emits: ['update'], + setup(props, { emit }) { + return () => + h('input', { + type: 'number', + value: props.value, + onInput: (e: InputEvent) => { + const num = (e.target! as HTMLInputElement).valueAsNumber + emit('update', Number.isNaN(num) ? null : num) + }, + }) + }, + }) + customElements.define('my-el-input', CustomInput) + const num = ref('12') + const containerComp = defineComponent({ + setup() { + return () => { + return h('div', [ + h('my-el-input', { + value: num.value, + onUpdate: ($event: CustomEvent) => { + num.value = $event.detail[0] + }, + }), + h('div', { id: 'move' }), + ]) + } + }, + }) + const app = createApp(containerComp) + app.mount(container) + const myInputEl = container.querySelector('my-el-input')! + const inputEl = myInputEl.shadowRoot!.querySelector('input')! + await nextTick() + expect(inputEl.value).toBe('12') + const moveEl = container.querySelector('#move')! + moveEl.append(myInputEl) + await nextTick() + myInputEl.removeAttribute('value') + await nextTick() + expect(inputEl.value).toBe('') + }) + test('should not unmount on move', async () => { container.innerHTML = `
` const e = container.childNodes[0].childNodes[0] as VueElement diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 01ce2bad464..93823027707 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -215,12 +215,12 @@ export class VueElement extends BaseClass { disconnectedCallback() { this._connected = false - if (this._ob) { - this._ob.disconnect() - this._ob = null - } nextTick(() => { if (!this._connected) { + if (this._ob) { + this._ob.disconnect() + this._ob = null + } render(null, this.shadowRoot!) this._instance = null }