diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap index ff692bbf6..b5075babe 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap @@ -17,6 +17,78 @@ export function render(_ctx) { }" `; +exports[`compiler: transform slot > dynamic slots name w/ v-for 1`] = ` +"import { resolveComponent as _resolveComponent, createComponent as _createComponent, createForSlots as _createForSlots, template as _template } from 'vue/vapor'; +const t0 = _template("foo") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n2 = _createComponent(_component_Comp, null, null, () => [_createForSlots(_ctx.list, (item) => ({ + name: item, + fn: () => { + const n0 = t0() + return n0 + } + }))], true) + return n2 +}" +`; + +exports[`compiler: transform slot > dynamic slots name w/ v-for and provide absent key 1`] = ` +"import { resolveComponent as _resolveComponent, createComponent as _createComponent, createForSlots as _createForSlots, template as _template } from 'vue/vapor'; +const t0 = _template("foo") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n2 = _createComponent(_component_Comp, null, null, () => [_createForSlots(_ctx.list, (_, __, index) => ({ + name: index, + fn: () => { + const n0 = t0() + return n0 + } + }))], true) + return n2 +}" +`; + +exports[`compiler: transform slot > dynamic slots name w/ v-if / v-else[-if] 1`] = ` +"import { resolveComponent as _resolveComponent, createComponent as _createComponent, template as _template } from 'vue/vapor'; +const t0 = _template("condition slot") +const t1 = _template("another condition") +const t2 = _template("else condition") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n6 = _createComponent(_component_Comp, null, null, () => [_ctx.condition + ? { + name: "condition", + fn: () => { + const n0 = t0() + return n0 + }, + key: "0" + } + : _ctx.anotherCondition + ? { + name: "condition", + fn: () => { + const n2 = t1() + return n2 + }, + key: "1" + } + : { + name: "condition", + fn: () => { + const n4 = t2() + return n4 + }, + key: "2" + }], true) + return n6 +}" +`; + exports[`compiler: transform slot > implicit default slot 1`] = ` "import { resolveComponent as _resolveComponent, createComponent as _createComponent, template as _template } from 'vue/vapor'; const t0 = _template("
") diff --git a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts index c98b75538..ce09fb646 100644 --- a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts @@ -1,5 +1,6 @@ import { ErrorCodes, NodeTypes } from '@vue/compiler-core' import { + DynamicSlotType, IRNodeTypes, transformChildren, transformElement, @@ -126,6 +127,112 @@ describe('compiler: transform slot', () => { ]) }) + test('dynamic slots name w/ v-for', () => { + const { ir, code } = compileWithSlots( + ` + + `, + ) + expect(code).toMatchSnapshot() + expect(ir.block.operation[0].type).toBe(IRNodeTypes.CREATE_COMPONENT_NODE) + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Comp', + slots: undefined, + dynamicSlots: [ + { + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'item', + isStatic: false, + }, + fn: { type: IRNodeTypes.BLOCK }, + loop: { + source: { content: 'list' }, + value: { content: 'item' }, + key: undefined, + index: undefined, + }, + }, + ], + }, + ]) + }) + + test('dynamic slots name w/ v-for and provide absent key', () => { + const { ir, code } = compileWithSlots( + ` + + `, + ) + expect(code).toMatchSnapshot() + expect(ir.block.operation[0].type).toBe(IRNodeTypes.CREATE_COMPONENT_NODE) + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Comp', + slots: undefined, + dynamicSlots: [ + { + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'index', + isStatic: false, + }, + fn: { type: IRNodeTypes.BLOCK }, + loop: { + source: { content: 'list' }, + value: undefined, + key: undefined, + index: { + type: NodeTypes.SIMPLE_EXPRESSION, + }, + }, + }, + ], + }, + ]) + }) + + test('dynamic slots name w/ v-if / v-else[-if]', () => { + const { ir, code } = compileWithSlots( + ` + + + + `, + ) + expect(code).toMatchSnapshot() + expect(ir.block.operation[0].type).toBe(IRNodeTypes.CREATE_COMPONENT_NODE) + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Comp', + slots: undefined, + dynamicSlots: [ + { + slotType: DynamicSlotType.CONDITIONAL, + condition: { content: 'condition' }, + positive: { + slotType: DynamicSlotType.BASIC, + key: 0, + }, + negative: { + slotType: DynamicSlotType.CONDITIONAL, + condition: { content: 'anotherCondition' }, + positive: { + slotType: DynamicSlotType.BASIC, + key: 1, + }, + negative: { slotType: DynamicSlotType.BASIC, key: 2 }, + }, + }, + ], + }, + ]) + }) + describe('errors', () => { test('error on extraneous children w/ named default slot', () => { const onError = vi.fn() diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 74602d70e..4f5f5477e 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -1,9 +1,13 @@ import { camelize, extend, isArray } from '@vue/shared' import type { CodegenContext } from '../generate' import { + type ComponentBasicDynamicSlot, + type ComponentConditionalDynamicSlot, type ComponentDynamicSlot, + type ComponentLoopDynamicSlot, type ComponentSlots, type CreateComponentIRNode, + DynamicSlotType, IRDynamicPropsKind, type IRProp, type IRProps, @@ -15,6 +19,8 @@ import { DELIMITERS_ARRAY_NEWLINE, DELIMITERS_OBJECT, DELIMITERS_OBJECT_NEWLINE, + INDENT_END, + INDENT_START, NEWLINE, genCall, genMulti, @@ -155,13 +161,90 @@ function genDynamicSlots( ) { const slotsExpr = genMulti( dynamicSlots.length > 1 ? DELIMITERS_ARRAY_NEWLINE : DELIMITERS_ARRAY, - ...dynamicSlots.map(({ name, fn }) => - genMulti( - DELIMITERS_OBJECT_NEWLINE, - ['name: ', ...genExpression(name, context)], - ['fn: ', ...genBlock(fn, context)], - ), - ), + ...dynamicSlots.map(slot => genDynamicSlot(slot, context)), ) return ['() => ', ...slotsExpr] } + +function genDynamicSlot( + slot: ComponentDynamicSlot, + context: CodegenContext, +): CodeFragment[] { + switch (slot.slotType) { + case DynamicSlotType.BASIC: + return genBasicDynamicSlot(slot, context) + case DynamicSlotType.LOOP: + return genLoopSlot(slot, context) + case DynamicSlotType.CONDITIONAL: + return genConditionalSlot(slot, context) + } +} + +function genBasicDynamicSlot( + slot: ComponentBasicDynamicSlot, + context: CodegenContext, +): CodeFragment[] { + const { name, fn, key } = slot + return genMulti( + DELIMITERS_OBJECT_NEWLINE, + ['name: ', ...genExpression(name, context)], + ['fn: ', ...genBlock(fn, context)], + ...(key !== undefined ? [`key: "${key}"`] : []), + ) +} + +function genLoopSlot( + slot: ComponentLoopDynamicSlot, + context: CodegenContext, +): CodeFragment[] { + const { name, fn, loop } = slot + const { value, key, index, source } = loop + const rawValue = value && value.content + const rawKey = key && key.content + const rawIndex = index && index.content + + const idMap: Record = {} + if (rawValue) idMap[rawValue] = rawValue + if (rawKey) idMap[rawKey] = rawKey + if (rawIndex) idMap[rawIndex] = rawIndex + const slotExpr = genMulti( + DELIMITERS_OBJECT_NEWLINE, + ['name: ', ...context.withId(() => genExpression(name, context), idMap)], + ['fn: ', ...context.withId(() => genBlock(fn, context), idMap)], + ) + return [ + ...genCall( + context.vaporHelper('createForSlots'), + genExpression(source, context), + [ + ...genMulti( + ['(', ')', ', '], + rawValue ? rawValue : rawKey || rawIndex ? '_' : undefined, + rawKey ? rawKey : rawIndex ? '__' : undefined, + rawIndex, + ), + ' => (', + ...slotExpr, + ')', + ], + ), + ] +} + +function genConditionalSlot( + slot: ComponentConditionalDynamicSlot, + context: CodegenContext, +): CodeFragment[] { + const { condition, positive, negative } = slot + return [ + ...genExpression(condition, context), + INDENT_START, + NEWLINE, + '? ', + ...genDynamicSlot(positive, context), + NEWLINE, + ': ', + ...(negative ? [...genDynamicSlot(negative, context)] : ['void 0']), + INDENT_END, + ] +} diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index c86e7c2ea..83e03126f 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -73,13 +73,16 @@ export interface IfIRNode extends BaseIRNode { once?: boolean } -export interface ForIRNode extends BaseIRNode { - type: IRNodeTypes.FOR - id: number +export interface IRFor { source: SimpleExpressionNode value?: SimpleExpressionNode key?: SimpleExpressionNode index?: SimpleExpressionNode +} + +export interface ForIRNode extends BaseIRNode, IRFor { + type: IRNodeTypes.FOR + id: number keyProp?: SimpleExpressionNode render: BlockIRNode once: boolean @@ -208,12 +211,39 @@ export interface ComponentSlotBlockIRNode extends BlockIRNode { // TODO slot props } export type ComponentSlots = Record -export interface ComponentDynamicSlot { + +export enum DynamicSlotType { + BASIC, + LOOP, + CONDITIONAL, +} + +export interface ComponentBasicDynamicSlot { + slotType: DynamicSlotType.BASIC name: SimpleExpressionNode fn: ComponentSlotBlockIRNode - key?: string + key?: number } +export interface ComponentLoopDynamicSlot { + slotType: DynamicSlotType.LOOP + name: SimpleExpressionNode + fn: ComponentSlotBlockIRNode + loop: IRFor +} + +export interface ComponentConditionalDynamicSlot { + slotType: DynamicSlotType.CONDITIONAL + condition: SimpleExpressionNode + positive: ComponentBasicDynamicSlot + negative?: ComponentBasicDynamicSlot | ComponentConditionalDynamicSlot +} + +export type ComponentDynamicSlot = + | ComponentBasicDynamicSlot + | ComponentLoopDynamicSlot + | ComponentConditionalDynamicSlot + export interface CreateComponentIRNode extends BaseIRNode { type: IRNodeTypes.CREATE_COMPONENT_NODE id: number diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index ffd093a65..7aeb2abc9 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -10,7 +10,15 @@ import { } from '@vue/compiler-core' import type { NodeTransform, TransformContext } from '../transform' import { newBlock } from './utils' -import { type BlockIRNode, DynamicFlag, type VaporDirectiveNode } from '../ir' +import { + type BlockIRNode, + type ComponentBasicDynamicSlot, + type ComponentConditionalDynamicSlot, + DynamicFlag, + DynamicSlotType, + type IRFor, + type VaporDirectiveNode, +} from '../ir' import { findDir, resolveExpression } from '../utils' // TODO dynamic slots @@ -69,6 +77,9 @@ export const transformVSlot: NodeTransform = (node, context) => { context.dynamic.flags |= DynamicFlag.NON_TEMPLATE + const vFor = findDir(node, 'for') + const vIf = findDir(node, 'if') + const vElse = findDir(node, /^else(-if)?$/, true /* allowEmpty */) const slots = context.slots! const dynamicSlots = context.dynamicSlots! @@ -79,7 +90,7 @@ export const transformVSlot: NodeTransform = (node, context) => { arg &&= resolveExpression(arg) - if (!arg || arg.isStatic) { + if ((!arg || arg.isStatic) && !vFor && !vIf && !vElse) { const slotName = arg ? arg.content : 'default' if (slots[slotName]) { @@ -92,12 +103,75 @@ export const transformVSlot: NodeTransform = (node, context) => { } else { slots[slotName] = block } + } else if (vIf) { + dynamicSlots.push({ + slotType: DynamicSlotType.CONDITIONAL, + condition: vIf.exp!, + positive: { + slotType: DynamicSlotType.BASIC, + name: arg!, + fn: block, + key: 0, + }, + }) + } else if (vElse) { + const vIfIR = dynamicSlots[dynamicSlots.length - 1] + if (vIfIR.slotType === DynamicSlotType.CONDITIONAL) { + let ifNode = vIfIR + while ( + ifNode.negative && + ifNode.negative.slotType === DynamicSlotType.CONDITIONAL + ) + ifNode = ifNode.negative + const negative: + | ComponentBasicDynamicSlot + | ComponentConditionalDynamicSlot = vElse.exp + ? { + slotType: DynamicSlotType.CONDITIONAL, + condition: vElse.exp, + positive: { + slotType: DynamicSlotType.BASIC, + name: arg!, + fn: block, + key: ifNode.positive.key! + 1, + }, + } + : { + slotType: DynamicSlotType.BASIC, + name: arg!, + fn: block, + key: ifNode.positive.key! + 1, + } + ifNode.negative = negative + } else { + context.options.onError( + createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc), + ) + } + } else if (vFor) { + if (vFor.forParseResult) { + dynamicSlots.push({ + slotType: DynamicSlotType.LOOP, + name: arg!, + fn: block, + loop: vFor.forParseResult as IRFor, + }) + } else { + context.options.onError( + createCompilerError( + ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, + vFor.loc, + ), + ) + } } else { dynamicSlots.push({ - name: arg, + slotType: DynamicSlotType.BASIC, + name: arg!, fn: block, }) } + return () => onExit() } } diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index e487e14ac..fd7274c56 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -5,6 +5,7 @@ import { renderEffect } from './renderEffect' import { type Block, type Fragment, fragmentKey } from './apiRender' import { warn } from './warning' import { componentKey } from './component' +import type { DynamicSlot } from './componentSlots' interface ForBlock extends Fragment { scope: EffectScope @@ -301,44 +302,57 @@ export const createFor = ( remove(nodes, parent!) scope.stop() } +} - function getLength(source: any): number { - if (isArray(source) || isString(source)) { - return source.length - } else if (typeof source === 'number') { - if (__DEV__ && !Number.isInteger(source)) { - warn(`The v-for range expect an integer value but got ${source}.`) - } - return source - } else if (isObject(source)) { - if (source[Symbol.iterator as any]) { - return Array.from(source as Iterable).length - } else { - return Object.keys(source).length - } +export function createForSlots( + source: any[] | Record | number | Set | Map, + getSlot: (item: any, key: any, index?: number) => DynamicSlot, +): DynamicSlot[] { + const sourceLength = getLength(source) + const slots = new Array(sourceLength) + for (let i = 0; i < sourceLength; i++) { + const [item, key, index] = getItem(source, i) + slots[i] = getSlot(item, key, index) + } + return slots +} + +function getLength(source: any): number { + if (isArray(source) || isString(source)) { + return source.length + } else if (typeof source === 'number') { + if (__DEV__ && !Number.isInteger(source)) { + warn(`The v-for range expect an integer value but got ${source}.`) + } + return source + } else if (isObject(source)) { + if (source[Symbol.iterator as any]) { + return Array.from(source as Iterable).length + } else { + return Object.keys(source).length } - return 0 } + return 0 +} - function getItem( - source: any, - idx: number, - ): [item: any, key: any, index?: number] { - if (isArray(source) || isString(source)) { +function getItem( + source: any, + idx: number, +): [item: any, key: any, index?: number] { + if (isArray(source) || isString(source)) { + return [source[idx], idx, undefined] + } else if (typeof source === 'number') { + return [idx + 1, idx, undefined] + } else if (isObject(source)) { + if (source && source[Symbol.iterator as any]) { + source = Array.from(source as Iterable) return [source[idx], idx, undefined] - } else if (typeof source === 'number') { - return [idx + 1, idx, undefined] - } else if (isObject(source)) { - if (source && source[Symbol.iterator as any]) { - source = Array.from(source as Iterable) - return [source[idx], idx, undefined] - } else { - const key = Object.keys(source)[idx] - return [source[key], key, idx] - } + } else { + const key = Object.keys(source)[idx] + return [source[key], key, idx] } - return null! } + return null! } function normalizeAnchor(node: Block): Node { diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index b702ff526..6b0db6cd4 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -53,11 +53,12 @@ export function initSlots( slots = shallowReactive(slots) const dynamicSlotKeys: Record = {} firstEffect(instance, () => { - const _dynamicSlots = callWithAsyncErrorHandling( - dynamicSlots, - instance, - VaporErrorCodes.RENDER_FUNCTION, - ) + const _dynamicSlots: (DynamicSlot | DynamicSlot[])[] = + callWithAsyncErrorHandling( + dynamicSlots, + instance, + VaporErrorCodes.RENDER_FUNCTION, + ) for (let i = 0; i < _dynamicSlots.length; i++) { const slot = _dynamicSlots[i] // array of dynamic slot generated by