Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 324aacc

Browse files
committed
fix(custom-element): keep nested fallback blocks live in shadowRoot false custom elements
1 parent e972bcc commit 324aacc

5 files changed

Lines changed: 99 additions & 37 deletions

File tree

‎packages/runtime-dom/src/apiCustomElement.ts‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,12 @@ export abstract class VueElementBase<
265265
protected abstract _mount(def: Def): void
266266
protected abstract _update(): void
267267
protected abstract _unmount(): void
268-
protected abstract _updateSlotNodes(slot: Map<Node, Node[]>): void
268+
// `usedFallback` preserves whether the outlet rendered native slotted
269+
// content or its own fallback DOM so implementations can keep the right
270+
// ownership model when syncing their block trees.
271+
protected abstract _updateSlotNodes(
272+
slot: Map<Node, { nodes: Node[]; usedFallback: boolean }>,
273+
): void
269274

270275
constructor(
271276
/**
@@ -693,7 +698,13 @@ export abstract class VueElementBase<
693698
protected _renderSlots(): void {
694699
const outlets = this._getSlots()
695700
const scopeId = this._instance!.type.__scopeId
696-
const slotReplacements: Map<Node, Node[]> = new Map()
701+
// Record both the final DOM nodes and whether they came from fallback.
702+
// The nodes alone are not enough for runtimes that need to distinguish a
703+
// plain DOM replacement from a live fallback owner.
704+
const slotReplacements: Map<
705+
Node,
706+
{ nodes: Node[]; usedFallback: boolean }
707+
> = new Map()
697708

698709
for (let i = 0; i < outlets.length; i++) {
699710
const o = outlets[i] as HTMLSlotElement
@@ -725,7 +736,10 @@ export abstract class VueElementBase<
725736
}
726737
}
727738
parent.removeChild(o)
728-
slotReplacements.set(o, replacementNodes)
739+
slotReplacements.set(o, {
740+
nodes: replacementNodes,
741+
usedFallback: !content,
742+
})
729743
}
730744

731745
this._updateSlotNodes(slotReplacements)
@@ -866,7 +880,9 @@ export class VueElement extends VueElementBase<
866880
/**
867881
* Only called when shadowRoot is false
868882
*/
869-
protected _updateSlotNodes(replacements: Map<Node, Node[]>): void {
883+
protected _updateSlotNodes(
884+
replacements: Map<Node, { nodes: Node[]; usedFallback: boolean }>,
885+
): void {
870886
// do nothing
871887
}
872888

‎packages/runtime-vapor/__tests__/customElement.spec.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1721,6 +1721,52 @@ describe('defineVaporCustomElement', () => {
17211721
)
17221722
})
17231723

1724+
test('should unmount nested slot fallback rendered from outer fallback after updates', async () => {
1725+
const showNamedFallback = ref(true)
1726+
const NestedFallback = defineVaporCustomElement(
1727+
{
1728+
setup() {
1729+
return createSlot('default', null, () =>
1730+
createSlot('named', null, () =>
1731+
createIf(
1732+
() => showNamedFallback.value,
1733+
() => template('<span>named fallback</span>')(),
1734+
),
1735+
),
1736+
)
1737+
},
1738+
},
1739+
{ shadowRoot: false },
1740+
)
1741+
customElements.define(
1742+
'my-el-shadowroot-false-nested-fallback-unmount',
1743+
NestedFallback,
1744+
)
1745+
1746+
container.innerHTML =
1747+
`<my-el-shadowroot-false-nested-fallback-unmount>` +
1748+
`</my-el-shadowroot-false-nested-fallback-unmount>`
1749+
const e = container.childNodes[0] as VaporElement
1750+
1751+
expect(e.innerHTML).toBe(
1752+
`<span>named fallback</span><!--if--><!--slot--><!--slot-->`,
1753+
)
1754+
1755+
showNamedFallback.value = false
1756+
await nextTick()
1757+
expect(e.innerHTML).toBe(`<!--if--><!--slot--><!--slot-->`)
1758+
showNamedFallback.value = true
1759+
await nextTick()
1760+
expect(e.innerHTML).toBe(
1761+
`<span>named fallback</span><!--if--><!--slot--><!--slot-->`,
1762+
)
1763+
1764+
container.removeChild(e)
1765+
await nextTick()
1766+
expect(e.innerHTML).toBe(``)
1767+
expect(e._instance).toBe(null)
1768+
})
1769+
17241770
test('render nested customElement w/ shadowRoot false', async () => {
17251771
const calls: string[] = []
17261772

‎packages/runtime-vapor/src/apiDefineCustomElement.ts‎

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import type {
2929
VaporRenderResult,
3030
} from './apiDefineComponent'
3131
import type { StaticSlots } from './componentSlots'
32-
import { isFragment } from './fragment'
32+
import { SlotFragment, isFragment } from './fragment'
3333

3434
export type VaporElementConstructor<P = {}> = {
3535
new (initialProps?: Record<string, any>): VaporElement & P
@@ -285,7 +285,9 @@ export class VaporElement extends VueElementBase<
285285
/**
286286
* Only called when shadowRoot is false
287287
*/
288-
protected _updateSlotNodes(replacements: Map<Node, Node[]>): void {
288+
protected _updateSlotNodes(
289+
replacements: Map<Node, { nodes: Node[]; usedFallback: boolean }>,
290+
): void {
289291
this._updateFragmentNodes(
290292
(this._instance! as VaporComponentInstance).block,
291293
replacements,
@@ -298,45 +300,34 @@ export class VaporElement extends VueElementBase<
298300
*/
299301
private _updateFragmentNodes(
300302
block: Block,
301-
replacements: Map<Node, Node[]>,
303+
replacements: Map<Node, { nodes: Node[]; usedFallback: boolean }>,
302304
): void {
303-
const appendReplacementNodes = (
304-
slot: HTMLSlotElement,
305-
target: Block[],
306-
): void => {
307-
const replacement = replacements.get(slot)
308-
if (!replacement) return
309-
for (const node of replacement) {
310-
if (node instanceof HTMLSlotElement) {
311-
appendReplacementNodes(node, target)
312-
} else {
313-
target.push(node)
314-
}
315-
}
316-
}
317-
318305
if (Array.isArray(block)) {
319306
block.forEach(item => this._updateFragmentNodes(item, replacements))
320307
return
321308
}
322309

323310
if (!isFragment(block)) return
324311
const { nodes } = block
325-
if (Array.isArray(nodes)) {
326-
const newNodes: Block[] = []
327-
for (const node of nodes) {
328-
if (node instanceof HTMLSlotElement) {
329-
appendReplacementNodes(node, newNodes)
330-
} else {
331-
this._updateFragmentNodes(node, replacements)
332-
newNodes.push(node)
333-
}
312+
if (nodes instanceof HTMLSlotElement) {
313+
const replacement = replacements.get(nodes)
314+
if (!replacement) return
315+
316+
// Slotted content can be represented as plain nodes, but fallback must
317+
// stay as its live block so nested updates and unmounting keep using the
318+
// current owner rather than a stale DOM snapshot.
319+
if (
320+
replacement.usedFallback &&
321+
block instanceof SlotFragment &&
322+
block.customElementFallback
323+
) {
324+
this._updateFragmentNodes(block.customElementFallback, replacements)
325+
block.nodes = block.customElementFallback
326+
} else {
327+
block.nodes = replacement.nodes
334328
}
335-
block.nodes = newNodes
336-
} else if (nodes instanceof HTMLSlotElement) {
337-
const newNodes: Block[] = []
338-
appendReplacementNodes(nodes, newNodes)
339-
block.nodes = newNodes
329+
} else if (Array.isArray(nodes)) {
330+
nodes.forEach(item => this._updateFragmentNodes(item, replacements))
340331
} else {
341332
this._updateFragmentNodes(nodes, replacements)
342333
}

‎packages/runtime-vapor/src/componentSlots.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,12 @@ export function createSlot(
244244
})
245245
if (fallback) {
246246
withOwnedSlotBoundary(slotFragment.parentSlotBoundary, () => {
247-
insert(fallback(), el)
247+
const fallbackBlock = fallback()
248+
// Keep the live fallback owner on the SlotFragment itself. The
249+
// native slot outlet is temporary and gets removed by CE slot
250+
// replacement, but the fragment remains Vapor's long-lived owner.
251+
slotFragment.customElementFallback = fallbackBlock
252+
insert(fallbackBlock, el)
248253
})
249254
}
250255
fragment.nodes = el

‎packages/runtime-vapor/src/fragment.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,10 @@ function isReusableDynamicFragmentAnchor(
11331133
export class SlotFragment extends DynamicFragment {
11341134
forwarded = false
11351135
parentSlotBoundary: SlotBoundaryContext | null = getCurrentSlotBoundary()
1136+
// Custom elements with `shadowRoot: false` replace their native slot outlet
1137+
// after mount. Keep the live fallback owner on the fragment so CE slot sync
1138+
// can preserve block ownership after the outlet node is gone.
1139+
customElementFallback?: Block
11361140
private localFallback?: BlockFn
11371141
private isUpdatingSlot = false
11381142
private readonly controller: SlotFallbackController

0 commit comments

Comments
 (0)