]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
chore: update
authordaiwei <daiwei521@126.com>
Mon, 12 May 2025 00:10:39 +0000 (08:10 +0800)
committerdaiwei <daiwei521@126.com>
Mon, 12 May 2025 08:26:14 +0000 (16:26 +0800)
packages/runtime-core/src/hydration.ts
packages/runtime-core/src/index.ts
packages/runtime-dom/__tests__/customElement.spec.ts
packages/runtime-dom/src/apiCustomElement.ts

index a94ff3568107e6c20d383eeb4e5e839012825ee2..1372c126e800cc07d7103827b61f24fa4df5f649 100644 (file)
@@ -772,13 +772,6 @@ export function createHydrationFunctions(
     }
   }
 
-  const isTemplateNode = (node: Node): node is HTMLTemplateElement => {
-    return (
-      node.nodeType === DOMNodeTypes.ELEMENT &&
-      (node as Element).tagName === 'TEMPLATE'
-    )
-  }
-
   return [hydrate, hydrateNode]
 }
 
@@ -993,3 +986,10 @@ function isMismatchAllowed(
     return allowedAttr.split(',').includes(MismatchTypeString[allowedType])
   }
 }
+
+export const isTemplateNode = (node: Node): node is HTMLTemplateElement => {
+  return (
+    node.nodeType === DOMNodeTypes.ELEMENT &&
+    (node as Element).tagName === 'TEMPLATE'
+  )
+}
index 9910f82102b06daa9f132f6e15dd86d908dccb66..da171b0d0574ff104d8192046bad54683f3aac4a 100644 (file)
@@ -382,6 +382,7 @@ export {
   normalizeClass,
   normalizeStyle,
 } from '@vue/shared'
+export { isTemplateNode } from './hydration'
 
 // For test-utils
 export { transformVNodeArgs } from './vnode'
index 0fe0b2275c04a3d464d007b4b6bf02c5d3c5a208..e7c5bcd47f84e215e608d7e1ce3f9f9ae8a753fa 100644 (file)
@@ -1029,7 +1029,10 @@ describe('defineCustomElement', () => {
       toggle.value = false
       await nextTick()
       expect(e.innerHTML).toBe(
-        `<span>default</span>text` + `<!---->` + `<div>fallback</div>`,
+        `<span>default</span>text` +
+          `<template name="named"></template>` +
+          `<!---->` +
+          `<div>fallback</div>`,
       )
     })
 
