Skip to content

Commit 9f51f13

Browse files
committedMay 8, 2025·
wip: save
1 parent 1a5ea55 commit 9f51f13

File tree

4 files changed

+113
-28
lines changed

4 files changed

+113
-28
lines changed
 

‎packages/runtime-core/src/helpers/renderSlot.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ export function renderSlot(
9696
if (slot && (slot as ContextualRenderFn)._c) {
9797
;(slot as ContextualRenderFn)._d = true
9898
}
99-
rendered.slotName = name
10099
return rendered
101100
}
102101

‎packages/runtime-core/src/renderer.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -936,7 +936,7 @@ function baseCreateRenderer(
936936
}
937937

938938
if (el._isVueCE && el._def.shadowRoot === false) {
939-
el._updateSlots(n2.children)
939+
el._updateSlots(n1, n2)
940940
}
941941
}
942942

@@ -966,7 +966,9 @@ function baseCreateRenderer(
966966
!isSameVNodeType(oldVNode, newVNode) ||
967967
// - In the case of a component, it could contain anything.
968968
oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
969-
? hostParentNode(oldVNode.el)!
969+
? oldVNode.el._parentNode && !oldVNode.el.isConnected
970+
? oldVNode.el._parentNode
971+
: hostParentNode(oldVNode.el)!
970972
: // In other cases, the parent container is not actually used so we
971973
// just pass the block element here to avoid a DOM parentNode call.
972974
fallbackContainer

‎packages/runtime-core/src/vnode.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,6 @@ export interface VNode<
253253
* @internal custom element interception hook
254254
*/
255255
ce?: (instance: ComponentInternalInstance) => void
256-
/**
257-
* @internal
258-
*/
259-
slotName?: string
260256
}
261257

262258
// Since v-if and v-for are the two possible ways node structure can dynamically
@@ -719,7 +715,6 @@ export function cloneVNode<T, U>(
719715
anchor: vnode.anchor,
720716
ctx: vnode.ctx,
721717
ce: vnode.ce,
722-
slotName: vnode.slotName,
723718
}
724719

725720
// if the vnode will be replaced by the cloned one, it is necessary

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

Lines changed: 109 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ export class VueElement
245245
private _childStyles?: Map<string, HTMLStyleElement[]>
246246
private _ob?: MutationObserver | null = null
247247
private _slots?: Record<string, Node[]>
248+
private _slotFallbacks?: Record<string, Node[]>
249+
private _slotAnchors?: Map<string, Node>
248250

