Просмотр исходного кода

feat(compiler/runtime-vapor): implement v-slots + v-for / v-if (#207)

Co-authored-by: Rizumu Ayaka <rizumu@ayaka.moe>
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
Doctor Wu 2 лет назад
Родитель
Сommit
4e13a57d9c

+ 72 - 0
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("<div></div>")

+ 107 - 0
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(
+      `<Comp>
+        <template v-for="item in list" #[item]>foo</template>
+      </Comp>`,
+    )
+    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(
+      `<Comp>
+        <template v-for="(,,index) in list" #[index]>foo</template>
+      </Comp>`,
+    )
+    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(
+      `<Comp>
+        <template v-if="condition" #condition>condition slot</template>
+        <template v-else-if="anotherCondition" #condition>another condition</template>
+        <template v-else #condition>else condition</template>
+      </Comp>`,
+    )
+    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()

+ 90 - 7
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<string, string> = {}
+  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,
+  ]
+}

+ 35 - 5
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<string, ComponentSlotBlockIRNode>
-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

+ 77 - 3
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()
   }
 }

+ 45 - 31
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<any>).length
-      } else {
-        return Object.keys(source).length
-      }
+export function createForSlots(
+  source: any[] | Record<any, any> | number | Set<any> | Map<any, any>,
+  getSlot: (item: any, key: any, index?: number) => DynamicSlot,
+): DynamicSlot[] {
+  const sourceLength = getLength(source)
+  const slots = new Array<DynamicSlot>(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<any>).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<any>)
       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<any>)
-        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 {

+ 6 - 5
packages/runtime-vapor/src/componentSlots.ts

@@ -53,11 +53,12 @@ export function initSlots(
     slots = shallowReactive(slots)
     const dynamicSlotKeys: Record<string, true> = {}
     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 <template v-for="..." #[...]>

+ 1 - 1
packages/runtime-vapor/src/index.ts

@@ -126,7 +126,7 @@ export {
   type FunctionPlugin,
 } from './apiCreateVaporApp'
 export { createIf } from './apiCreateIf'
-export { createFor } from './apiCreateFor'
+export { createFor, createForSlots } from './apiCreateFor'
 export { createComponent } from './apiCreateComponent'
 
 export { resolveComponent, resolveDirective } from './helpers/resolveAssets'