mirror of https://github.com/vuejs/core.git
Merge branch 'edison/feat/fowardedSlots' into edison/feat/setScopeId
This commit is contained in:
commit
1169db8c81
|
@ -103,6 +103,97 @@ export function render(_ctx) {
|
|||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transform slot > forwarded slots > <slot w/ nested component> 1`] = `
|
||||
"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
|
||||
|
||||
export function render(_ctx) {
|
||||
const _createForwardedSlot = _forwardedSlotCreator()
|
||||
const _component_Comp = _resolveComponent("Comp")
|
||||
const n2 = _createComponentWithFallback(_component_Comp, null, {
|
||||
"default": () => {
|
||||
const n1 = _createComponentWithFallback(_component_Comp, null, {
|
||||
"default": () => {
|
||||
const n0 = _createForwardedSlot("default", null)
|
||||
return n0
|
||||
}
|
||||
})
|
||||
return n1
|
||||
}
|
||||
}, true)
|
||||
return n2
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transform slot > forwarded slots > <slot> tag only 1`] = `
|
||||
"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
|
||||
|
||||
export function render(_ctx) {
|
||||
const _createForwardedSlot = _forwardedSlotCreator()
|
||||
const _component_Comp = _resolveComponent("Comp")
|
||||
const n1 = _createComponentWithFallback(_component_Comp, null, {
|
||||
"default": () => {
|
||||
const n0 = _createForwardedSlot("default", null)
|
||||
return n0
|
||||
}
|
||||
}, true)
|
||||
return n1
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transform slot > forwarded slots > <slot> tag w/ template 1`] = `
|
||||
"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
|
||||
|
||||
export function render(_ctx) {
|
||||
const _createForwardedSlot = _forwardedSlotCreator()
|
||||
const _component_Comp = _resolveComponent("Comp")
|
||||
const n2 = _createComponentWithFallback(_component_Comp, null, {
|
||||
"default": () => {
|
||||
const n0 = _createForwardedSlot("default", null)
|
||||
return n0
|
||||
}
|
||||
}, true)
|
||||
return n2
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transform slot > forwarded slots > <slot> tag w/ v-for 1`] = `
|
||||
"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createFor as _createFor, createComponentWithFallback as _createComponentWithFallback } from 'vue';
|
||||
|
||||
export function render(_ctx) {
|
||||
const _createForwardedSlot = _forwardedSlotCreator()
|
||||
const _component_Comp = _resolveComponent("Comp")
|
||||
const n3 = _createComponentWithFallback(_component_Comp, null, {
|
||||
"default": () => {
|
||||
const n0 = _createFor(() => (_ctx.b), (_for_item0) => {
|
||||
const n2 = _createForwardedSlot("default", null)
|
||||
return n2
|
||||
})
|
||||
return n0
|
||||
}
|
||||
}, true)
|
||||
return n3
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transform slot > forwarded slots > <slot> tag w/ v-if 1`] = `
|
||||
"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createIf as _createIf, createComponentWithFallback as _createComponentWithFallback } from 'vue';
|
||||
|
||||
export function render(_ctx) {
|
||||
const _createForwardedSlot = _forwardedSlotCreator()
|
||||
const _component_Comp = _resolveComponent("Comp")
|
||||
const n3 = _createComponentWithFallback(_component_Comp, null, {
|
||||
"default": () => {
|
||||
const n0 = _createIf(() => (_ctx.ok), () => {
|
||||
const n2 = _createForwardedSlot("default", null)
|
||||
return n2
|
||||
})
|
||||
return n0
|
||||
}
|
||||
}, true)
|
||||
return n3
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transform slot > implicit default slot 1`] = `
|
||||
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
|
||||
const t0 = _template("<div></div>")
|
||||
|
|
|
@ -409,6 +409,35 @@ describe('compiler: transform slot', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('forwarded slots', () => {
|
||||
test('<slot> tag only', () => {
|
||||
const { code } = compileWithSlots(`<Comp><slot/></Comp>`)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('<slot> tag w/ v-if', () => {
|
||||
const { code } = compileWithSlots(`<Comp><slot v-if="ok"/></Comp>`)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('<slot> tag w/ v-for', () => {
|
||||
const { code } = compileWithSlots(`<Comp><slot v-for="a in b"/></Comp>`)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('<slot> tag w/ template', () => {
|
||||
const { code } = compileWithSlots(
|
||||
`<Comp><template #default><slot/></template></Comp>`,
|
||||
)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('<slot w/ nested component>', () => {
|
||||
const { code } = compileWithSlots(`<Comp><Comp><slot/></Comp></Comp>`)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('errors', () => {
|
||||
test('error on extraneous children w/ named default slot', () => {
|
||||
const onError = vi.fn()
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
genCall,
|
||||
} from './generators/utils'
|
||||
import { setTemplateRefIdent } from './generators/templateRef'
|
||||
import { createForwardedSlotIdent } from './generators/slotOutlet'
|
||||
|
||||
export type CodegenOptions = Omit<BaseCodegenOptions, 'optimizeImports'>
|
||||
|
||||
|
@ -129,6 +130,12 @@ export function generate(
|
|||
`const ${setTemplateRefIdent} = ${context.helper('createTemplateRefSetter')}()`,
|
||||
)
|
||||
}
|
||||
if (ir.hasForwardedSlot) {
|
||||
push(
|
||||
NEWLINE,
|
||||
`const ${createForwardedSlotIdent} = ${context.helper('forwardedSlotCreator')}()`,
|
||||
)
|
||||
}
|
||||
push(...genBlockContent(ir.block, context, true))
|
||||
push(INDENT_END, NEWLINE)
|
||||
|
||||
|
|
|
@ -5,12 +5,14 @@ import { genExpression } from './expression'
|
|||
import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
|
||||
import { genRawProps } from './component'
|
||||
|
||||
export const createForwardedSlotIdent = `_createForwardedSlot`
|
||||
|
||||
export function genSlotOutlet(
|
||||
oper: SlotOutletIRNode,
|
||||
context: CodegenContext,
|
||||
): CodeFragment[] {
|
||||
const { helper } = context
|
||||
const { id, name, fallback } = oper
|
||||
const { id, name, fallback, forwarded } = oper
|
||||
const [frag, push] = buildCodeFragment()
|
||||
|
||||
const nameExpr = name.isStatic
|
||||
|
@ -26,7 +28,7 @@ export function genSlotOutlet(
|
|||
NEWLINE,
|
||||
`const n${id} = `,
|
||||
...genCall(
|
||||
helper('createSlot'),
|
||||
forwarded ? createForwardedSlotIdent : helper('createSlot'),
|
||||
nameExpr,
|
||||
genRawProps(oper.props, context) || 'null',
|
||||
fallbackArg,
|
||||
|
|
|
@ -66,6 +66,7 @@ export interface RootIRNode {
|
|||
directive: Set<string>
|
||||
block: BlockIRNode
|
||||
hasTemplateRef: boolean
|
||||
hasForwardedSlot: boolean
|
||||
}
|
||||
|
||||
export interface IfIRNode extends BaseIRNode {
|
||||
|
@ -209,6 +210,7 @@ export interface SlotOutletIRNode extends BaseIRNode {
|
|||
name: SimpleExpressionNode
|
||||
props: IRProps[]
|
||||
fallback?: BlockIRNode
|
||||
forwarded?: boolean
|
||||
parent?: number
|
||||
anchor?: number
|
||||
}
|
||||
|
|
|
@ -76,6 +76,7 @@ export class TransformContext<T extends AllNode = AllNode> {
|
|||
|
||||
inVOnce: boolean = false
|
||||
inVFor: number = 0
|
||||
inSlot: boolean = false
|
||||
|
||||
comment: CommentNode[] = []
|
||||
component: Set<string> = this.ir.component
|
||||
|
@ -230,6 +231,7 @@ export function transform(
|
|||
directive: new Set(),
|
||||
block: newBlock(node),
|
||||
hasTemplateRef: false,
|
||||
hasForwardedSlot: false,
|
||||
}
|
||||
|
||||
const context = new TransformContext(ir, node, options)
|
||||
|
|
|
@ -99,6 +99,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
|
|||
}
|
||||
|
||||
return () => {
|
||||
if (context.inSlot) context.ir.hasForwardedSlot = true
|
||||
exitBlock && exitBlock()
|
||||
context.dynamic.operation = {
|
||||
type: IRNodeTypes.SLOT_OUTLET_NODE,
|
||||
|
@ -106,6 +107,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
|
|||
name: slotName,
|
||||
props: irProps,
|
||||
fallback,
|
||||
forwarded: context.inSlot,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -237,7 +237,14 @@ function createSlotBlock(
|
|||
const block: SlotBlockIRNode = newBlock(slotNode)
|
||||
block.props = dir && dir.exp
|
||||
const exitBlock = context.enterBlock(block)
|
||||
return [block, exitBlock]
|
||||
context.inSlot = true
|
||||
return [
|
||||
block,
|
||||
() => {
|
||||
context.inSlot = false
|
||||
exitBlock()
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function isNonWhitespaceContent(node: TemplateChildNode): boolean {
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
createSlot,
|
||||
createVaporApp,
|
||||
defineVaporComponent,
|
||||
forwardedSlotCreator,
|
||||
insert,
|
||||
prepend,
|
||||
renderEffect,
|
||||
|
@ -15,7 +16,7 @@ import {
|
|||
import { currentInstance, nextTick, ref } from '@vue/runtime-dom'
|
||||
import { makeRender } from './_utils'
|
||||
import type { DynamicSlot } from '../src/componentSlots'
|
||||
import { setElementText } from '../src/dom/prop'
|
||||
import { setElementText, setText } from '../src/dom/prop'
|
||||
|
||||
const define = makeRender<any>()
|
||||
|
||||
|
@ -503,4 +504,106 @@ describe('component: slots', () => {
|
|||
expect(host.innerHTML).toBe('<div><h1></h1><!--slot--></div>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('forwarded slot', () => {
|
||||
test('should work', async () => {
|
||||
const Child = defineVaporComponent({
|
||||
setup() {
|
||||
return createSlot('foo', null)
|
||||
},
|
||||
})
|
||||
const Parent = defineVaporComponent({
|
||||
setup() {
|
||||
const createForwardedSlot = forwardedSlotCreator()
|
||||
const n2 = createComponent(
|
||||
Child,
|
||||
null,
|
||||
{
|
||||
foo: () => {
|
||||
return createForwardedSlot('foo', null)
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
return n2
|
||||
},
|
||||
})
|
||||
|
||||
const foo = ref('foo')
|
||||
const { host } = define({
|
||||
setup() {
|
||||
const n2 = createComponent(
|
||||
Parent,
|
||||
null,
|
||||
{
|
||||
foo: () => {
|
||||
const n0 = template(' ')() as any
|
||||
renderEffect(() => setText(n0, foo.value))
|
||||
return n0
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
return n2
|
||||
},
|
||||
}).render()
|
||||
|
||||
expect(host.innerHTML).toBe('foo<!--slot--><!--slot-->')
|
||||
|
||||
foo.value = 'bar'
|
||||
await nextTick()
|
||||
expect(host.innerHTML).toBe('bar<!--slot--><!--slot-->')
|
||||
})
|
||||
|
||||
test('mixed with non-forwarded slot', async () => {
|
||||
const Child = defineVaporComponent({
|
||||
setup() {
|
||||
return [createSlot('foo', null)]
|
||||
},
|
||||
})
|
||||
const Parent = defineVaporComponent({
|
||||
setup() {
|
||||
const createForwardedSlot = forwardedSlotCreator()
|
||||
const n2 = createComponent(Child, null, {
|
||||
foo: () => {
|
||||
const n0 = createForwardedSlot('foo', null)
|
||||
return n0
|
||||
},
|
||||
})
|
||||
const n3 = createSlot('default', null)
|
||||
return [n2, n3]
|
||||
},
|
||||
})
|
||||
|
||||
const foo = ref('foo')
|
||||
const { host } = define({
|
||||
setup() {
|
||||
const n2 = createComponent(
|
||||
Parent,
|
||||
null,
|
||||
{
|
||||
foo: () => {
|
||||
const n0 = template(' ')() as any
|
||||
renderEffect(() => setText(n0, foo.value))
|
||||
return n0
|
||||
},
|
||||
default: () => {
|
||||
const n3 = template(' ')() as any
|
||||
renderEffect(() => setText(n3, foo.value))
|
||||
return n3
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
return n2
|
||||
},
|
||||
}).render()
|
||||
|
||||
expect(host.innerHTML).toBe('foo<!--slot--><!--slot-->foo<!--slot-->')
|
||||
|
||||
foo.value = 'bar'
|
||||
await nextTick()
|
||||
expect(host.innerHTML).toBe('bar<!--slot--><!--slot-->bar<!--slot-->')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -210,7 +210,8 @@ export function hasAttrFromRawProps(rawProps: RawProps, key: string): boolean {
|
|||
if (dynamicSources) {
|
||||
let i = dynamicSources.length
|
||||
while (i--) {
|
||||
if (hasOwn(resolveSource(dynamicSources[i]), key)) {
|
||||
const source = resolveSource(dynamicSources[i])
|
||||
if (source && hasOwn(source, key)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,10 +87,21 @@ export function getSlot(
|
|||
}
|
||||
}
|
||||
|
||||
export function forwardedSlotCreator(): (
|
||||
name: string | (() => string),
|
||||
rawProps?: LooseRawProps | null,
|
||||
fallback?: VaporSlot,
|
||||
) => Block {
|
||||
const instance = currentInstance as VaporComponentInstance
|
||||
return (name, rawProps, fallback) =>
|
||||
createSlot(name, rawProps, fallback, instance)
|
||||
}
|
||||
|
||||
export function createSlot(
|
||||
name: string | (() => string),
|
||||
rawProps?: LooseRawProps | null,
|
||||
fallback?: VaporSlot,
|
||||
i?: VaporComponentInstance,
|
||||
): Block {
|
||||
const _insertionParent = insertionParent
|
||||
const _insertionAnchor = insertionAnchor
|
||||
|
@ -98,7 +109,7 @@ export function createSlot(
|
|||
locateHydrationNode()
|
||||
}
|
||||
|
||||
const instance = currentInstance as VaporComponentInstance
|
||||
const instance = i || (currentInstance as VaporComponentInstance)
|
||||
const rawSlots = instance.rawSlots
|
||||
const slotProps = rawProps
|
||||
? new Proxy(rawProps, rawPropsProxyHandlers)
|
||||
|
|
|
@ -9,7 +9,7 @@ export { insert, prepend, remove, isFragment, VaporFragment } from './block'
|
|||
export { setInsertionState } from './insertionState'
|
||||
export { createComponent, createComponentWithFallback } from './component'
|
||||
export { renderEffect } from './renderEffect'
|
||||
export { createSlot } from './componentSlots'
|
||||
export { createSlot, forwardedSlotCreator } from './componentSlots'
|
||||
export { template } from './dom/template'
|
||||
export { createTextNode, child, nthChild, next } from './dom/node'
|
||||
export {
|
||||
|
|
|
@ -26,7 +26,14 @@ import {
|
|||
mountComponent,
|
||||
unmountComponent,
|
||||
} from './component'
|
||||
import { type Block, VaporFragment, insert, remove } from './block'
|
||||
import {
|
||||
type Block,
|
||||
VaporFragment,
|
||||
insert,
|
||||
isFragment,
|
||||
isValidBlock,
|
||||
remove,
|
||||
} from './block'
|
||||
import { EMPTY_OBJ, extend, isFunction } from '@vue/shared'
|
||||
import { type RawProps, rawPropsProxyHandlers } from './componentProps'
|
||||
import type { RawSlots, VaporSlot } from './componentSlots'
|
||||
|
@ -236,7 +243,24 @@ function renderVDOMSlot(
|
|||
isFunction(name) ? name() : name,
|
||||
props,
|
||||
)
|
||||
if ((vnode.children as any[]).length) {
|
||||
let isValidSlotContent
|
||||
let children = vnode.children as any[]
|
||||
|
||||
// TODO add tests
|
||||
// handle forwarded vapor slot
|
||||
let vaporSlot
|
||||
if (children.length === 1 && (vaporSlot = children[0].vs)) {
|
||||
const block = vaporSlot.slot(props)
|
||||
isValidSlotContent =
|
||||
isValidBlock(block) ||
|
||||
// if block is a vapor fragment with insert, it indicates a forwarded VDOM slot
|
||||
(isFragment(block) && block.insert)
|
||||
}
|
||||
// vnode children
|
||||
else {
|
||||
isValidSlotContent = children.length > 0
|
||||
}
|
||||
if (isValidSlotContent) {
|
||||
if (fallbackNodes) {
|
||||
remove(fallbackNodes, parentNode)
|
||||
fallbackNodes = undefined
|
||||
|
|
Loading…
Reference in New Issue