})
// #13206
- test('update slotted v-if nodes w/ shadowRoot false', async () => {
+ test('update slotted v-if nodes w/ shadowRoot false (optimized mode)', async () => {
const E = defineCustomElement(
defineComponent({
props: {
}),
{ shadowRoot: false },
)
- customElements.define('ce-shadow-root-false', E)
+ customElements.define('ce-shadow-root-false-optimized', 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'),
- ])
+ return h(
+ 'ce-shadow-root-false-optimized',
+ { 'is-shown': this.isShown },
+ [renderSlot(this.$slots, 'default')],
+ )
},
})
{ isShown: isShown.value },
{
default: withCtx(() => [
- createElementVNode('div', null, isShown.value, 1 /* TEXT */),
+ createElementVNode(
+ 'div',
+ null,
+ String(isShown.value),
+ 1 /* TEXT */,
+ ),
count.value > 1
? (openBlock(), createElementBlock('div', { key: 0 }, 'hi'))
: createCommentVNode('v-if', true),
const app = createApp(App)
app.mount(container)
expect(container.innerHTML).toBe(
- `<ce-shadow-root-false data-v-app=""><!--v-if--></ce-shadow-root-false>`,
+ `<ce-shadow-root-false-optimized data-v-app="">` +
+ `<div>false</div><!--v-if-->` +
+ `</ce-shadow-root-false-optimized>`,
+ )
+
+ click()
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<ce-shadow-root-false-optimized data-v-app="" is-shown="">` +
+ `<div><div>true</div><!--v-if--></div>` +
+ `</ce-shadow-root-false-optimized>`,
+ )
+
+ click()
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<ce-shadow-root-false-optimized data-v-app="">` +
+ `<div>false</div><!--v-if-->` +
+ `</ce-shadow-root-false-optimized>`,
+ )
+
+ click()
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<ce-shadow-root-false-optimized data-v-app="" is-shown="">` +
+ `<div><div>true</div><div>hi</div></div>` +
+ `</ce-shadow-root-false-optimized>`,
+ )
+ })
+
+ test.todo('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=""><div>false</div><!--v-if--></ce-shadow-root-false>`,
)
click()
click()
await nextTick()
expect(container.innerHTML).toBe(
- `<ce-shadow-root-false data-v-app=""><!--v-if--></ce-shadow-root-false>`,
+ `<ce-shadow-root-false data-v-app=""><div>false</div><!--v-if--></ce-shadow-root-false>`,
)
click()
})
// #13234
- test('switch between slotted and fallback nodes w/ shadowRoot false', async () => {
+ test('switch between slotted and fallback nodes w/ shadowRoot false (optimized mode)', async () => {
const E = defineCustomElement(
defineComponent({
render() {
}),
{ shadowRoot: false },
)
- customElements.define('ce-with-fallback-shadow-root-false', E)
+ customElements.define('ce-with-fallback-shadow-root-false-optimized', E)
const Comp = defineComponent({
render() {
return (
openBlock(),
- createElementBlock('ce-with-fallback-shadow-root-false', null, [
- this.$slots.foo
- ? (openBlock(),
- createElementBlock('div', { key: 0, slot: 'foo' }, [
- renderSlot(this.$slots, 'foo'),
- ]))
- : createCommentVNode('v-if', true),
- renderSlot(this.$slots, 'default'),
- ])
+ createElementBlock(
+ 'ce-with-fallback-shadow-root-false-optimized',
+ null,
+ [
+ this.$slots.foo
+ ? (openBlock(),
+ createElementBlock('div', { key: 0, slot: 'foo' }, [
+ renderSlot(this.$slots, 'foo'),
+ ]))
+ : createCommentVNode('v-if', true),
+ renderSlot(this.$slots, 'default'),
+ ],
+ )
)
},
})
const app = createApp(App)
app.mount(container)
expect(container.innerHTML).toBe(
- `<ce-with-fallback-shadow-root-false data-v-app="">` +
+ `<ce-with-fallback-shadow-root-false-optimized data-v-app="">` +
`fallback` +
- `</ce-with-fallback-shadow-root-false>`,
+ `</ce-with-fallback-shadow-root-false-optimized>`,
)
isShown.value = true
await nextTick()
expect(container.innerHTML).toBe(
- `<ce-with-fallback-shadow-root-false data-v-app="">` +
+ `<ce-with-fallback-shadow-root-false-optimized data-v-app="">` +
`<div slot="foo">foo</div>` +
- `</ce-with-fallback-shadow-root-false>`,
+ `</ce-with-fallback-shadow-root-false-optimized>`,
)
isShown.value = false
await nextTick()
expect(container.innerHTML).toBe(
- `<ce-with-fallback-shadow-root-false data-v-app="">` +
+ `<ce-with-fallback-shadow-root-false-optimized data-v-app="">` +
`fallback<!--v-if-->` +
- `</ce-with-fallback-shadow-root-false>`,
+ `</ce-with-fallback-shadow-root-false-optimized>`,
)
})
+
+ test.todo(
+ '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` +
+ `</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="">` +
+ `fallback<!--v-if-->` +
+ `</ce-with-fallback-shadow-root-false>`,
+ )
+ },
+ )
})
describe('helpers', () => {
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
+ ;(n as any).$parentNode = n.parentNode
if (remove) this.removeChild(n)
n = next
}
const outlets = (this._teleportTarget || this).querySelectorAll('slot')
const scopeId = this._instance!.type.__scopeId
this._slotAnchors = new Map()
+ 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) {
- 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, '')
- }
- }
- parent.insertBefore(n, anchor)
- }
+ insertSlottedContent(content, scopeId, parent, anchor)
} else if (this._slotFallbacks) {
const nodes = this._slotFallbacks[slotName]
if (nodes) {
}
parent.removeChild(o)
}
+
+ // ensure default slot content is rendered if provided
+ if (!processedSlots.has('default')) {
+ let content = this._slots!['default']
+ if (content) {
+ // TODO
+ content = content.filter(
+ n => !(n.nodeType === 8 && (n as Comment).data === 'v-if'),
+ )
+ insertSlottedContent(content, scopeId, this, this.firstChild)
+ }
+ }
}
/**
* Only called when shadowRoot is false
*/
_updateSlots(n1: VNode, n2: VNode): void {
- // replace v-if nodes
+ // switch v-if nodes
const prevNodes = collectNodes(n1.children as VNodeArrayChildren)
const newNodes = collectNodes(n2.children as VNodeArrayChildren)
for (let i = 0; i < prevNodes.length; i++) {
prevNode !== newNode &&
(isComment(prevNode, 'v-if') || isComment(newNode, 'v-if'))
) {
- Object.keys(this._slots!).forEach(name => {
- const slotNodes = this._slots![name]
- if (slotNodes) {
- for (const node of slotNodes) {
- if (node === prevNode) {
- this._slots![name][i] = newNode
- break
- }
- }
+ Object.entries(this._slots!).forEach(([_, nodes]) => {
+ const nodeIndex = nodes.indexOf(prevNode)
+ if (nodeIndex > -1) {
+ nodes[nodeIndex] = newNode
}
})
}
return el && el.shadowRoot
}
+function insertSlottedContent(
+ content: 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, '')
+ }
+ }
+ parent.insertBefore(n, anchor)
+ }
+}
+
function collectFragmentNodes(child: VNode): Node[] {
return [
child.el as Node,