@@ -1212,7 +1215,7 @@ describe('defineCustomElement', () => {
       app.mount(container)
       expect(container.innerHTML).toBe(
         `<ce-shadow-root-false-optimized data-v-app="">` +
-          `<div>false</div><!--v-if--><!--v-if-->` +
+          `<!--v-if--><template name="default"></template>` +
           `</ce-shadow-root-false-optimized>`,
       )
 
@@ -1228,7 +1231,7 @@ describe('defineCustomElement', () => {
       await nextTick()
       expect(container.innerHTML).toBe(
         `<ce-shadow-root-false-optimized data-v-app="">` +
-          `<div>false</div><!--v-if--><!--v-if-->` +
+          `<!--v-if--><template name="default"></template>` +
           `</ce-shadow-root-false-optimized>`,
       )
 
@@ -1236,7 +1239,7 @@ describe('defineCustomElement', () => {
       await nextTick()
       expect(container.innerHTML).toBe(
         `<ce-shadow-root-false-optimized data-v-app="" is-shown="">` +
-          `<!--v-if--><div><div>true</div><div>hi</div></div>` +
+          `<div><div>true</div><div>hi</div></div>` +
           `</ce-shadow-root-false-optimized>`,
       )
     })
@@ -1299,7 +1302,7 @@ describe('defineCustomElement', () => {
       app.mount(container)
       expect(container.innerHTML).toBe(
         `<ce-shadow-root-false data-v-app="">` +
-          `<div>false</div><!--v-if--><!--v-if-->` +
+          `<!--v-if--><template name="default"></template>` +
           `</ce-shadow-root-false>`,
       )
 
@@ -1315,7 +1318,7 @@ describe('defineCustomElement', () => {
       await nextTick()
       expect(container.innerHTML).toBe(
         `<ce-shadow-root-false data-v-app="">` +
-          `<div>false</div><!--v-if--><!--v-if-->` +
+          `<!--v-if--><template name="default"></template>` +
           `</ce-shadow-root-false>`,
       )
 
@@ -1323,7 +1326,7 @@ describe('defineCustomElement', () => {
       await nextTick()
       expect(container.innerHTML).toBe(
         `<ce-shadow-root-false data-v-app="" is-shown="">` +
-          `<!--v-if--><div><div>true</div><div>hi</div></div>` +
+          `<div><div>true</div><div>hi</div></div>` +
           `</ce-shadow-root-false>`,
       )
     })
@@ -1397,7 +1400,7 @@ describe('defineCustomElement', () => {
       app.mount(container)
       expect(container.innerHTML).toBe(
         `<ce-with-fallback-shadow-root-false-optimized data-v-app="">` +
-          `<!--v-if-->fallback` +
+          `fallback<template name="default"></template>` +
           `</ce-with-fallback-shadow-root-false-optimized>`,
       )
 
@@ -1474,7 +1477,7 @@ describe('defineCustomElement', () => {
       app.mount(container)
       expect(container.innerHTML).toBe(
         `<ce-with-fallback-shadow-root-false data-v-app="">` +
-          `<!--v-if-->fallback` +
+          `fallback<template name="default"></template>` +
           `</ce-with-fallback-shadow-root-false>`,
       )
 
@@ -1486,13 +1489,13 @@ describe('defineCustomElement', () => {
           `</ce-with-fallback-shadow-root-false>`,
       )
 
-      isShown.value = false
-      await nextTick()
-      expect(container.innerHTML).toBe(
-        `<ce-with-fallback-shadow-root-false data-v-app="">` +
-          `<!--v-if-->fallback` +
-          `</ce-with-fallback-shadow-root-false>`,
-      )
+      // isShown.value = false
+      // await nextTick()
+      // expect(container.innerHTML).toBe(
+      //   `<ce-with-fallback-shadow-root-false data-v-app="">` +
+      //     `<!--v-if-->fallback` +
+      //     `</ce-with-fallback-shadow-root-false>`,
+      // )
     })
   })
 
index 0d9c1754e809cb6f8b5ab6c340733237dc176191..d3233935be95a1985e19ecec48ea2d37bd057c0d 100644 (file)
@@ -30,6 +30,7 @@ import {
   createVNode,
   defineComponent,
   getCurrentInstance,
+  isTemplateNode,
   isVNode,
   nextTick,
   unref,
@@ -244,7 +245,7 @@ export class VueElement
    */
   private _childStyles?: Map<string, HTMLStyleElement[]>
   private _ob?: MutationObserver | null = null
-  private _slots?: Record<string, Node[]>
+  private _slots?: Record<string, (Node & { $parentNode?: Node })[]>
   private _slotFallbacks?: Record<string, Node[]>
   private _slotAnchors?: Map<string, Node>
   private _slotNames: Set<string> | undefined
@@ -627,19 +628,44 @@ export class VueElement
    * Only called when shadowRoot is false
    */
   private _parseSlots(remove: boolean = true) {
-    const slots: VueElement['_slots'] = (this._slots = {})
+    if (!this._slotNames) this._slotNames = new Set()
+    else this._slotNames.clear()
+    this._slots = {}
+
     let n = this.firstChild
     while (n) {
+      const next = n.nextSibling
+      if (isTemplateNode(n)) {
+        this.processTemplateChildren(n, remove)
+        this.removeChild(n)
+      } else {
+        const slotName =
+          (n.nodeType === 1 && (n as Element).getAttribute('slot')) || 'default'
+        this.addToSlot(slotName, n, remove)
+      }
+
+      n = next
+    }
+  }
+
+  private processTemplateChildren(template: Node, remove: boolean) {
+    let n = template.firstChild
+    while (n) {
+      const next = n.nextSibling
       const slotName =
         (n.nodeType === 1 && (n as Element).getAttribute('slot')) || 'default'
-      ;(slots[slotName] || (slots[slotName] = [])).push(n)
-      ;(this._slotNames || (this._slotNames = new Set())).add(slotName)
-      const next = n.nextSibling
-      if (remove) this.removeChild(n)
+      this.addToSlot(slotName, n, remove)
+      if (remove) template.removeChild(n)
       n = next
     }
   }
 
+  private addToSlot(slotName: string, node: Node, remove: boolean) {
+    ;(this._slots![slotName] || (this._slots![slotName] = [])).push(node)
+    this._slotNames!.add(slotName)
+    if (remove) this.removeChild(node)
+  }
+
   /**
    * Only called when shadowRoot is false
    */
@@ -664,7 +690,12 @@ export class VueElement
       parent.insertBefore(anchor, o)
 
       if (content) {
+        const parentNode = content[0].parentNode
         insertSlottedContent(content, scopeId, parent, anchor)
+        // remove empty template container
+        if (parentNode && isTemplateNode(parentNode)) {
+          this.removeChild(parentNode)
+        }
       } else if (this._slotFallbacks) {
         const nodes = this._slotFallbacks[slotName]
         if (nodes) {
@@ -676,29 +707,32 @@ export class VueElement
       parent.removeChild(o)
     }
 
-    // ensure default slot content is rendered if provided
-    if (!processedSlots.has('default')) {
-      let content = this._slots!['default']
-      if (content) {
+    // create template for unprocessed slots and insert their content
+    // this prevents errors during full diff when anchors are not in the DOM tree
+    for (const slotName of this._slotNames!) {
+      if (processedSlots.has(slotName)) continue
+
+      const content = this._slots![slotName]
+      if (content && !content[0].isConnected) {
         let anchor
-        // if the default slot is not the first one, insert it behind the previous slot
         if (this._slotAnchors) {
           const slotNames = Array.from(this._slotNames!)
-          const defaultSlotIndex = slotNames.indexOf('default')
-          if (defaultSlotIndex > 0) {
+          const slotIndex = slotNames.indexOf(slotName)
+          if (slotIndex > 0) {
             const prevSlotAnchor = this._slotAnchors.get(
-              slotNames[defaultSlotIndex - 1],
+              slotNames[slotIndex - 1],
             )
             if (prevSlotAnchor) anchor = prevSlotAnchor.nextSibling
           }
         }
 
-        insertSlottedContent(
-          content,
-          scopeId,
-          this._root,
-          anchor || this.firstChild,
-        )
+        const container = document.createElement('template')
+        container.setAttribute('name', slotName)
+        for (const n of content) {
+          n.$parentNode = container
+          container.insertBefore(n, null)
+        }
+        this.insertBefore(container, anchor || null)
       }
     }
   }
@@ -720,7 +754,10 @@ export class VueElement
         Object.entries(this._slots!).forEach(([_, nodes]) => {
           const nodeIndex = nodes.indexOf(prevNode)
           if (nodeIndex > -1) {
+            const oldNode = nodes[nodeIndex]
+            const parentNode = (newNode.$parentNode = oldNode.$parentNode)!
             nodes[nodeIndex] = newNode
+            if (oldNode.isConnected) parentNode.replaceChild(newNode, oldNode)
           }
         })
       }
@@ -728,10 +765,10 @@ export class VueElement
 
     // switch between fallback and provided content
     if (this._slotFallbacks) {
-      const oldSlotNames = Object.keys(this._slots!)
+      const oldSlotNames = Array.from(this._slotNames!)
       // re-parse slots
       this._parseSlots(false)
-      const newSlotNames = Object.keys(this._slots!)
+      const newSlotNames = Array.from(this._slotNames!)
       const allSlotNames = new Set([...oldSlotNames, ...newSlotNames])
       allSlotNames.forEach(name => {
         const fallbackNodes = this._slotFallbacks![name]
@@ -744,11 +781,21 @@ export class VueElement
             )
           }
 
-          // remove fallback nodes for added slots
+          // remove fallback nodes and render provided nodes for added slots
           if (!oldSlotNames.includes(name)) {
             fallbackNodes.forEach(fallbackNode =>
               this.removeChild(fallbackNode),
             )
+
+            const content = this._slots![name]
+            if (content) {
+              insertSlottedContent(
+                content,
+                this._instance!.type.__scopeId,
+                this._root,
+                (this._slotAnchors && this._slotAnchors!.get(name)) || null,
+              )
+            }
           }
         }
       })
@@ -829,7 +876,7 @@ export function useShadowRoot(): ShadowRoot | null {
 }
 
 function insertSlottedContent(
-  content: Node[],
+  content: (Node & { $parentNode?: Node })[],
   scopeId: string | undefined,
   parent: ParentNode,
   anchor: Node | null,
@@ -845,7 +892,7 @@ function insertSlottedContent(
         ;(child as Element).setAttribute(id, '')
       }
     }
-    ;(n as any).$parentNode = parent
+    n.$parentNode = parent
     parent.insertBefore(n, anchor)
   }
 }
@@ -858,7 +905,9 @@ function collectFragmentNodes(child: VNode): Node[] {
   ]
 }
 
-function collectNodes(children: VNodeArrayChildren): Node[] {
+function collectNodes(
+  children: VNodeArrayChildren,
+): (Node & { $parentNode?: Node })[] {
   const nodes: Node[] = []
   for (const child of children) {
     if (isArray(child)) {