toggle.value = false
await nextTick()
expect(e.innerHTML).toBe(
- `<span>default</span>text` +
- `<template name="named"></template>` +
- `<!---->` +
- `<div>fallback</div>`,
+ `<span>default</span>text` + `<!---->` + `<div>fallback</div>`,
)
})
app.mount(container)
expect(container.innerHTML).toBe(
`<ce-shadow-root-false-optimized data-v-app="">` +
- `<!--v-if--><template name="default"></template>` +
+ `<!--v-if-->` +
`</ce-shadow-root-false-optimized>`,
)
await nextTick()
expect(container.innerHTML).toBe(
`<ce-shadow-root-false-optimized data-v-app="">` +
- `<!--v-if--><template name="default"></template>` +
+ `<!--v-if-->` +
`</ce-shadow-root-false-optimized>`,
)
)
})
- test('update slotted v-if nodes w/ shadowRoot false', async () => {
- const E = defineCustomElement(
- defineComponent({
- props: {
- isShown: { type: Boolean, required: true },
- },
- render() {
- return this.isShown
- ? h('div', { key: 0 }, [renderSlot(this.$slots, 'default')])
- : createCommentVNode('v-if')
- },
- }),
- { shadowRoot: false },
- )
- customElements.define('ce-shadow-root-false', E)
-
- const Comp = defineComponent({
- props: {
- isShown: { type: Boolean, required: true },
- },
- render() {
- return h('ce-shadow-root-false', { 'is-shown': this.isShown }, [
- renderSlot(this.$slots, 'default'),
- ])
- },
- })
-
- const isShown = ref(false)
- const count = ref(0)
-
- function click() {
- isShown.value = !isShown.value
- count.value++
- }
-
- const App = {
- render() {
- return h(
- Comp,
- { isShown: isShown.value },
- {
- default: () => [
- h('div', null, String(isShown.value)),
- count.value > 1
- ? h('div', { key: 0 }, 'hi')
- : createCommentVNode('v-if', true),
- ],
- },
- )
- },
- }
- const container = document.createElement('div')
- document.body.appendChild(container)
-
- const app = createApp(App)
- app.mount(container)
- expect(container.innerHTML).toBe(
- `<ce-shadow-root-false data-v-app="">` +
- `<!--v-if--><template name="default"></template>` +
- `</ce-shadow-root-false>`,
- )
-
- click()
- await nextTick()
- expect(container.innerHTML).toBe(
- `<ce-shadow-root-false data-v-app="" is-shown="">` +
- `<div><div>true</div><!--v-if--></div>` +
- `</ce-shadow-root-false>`,
- )
-
- click()
- await nextTick()
- expect(container.innerHTML).toBe(
- `<ce-shadow-root-false data-v-app="">` +
- `<!--v-if--><template name="default"></template>` +
- `</ce-shadow-root-false>`,
- )
-
- click()
- await nextTick()
- expect(container.innerHTML).toBe(
- `<ce-shadow-root-false data-v-app="" is-shown="">` +
- `<div><div>true</div><div>hi</div></div>` +
- `</ce-shadow-root-false>`,
- )
- })
-
// #13234
test('switch between slotted and fallback nodes w/ shadowRoot false (optimized mode)', async () => {
const E = defineCustomElement(
app.mount(container)
expect(container.innerHTML).toBe(
`<ce-with-fallback-shadow-root-false-optimized data-v-app="">` +
- `fallback<template name="default"></template>` +
+ `fallback` +
`</ce-with-fallback-shadow-root-false-optimized>`,
)
await nextTick()
expect(container.innerHTML).toBe(
`<ce-with-fallback-shadow-root-false-optimized data-v-app="">` +
- `<!--v-if-->fallback` +
+ `fallback<!--v-if-->` +
`</ce-with-fallback-shadow-root-false-optimized>`,
)
})
-
- test('switch between slotted and fallback nodes w/ shadowRoot false', async () => {
- const E = defineCustomElement(
- defineComponent({
- render() {
- return renderSlot(this.$slots, 'foo', {}, () => [
- createTextVNode('fallback'),
- ])
- },
- }),
- { shadowRoot: false },
- )
- customElements.define('ce-with-fallback-shadow-root-false', E)
-
- const Comp = defineComponent({
- render() {
- return h('ce-with-fallback-shadow-root-false', null, [
- this.$slots.foo
- ? h('div', { key: 0, slot: 'foo' }, [
- renderSlot(this.$slots, 'foo'),
- ])
- : createCommentVNode('v-if', true),
- renderSlot(this.$slots, 'default'),
- ])
- },
- })
-
- const isShown = ref(false)
- const App = defineComponent({
- components: { Comp },
- render() {
- return h(
- Comp,
- null,
- createSlots(
- { _: 2 /* DYNAMIC */ } as any,
- [
- isShown.value
- ? {
- name: 'foo',
- fn: withCtx(() => [createTextVNode('foo')]),
- key: '0',
- }
- : undefined,
- ] as any,
- ),
- )
- },
- })
-
- const container = document.createElement('div')
- document.body.appendChild(container)
-
- const app = createApp(App)
- app.mount(container)
- expect(container.innerHTML).toBe(
- `<ce-with-fallback-shadow-root-false data-v-app="">` +
- `fallback<template name="default"></template>` +
- `</ce-with-fallback-shadow-root-false>`,
- )
-
- isShown.value = true
- await nextTick()
- expect(container.innerHTML).toBe(
- `<ce-with-fallback-shadow-root-false data-v-app="">` +
- `<div slot="foo">foo</div>` +
- `</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>`,
- // )
- })
})
describe('helpers', () => {
createVNode,
defineComponent,
getCurrentInstance,
- isTemplateNode,
isVNode,
nextTick,
unref,
private _slots?: Record<string, (Node & { $parentNode?: Node })[]>
private _slotFallbacks?: Record<string, Node[]>
private _slotAnchors?: Map<string, Node>
- private _slotNames: Set<string> | undefined
constructor(
/**
const baseProps: VNodeProps = {}
if (!this.shadowRoot) {
baseProps.onVnodeMounted = () => {
- this._captureSlotFallbacks()
+ this._parseSlotFallbacks()
this._renderSlots()
}
baseProps.onVnodeUpdated = this._renderSlots.bind(this)
* Only called when shadowRoot is false
*/
private _parseSlots(remove: boolean = true) {
- if (!this._slotNames) this._slotNames = new Set()
- else this._slotNames.clear()
- this._slots = {}
-
+ const slots: VueElement['_slots'] = (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'
- this.addToSlot(slotName, n, remove)
- if (remove) template.removeChild(n)
+ ;(slots[slotName] || (slots[slotName] = [])).push(n)
+ const next = n.nextSibling
+ // store the parentNode reference since node will be removed
+ // but it is needed during patching
+ ;(n as any).$parentNode = n.parentNode
+ if (remove) this.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
*/
private _renderSlots() {
const outlets = (this._teleportTarget || this).querySelectorAll('slot')
const scopeId = this._instance!.type.__scopeId
- const processedSlots = new Set<string>()
for (let i = 0; i < outlets.length; i++) {
const o = outlets[i] as HTMLSlotElement
const slotName = o.getAttribute('name') || 'default'
- processedSlots.add(slotName)
const content = this._slots![slotName]
const parent = o.parentNode!
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)
+ for (const n of content) {
+ // for :slotted css
+ if (scopeId && n.nodeType === 1) {
+ const id = scopeId + '-s'
+ const walker = document.createTreeWalker(n, 1)
+ ;(n as Element).setAttribute(id, '')
+ let child
+ while ((child = walker.nextNode())) {
+ ;(child as Element).setAttribute(id, '')
+ }
+ }
+ n.$parentNode = parent
+ parent.insertBefore(n, anchor)
}
} else if (this._slotFallbacks) {
const nodes = this._slotFallbacks[slotName]
}
parent.removeChild(o)
}
-
- // 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 (this._slotAnchors) {
- const slotNames = Array.from(this._slotNames!)
- const slotIndex = slotNames.indexOf(slotName)
- if (slotIndex > 0) {
- const prevSlotAnchor = this._slotAnchors.get(
- slotNames[slotIndex - 1],
- )
- if (prevSlotAnchor) anchor = prevSlotAnchor.nextSibling
- }
- }
-
- 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)
- }
- }
}
/**
for (let i = 0; i < prevNodes.length; i++) {
const prevNode = prevNodes[i]
const newNode = newNodes[i]
- if (
- prevNode !== newNode &&
- (isComment(prevNode, 'v-if') || isComment(newNode, 'v-if'))
- ) {
+ if (isComment(prevNode, 'v-if') || isComment(newNode, 'v-if')) {
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)
}
})
}
// switch between fallback and provided content
if (this._slotFallbacks) {
- const oldSlotNames = Array.from(this._slotNames!)
+ const oldSlotNames = Object.keys(this._slots!)
// re-parse slots
this._parseSlots(false)
- const newSlotNames = Array.from(this._slotNames!)
+ const newSlotNames = Object.keys(this._slots!)
const allSlotNames = new Set([...oldSlotNames, ...newSlotNames])
allSlotNames.forEach(name => {
const fallbackNodes = this._slotFallbacks![name]
if (fallbackNodes) {
// render fallback nodes for removed slots
- if (!newSlotNames.includes(name) && this._slotAnchors) {
- const anchor = this._slotAnchors.get(name)!
+ if (!newSlotNames.includes(name)) {
+ const anchor = this._slotAnchors!.get(name)!
fallbackNodes.forEach(fallbackNode =>
this.insertBefore(fallbackNode, anchor),
)
}
- // remove fallback nodes and render provided nodes for added slots
+ // remove fallback 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,
- )
- }
}
}
})
/**
* Only called when shadowRoot is false
*/
- private _captureSlotFallbacks() {
+ private _parseSlotFallbacks() {
const outlets = (this._teleportTarget || this).querySelectorAll('slot')
for (let i = 0; i < outlets.length; i++) {
const slotElement = outlets[i] as HTMLSlotElement
return el && el.shadowRoot
}
-function insertSlottedContent(
- content: (Node & { $parentNode?: Node })[],
- scopeId: string | undefined,
- parent: ParentNode,
- anchor: Node | null,
-) {
- for (const n of content) {
- // for :slotted css
- if (scopeId && n.nodeType === 1) {
- const id = scopeId + '-s'
- const walker = document.createTreeWalker(n, 1)
- ;(n as Element).setAttribute(id, '')
- let child
- while ((child = walker.nextNode())) {
- ;(child as Element).setAttribute(id, '')
- }
- }
- n.$parentNode = parent
- parent.insertBefore(n, anchor)
- }
-}
-
function collectFragmentNodes(child: VNode): Node[] {
return [
child.el as Node,