From: daiwei Date: Fri, 9 May 2025 08:54:54 +0000 (+0800) Subject: wip: save X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=aec2dfb59d4f5d0cfb7f9e74cb52d199f27499d6;p=thirdparty%2Fvuejs%2Fcore.git wip: save --- diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index d75efb890c..79965b28a9 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -966,7 +966,7 @@ function baseCreateRenderer( !isSameVNodeType(oldVNode, newVNode) || // - In the case of a component, it could contain anything. oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT)) - ? hostParentNode(oldVNode.el) || oldVNode.el._parentNode + ? hostParentNode(oldVNode.el) || oldVNode.el.$parentNode : // In other cases, the parent container is not actually used so we // just pass the block element here to avoid a DOM parentNode call. fallbackContainer diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index 85f1d0820a..02522fbd92 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -1141,7 +1141,7 @@ describe('defineCustomElement', () => { }) // #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: { @@ -1155,16 +1155,18 @@ describe('defineCustomElement', () => { }), { 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')], + ) }, }) @@ -1185,7 +1187,12 @@ describe('defineCustomElement', () => { { 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), @@ -1204,7 +1211,94 @@ describe('defineCustomElement', () => { const app = createApp(App) app.mount(container) expect(container.innerHTML).toBe( - ``, + `` + + `
false
` + + `
`, + ) + + click() + await nextTick() + expect(container.innerHTML).toBe( + `` + + `
true
` + + `
`, + ) + + click() + await nextTick() + expect(container.innerHTML).toBe( + `` + + `
false
` + + `
`, + ) + + click() + await nextTick() + expect(container.innerHTML).toBe( + `` + + `
true
hi
` + + `
`, + ) + }) + + 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( + `
false
`, ) click() @@ -1216,7 +1310,7 @@ describe('defineCustomElement', () => { click() await nextTick() expect(container.innerHTML).toBe( - ``, + `
false
`, ) click() @@ -1227,7 +1321,7 @@ describe('defineCustomElement', () => { }) // #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() { @@ -1238,21 +1332,25 @@ describe('defineCustomElement', () => { }), { 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'), + ], + ) ) }, }) @@ -1290,27 +1388,107 @@ describe('defineCustomElement', () => { const app = createApp(App) app.mount(container) expect(container.innerHTML).toBe( - `` + + `` + `fallback` + - ``, + ``, ) isShown.value = true await nextTick() expect(container.innerHTML).toBe( - `` + + `` + `
foo
` + - `
`, + ``, ) isShown.value = false await nextTick() expect(container.innerHTML).toBe( - `` + + `` + `fallback` + - ``, + ``, ) }) + + 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( + `` + + `fallback` + + ``, + ) + + isShown.value = true + await nextTick() + expect(container.innerHTML).toBe( + `` + + `
foo
` + + `
`, + ) + + isShown.value = false + await nextTick() + expect(container.innerHTML).toBe( + `` + + `fallback` + + ``, + ) + }, + ) }) describe('helpers', () => { diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index c5f3c9e341..a6a4b1ba1e 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -635,7 +635,7 @@ export class VueElement 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 } @@ -648,9 +648,12 @@ export class VueElement const outlets = (this._teleportTarget || this).querySelectorAll('slot') const scopeId = this._instance!.type.__scopeId this._slotAnchors = new Map() + const processedSlots = new Set() + 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! @@ -660,19 +663,7 @@ export class VueElement 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) { @@ -683,13 +674,25 @@ 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) { + // 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++) { @@ -699,15 +702,10 @@ export class VueElement 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 } }) } @@ -815,6 +813,27 @@ export function useShadowRoot(): ShadowRoot | null { 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,