Skip to content

Commit

Permalink
feat(jsx-directive/v-slot): support dynamic argument (#648)
Browse files Browse the repository at this point in the history
* feat(jsx-directive/v-slot): support dynamic argument

* chore: add changeset

* docs: update
  • Loading branch information
zhiyuanzmj committed Mar 22, 2024
1 parent 486d64d commit a7d4d68
Show file tree
Hide file tree
Showing 17 changed files with 486 additions and 276 deletions.
6 changes: 6 additions & 0 deletions .changeset/stupid-flowers-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@vue-macros/jsx-directive": patch
"@vue-macros/volar": patch
---

support dynamic argument for v-slot
12 changes: 11 additions & 1 deletion docs/features/jsx-directive.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,24 @@ Vue built-in directives for JSX.

## Dynamic Arguments

It is possible to use a JavaScript expression in a directive argument by wrapping it with a pair of `$`:
It is also possible to use a JavaScript expression in a directive argument by wrapping it with a pair of `$`:

`v-model`

```tsx
<Comp v-model:$name$={value} />
```

`v-slot`

```tsx
<Comp>
<template v-for={(Slot, slotName) in slots} v-slot:$slotName$={scope}>
<Slot {...scope} />
</template>
</Comp>
```

## Modifiers

Modifiers are special postfixes denoted by a `_`, which indicate that a directive should be bound in some special way.
Expand Down
10 changes: 10 additions & 0 deletions docs/zh-CN/features/jsx-directive.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@
<Comp v-model:$name$={value} />
```

`v-slot`

```tsx
<Comp>
<template v-for={(Slot, slotName) in slots} v-slot:$slotName$={scope}>
<Slot {...scope} />
</template>
</Comp>
```

## 修饰符

修饰符是以 `_` 开头的特殊后缀,表明指令需要以一些特殊的方式被绑定。
Expand Down
42 changes: 23 additions & 19 deletions packages/jsx-directive/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,22 +114,24 @@ export function transformJsxDirective(
}
}

if (vIfAttribute && !(vSlotAttribute && tagName === 'template')) {
vIfMap.get(parent) || vIfMap.set(parent, [])
vIfMap.get(parent)!.push({
node,
attribute: vIfAttribute,
parent,
})
}
if (!(vSlotAttribute && tagName === 'template')) {
if (vIfAttribute) {
vIfMap.get(parent) || vIfMap.set(parent, [])
vIfMap.get(parent)!.push({
node,
attribute: vIfAttribute,
parent,
})
}

if (vForAttribute) {
vForNodes.push({
node,
attribute: vForAttribute,
parent: vIfAttribute ? undefined : parent,
vMemoAttribute,
})
if (vForAttribute) {
vForNodes.push({
node,
attribute: vForAttribute,
parent: vIfAttribute ? undefined : parent,
vMemoAttribute,
})
}
}

if (vMemoAttribute) {
Expand Down Expand Up @@ -159,10 +161,12 @@ export function transformJsxDirective(
attributeMap
.set(vSlotAttribute, {
children: [],
vIfAttribute:
tagName === 'template' && vIfAttribute
? vIfAttribute
: undefined,
...(tagName === 'template'
? {
vIfAttribute,
vForAttribute,
}
: {}),
})
.get(vSlotAttribute)!.children

Expand Down
61 changes: 43 additions & 18 deletions packages/jsx-directive/src/core/v-for.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@ import {
} from '@vue-macros/common'
import type { JsxDirective } from '.'

export function transformVFor(
nodes: JsxDirective[],
s: MagicString,
offset: number,
version: number,
export function resolveVFor(
attribute: JsxDirective['attribute'],
{
s,
offset,
version,
vMemoAttribute,
}: {
s: MagicString
offset: number
version: number
vMemoAttribute?: JsxDirective['attribute']
},
) {
if (nodes.length === 0) return
const renderList =
version < 3 ? 'Array.from' : importHelperFn(s, offset, 'renderList', 'vue')

nodes.forEach(({ node, attribute, parent, vMemoAttribute }) => {
if (!attribute.value) return

if (attribute.value) {
let item, index, objectIndex, list
if (
attribute.value.type === 'JSXExpressionContainer' &&
Expand All @@ -34,18 +36,41 @@ export function transformVFor(
item = s.sliceNode(attribute.value.expression.left, { offset })
}

if (vMemoAttribute) index ??= `${HELPER_PREFIX}index`

list = s.sliceNode(attribute.value.expression.right, { offset })
}
if (!item || !list) return

if (item && list) {
if (vMemoAttribute) {
index ??= `${HELPER_PREFIX}index`
}

const renderList =
version < 3
? 'Array.from'
: importHelperFn(s, offset, 'renderList', 'vue')

return `${renderList}(${list}, (${item}${
index ? `, ${index}` : ''
}${objectIndex ? `, ${objectIndex}` : ''}) => `
}
}

return ''
}

export function transformVFor(
nodes: JsxDirective[],
s: MagicString,
offset: number,
version: number,
) {
if (nodes.length === 0) return

nodes.forEach(({ node, attribute, parent, vMemoAttribute }) => {
const hasScope = ['JSXElement', 'JSXFragment'].includes(`${parent?.type}`)
s.appendLeft(
node.start! + offset,
`${hasScope ? '{' : ''}${renderList}(${list}, (${item}${
index ? `, ${index}` : ''
}${objectIndex ? `, ${objectIndex}` : ''}) => `,
`${hasScope ? '{' : ''}${resolveVFor(attribute, { s, offset, version, vMemoAttribute })}`,
)

const isTemplate =
Expand Down
133 changes: 79 additions & 54 deletions packages/jsx-directive/src/core/v-slot.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { MagicString } from '@vue-macros/common'
import { type MagicString, importHelperFn } from '@vue-macros/common'
import { resolveVFor } from './v-for'
import type { JSXAttribute, JSXElement, Node } from '@babel/types'

export type VSlotMap = Map<
Expand All @@ -10,6 +11,7 @@ export type VSlotMap = Map<
{
children: Node[]
vIfAttribute?: JSXAttribute
vForAttribute?: JSXAttribute
}
>
}
Expand All @@ -26,72 +28,95 @@ export function transformVSlot(
.forEach(([node, { attributeMap, vSlotAttribute }]) => {
const result = [` ${version < 3 ? 'scopedSlots' : 'v-slots'}={{`]
const attributes = Array.from(attributeMap)
attributes.forEach(([attribute, { children, vIfAttribute }], index) => {
if (!attribute) return
attributes.forEach(
([attribute, { children, vIfAttribute, vForAttribute }], index) => {
if (!attribute) return

if (vIfAttribute) {
if ('v-if' === vIfAttribute.name.name) {
result.push('...')
if (vIfAttribute) {
if ('v-if' === vIfAttribute.name.name) {
result.push('...')
}
if (
['v-if', 'v-else-if'].includes(`${vIfAttribute.name.name}`) &&
vIfAttribute.value?.type === 'JSXExpressionContainer'
) {
result.push(
`(${s.sliceNode(vIfAttribute.value.expression, {
offset,
})}) ? {`,
)
} else if ('v-else' === vIfAttribute.name.name) {
result.push('{')
}
}
if (
['v-if', 'v-else-if'].includes(`${vIfAttribute.name.name}`) &&
vIfAttribute.value?.type === 'JSXExpressionContainer'
) {

if (vForAttribute) {
result.push(
`(${s.sliceNode(vIfAttribute.value.expression, {
offset,
})}) ? {`,
'...Object.fromEntries(',
resolveVFor(vForAttribute, { s, offset, version }),
'([',
)
} else if ('v-else' === vIfAttribute.name.name) {
result.push('{')
}
}

result.push(
`'${
let isDynamic = false
let attributeName =
attribute.name.type === 'JSXNamespacedName'
? attribute.name.name.name
: 'default'
}': (`,
attribute.value && attribute.value.type === 'JSXExpressionContainer'
? s.sliceNode(attribute.value.expression, { offset })
: '',
`) => `,
version < 3 ? '<span>' : '<>',
children
.map((child) => {
const str = s.sliceNode(
child.type === 'JSXElement' &&
s.sliceNode(child.openingElement.name, { offset }) ===
'template'
? child.children
: child,
{ offset },
)
attributeName = attributeName.replace(/\$(.*)\$/, (_, $1) => {
isDynamic = true
return $1
})
result.push(
isDynamic
? `[${importHelperFn(s, offset, 'unref')}(${attributeName})]`
: `'${attributeName}'`,
vForAttribute ? ', ' : ': ',
'(',
attribute.value && attribute.value.type === 'JSXExpressionContainer'
? s.sliceNode(attribute.value.expression, { offset })
: '',
') => ',
version < 3 ? '<span>' : '<>',
children
.map((child) => {
const str = s.sliceNode(
child.type === 'JSXElement' &&
s.sliceNode(child.openingElement.name, { offset }) ===
'template'
? child.children
: child,
{ offset },
)

s.removeNode(child, { offset })
return str
})
.join('') || ' ',
version < 3 ? '</span>,' : '</>,',
)
s.removeNode(child, { offset })
return str
})
.join('') || ' ',
version < 3 ? '</span>,' : '</>,',
)

if (vIfAttribute) {
if (['v-if', 'v-else-if'].includes(`${vIfAttribute.name.name}`)) {
const nextIndex = index + (attributes[index + 1]?.[0] ? 1 : 2)
result.push(
'}',
`${attributes[nextIndex]?.[1].vIfAttribute?.name.name}`.startsWith(
'v-else',
if (vIfAttribute) {
if (['v-if', 'v-else-if'].includes(`${vIfAttribute.name.name}`)) {
const nextIndex = index + (attributes[index + 1]?.[0] ? 1 : 2)
result.push(
'}',
`${attributes[nextIndex]?.[1].vIfAttribute?.name.name}`.startsWith(
'v-else',
)
? ' : '
: ' : null,',
)
? ' : '
: ' : null,',
)
} else if ('v-else' === vIfAttribute.name.name) {
result.push('},')
} else if ('v-else' === vIfAttribute.name.name) {
result.push('},')
}
}

if (vForAttribute) {
result.push('])))')
}
}
})
},
)

if (attributeMap.has(null)) {
result.push(`default: () => ${version < 3 ? '<span>' : '<>'}`)
Expand Down

0 comments on commit a7d4d68

Please sign in to comment.