249251
constructor(
250252
/**
@@ -529,8 +531,11 @@ export class VueElement
529531
private _createVNode(): VNode<any, any> {
530532
const baseProps: VNodeProps = {}
531533
if (!this.shadowRoot) {
532-
baseProps.onVnodeMounted = baseProps.onVnodeUpdated =
533-
this._renderSlots.bind(this)
534+
baseProps.onVnodeMounted = () => {
535+
this._captureSlotFallbacks()
536+
this._renderSlots()
537+
}
538+
baseProps.onVnodeUpdated = this._renderSlots.bind(this)
534539
}
535540
const vnode = createVNode(this._def, extend(baseProps, this._props))
536541
if (!this._instance) {
@@ -617,14 +622,19 @@ export class VueElement
617622
/**
618623
* Only called when shadowRoot is false
619624
*/
620-
private _parseSlots() {
625+
private _parseSlots(remove: boolean = true) {
621626
const slots: VueElement['_slots'] = (this._slots = {})
622-
let n
623-
while ((n = this.firstChild)) {
627+
let n = this.firstChild
628+
while (n) {
624629
const slotName =
625630
(n.nodeType === 1 && (n as Element).getAttribute('slot')) || 'default'
626631
;(slots[slotName] || (slots[slotName] = [])).push(n)
627-
this.removeChild(n)
632+
const next = n.nextSibling
633+
// store the parentNode reference since node will be removed
634+
// but it is needed during patching
635+
;(n as any)._parentNode = n.parentNode
636+
if (remove) this.removeChild(n)
637+
n = next
628638
}
629639
}
630640

@@ -634,11 +644,18 @@ export class VueElement
634644
private _renderSlots() {
635645
const outlets = (this._teleportTarget || this).querySelectorAll('slot')
636646
const scopeId = this._instance!.type.__scopeId
647+
this._slotAnchors = new Map()
637648
for (let i = 0; i < outlets.length; i++) {
638649
const o = outlets[i] as HTMLSlotElement
639650
const slotName = o.getAttribute('name') || 'default'
640651
const content = this._slots![slotName]
641652
const parent = o.parentNode!
653+
654+
// insert an anchor to facilitate updates
655+
const anchor = document.createTextNode('')
656+
this._slotAnchors.set(slotName, anchor)
657+
parent.insertBefore(anchor, o)
658+
642659
if (content) {
643660
for (const n of content) {
644661
// for :slotted css
@@ -651,23 +668,91 @@ export class VueElement
651668
;(child as Element).setAttribute(id, '')
652669
}
653670
}
654-
parent.insertBefore(n, o)
671+
parent.insertBefore(n, anchor)
672+
}
673+
} else if (this._slotFallbacks) {
674+
const nodes = this._slotFallbacks[slotName]
675+
if (nodes) {
676+
for (const n of nodes) {
677+
parent.insertBefore(n, anchor)
678+
}
655679
}
656-
} else {
657-
while (o.firstChild) parent.insertBefore(o.firstChild, o)
658680
}
659681
parent.removeChild(o)
660682
}
661683
}
662684

663685
/**
664-
* @internal
686+
* Only called when shadowRoot is false
665687
*/
666-
_updateSlots(children: VNode[]): void {
667-
children.forEach(child => {
668-
// slot children are always Fragments
669-
this._slots![child.slotName!] = collectFragmentElements(child)
670-
})
688+
_updateSlots(n1: VNode, n2: VNode): void {
689+
// replace v-if nodes
690+
const prevNodes = collectNodes(n1.children as VNodeArrayChildren)
691+
const newNodes = collectNodes(n2.children as VNodeArrayChildren)
692+
for (let i = 0; i < prevNodes.length; i++) {
693+
const prevNode = prevNodes[i]
694+
const newNode = newNodes[i]
695+
if (isComment(prevNode, 'v-if') || isComment(newNode, 'v-if')) {
696+
Object.keys(this._slots!).forEach(name => {
697+
const slotNodes = this._slots![name]
698+
if (slotNodes) {
699+
for (const node of slotNodes) {
700+
if (node === prevNode) {
701+
this._slots![name][i] = newNode
702+
break
703+
}
704+
}
705+
}
706+
})
707+
}
708+
}
709+
710+
// switch between fallback and provided content
711+
if (this._slotFallbacks) {
712+
const oldSlotNames = Object.keys(this._slots!)
713+
// re-parse slots
714+
this._parseSlots(false)
715+
const newSlotNames = Object.keys(this._slots!)
716+
const allSlotNames = new Set([...oldSlotNames, ...newSlotNames])
717+
allSlotNames.forEach(name => {
718+
const fallbackNodes = this._slotFallbacks![name]
719+
if (fallbackNodes) {
720+
// render fallback nodes for removed slots
721+
if (!newSlotNames.includes(name)) {
722+
const anchor = this._slotAnchors!.get(name)!
723+
fallbackNodes.forEach(fallbackNode =>
724+
this.insertBefore(fallbackNode, anchor),
725+
)
726+
}
727+
728+
// remove fallback nodes for added slots
729+
if (!oldSlotNames.includes(name)) {
730+
fallbackNodes.forEach(fallbackNode =>
731+
this.removeChild(fallbackNode),
732+
)
733+
}
734+
}
735+
})
736+
}
737+
}
738+
739+
/**
740+
* Only called when shadowRoot is false
741+
*/
742+
private _captureSlotFallbacks() {
743+
const outlets = (this._teleportTarget || this).querySelectorAll('slot')
744+
for (let i = 0; i < outlets.length; i++) {
745+
const slotElement = outlets[i] as HTMLSlotElement
746+
const slotName = slotElement.getAttribute('name') || 'default'
747+
const fallbackNodes: Node[] = []
748+
while (slotElement.firstChild) {
749+
fallbackNodes.push(slotElement.removeChild(slotElement.firstChild))
750+
}
751+
if (fallbackNodes.length) {
752+
;(this._slotFallbacks || (this._slotFallbacks = {}))[slotName] =
753+
fallbackNodes
754+
}
755+
}
671756
}
672757

673758
/**
@@ -724,26 +809,30 @@ export function useShadowRoot(): ShadowRoot | null {
724809
return el && el.shadowRoot
725810
}
726811

727-
function collectFragmentElements(child: VNode): Node[] {
812+
function collectFragmentNodes(child: VNode): Node[] {
728813
return [
729814
child.el as Node,
730-
...collectElements(child.children as VNodeArrayChildren),
815+
...collectNodes(child.children as VNodeArrayChildren),
731816
child.anchor as Node,
732817
]
733818
}
734819

735-
function collectElements(children: VNodeArrayChildren): Node[] {
820+
function collectNodes(children: VNodeArrayChildren): Node[] {
736821
const nodes: Node[] = []
737822
for (const child of children) {
738823
if (isArray(child)) {
739-
nodes.push(...collectElements(child))
824+
nodes.push(...collectNodes(child))
740825
} else if (isVNode(child)) {
741826
if (child.type === Fragment) {
742-
nodes.push(...collectFragmentElements(child))
827+
nodes.push(...collectFragmentNodes(child))
743828
} else if (child.el) {
744829
nodes.push(child.el as Node)
745830
}
746831
}
747832
}
748833
return nodes
749834
}
835+
836+
function isComment(node: Node, data: string): node is Comment {
837+
return node.nodeType === 8 && (node as Comment).data === data
838+
}

0 commit comments

Comments
 (0)
Please sign in to comment.