Skip to content

Commit

Permalink
fix(custom-element): fix parent, observer and exposed issues when rem…
Browse files Browse the repository at this point in the history
…ove element and append it back after fully unmounted (vuejs#12412)
  • Loading branch information
lejunyang committed Nov 16, 2024
1 parent 6eb29d3 commit 0e913dc
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 13 deletions.
37 changes: 37 additions & 0 deletions packages/runtime-dom/__tests__/customElement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,43 @@ describe('defineCustomElement', () => {
expect(e._instance).toBeTruthy()
expect(e.shadowRoot!.innerHTML).toBe('<div>hello</div>')
})

// #12412
test('remove element with child custom element and wait fully disconnected then insert', async () => {
const El = defineCustomElement({
props: {
msg: String,
},
setup(props, { expose }) {
expose({
text: () => props.msg,
})
provide('context', props)
const context = inject('context', {}) as typeof props
return () => context.msg || props.msg
},
})
customElements.define('my-el-remove-insert-expose', El)
container.innerHTML = `<div><my-el-remove-insert-expose msg="msg1"><my-el-remove-insert-expose></my-el-remove-insert-expose></my-el-remove-insert-expose></div>`
const parent = container.children[0].children[0] as VueElement & {
text: () => string
}
const child = parent.children[0] as VueElement
parent.remove()
await nextTick()
await nextTick() // wait two ticks for disconnect
expect('text' in parent).toBe(false)
container.appendChild(parent) // should not throw Error
await nextTick()
expect(parent.text()).toBe('msg1')
expect(parent.shadowRoot!.textContent).toBe('msg1')
expect(child.shadowRoot!.textContent).toBe('msg1')
parent.setAttribute('msg', 'msg2')
await nextTick()
expect(parent.shadowRoot!.textContent).toBe('msg2')
await nextTick()
expect(child.shadowRoot!.textContent).toBe('msg2')
})
})

describe('props', () => {
Expand Down
43 changes: 30 additions & 13 deletions packages/runtime-dom/src/apiCustomElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,9 @@ export class VueElement

if (!this._instance) {
if (this._resolved) {
this._setParent()
this._update()
// this element has been fully unmounted, should create observer again and re-mount
this._observe()
this._mount(this._def)
} else {
if (parent && parent._pendingResolve) {
this._pendingResolve = parent._pendingResolve.then(() => {
Expand Down Expand Up @@ -330,12 +331,31 @@ export class VueElement
}
// unmount
this._app && this._app.unmount()
if (this._instance) this._instance.ce = undefined
if (this._instance) {
const exposed = this._instance.exposed
if (exposed) {
for (const key in exposed) {
delete this[key as keyof this]
}
}
this._instance.ce = undefined
}
this._app = this._instance = null
}
})
}

private _observe() {
if (!this._ob) {
this._ob = new MutationObserver(mutations => {
for (const m of mutations) {
this._setAttr(m.attributeName!)
}
})
}
this._ob.observe(this, { attributes: true })
}

/**
* resolve inner component definition (handle possible async component)
*/
Expand All @@ -350,13 +370,7 @@ export class VueElement
}

// watch future attr changes
this._ob = new MutationObserver(mutations => {
for (const m of mutations) {
this._setAttr(m.attributeName!)
}
})

this._ob.observe(this, { attributes: true })
this._observe()

const resolve = (def: InnerComponentDef, isAsync = false) => {
this._resolved = true
Expand Down Expand Up @@ -430,11 +444,14 @@ export class VueElement
if (!hasOwn(this, key)) {
// exposed properties are readonly
Object.defineProperty(this, key, {
configurable: true, // should be configurable to allow deleting when disconnected
// 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.`)
} else {
delete exposed[key] // delete it from exposed in case of deleting wrong exposed key when disconnected
if (__DEV__)
warn(`Exposed property "${key}" already exists on custom element.`)
}
}
}
Expand Down Expand Up @@ -514,7 +531,7 @@ export class VueElement
} else if (!val) {
this.removeAttribute(hyphenate(key))
}
ob && ob.observe(this, { attributes: true })
this._observe()
}
}
}
Expand Down

0 comments on commit 0e913dc

Please sign in to comment.