Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/afraid-turkeys-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@vue-macros/jsx-directive': minor
'@vue-macros/volar': patch
---

Add v-slot directive.
5 changes: 5 additions & 0 deletions docs/features/jsx-directive.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ Vue built-in directives for JSX.
| v-once | :white_check_mark: | :x: | |
| v-memo | :white_check_mark: | :x: | |
| v-html | :white_check_mark: | :white_check_mark: | |
| v-slot | :white_check_mark: | :white_check_mark: | :white_check_mark: |

## Usage

```vue
<script setup lang="tsx">
import Child from './Child.vue'

const { foo, list } = defineProps<{
foo: number
list: number[]
Expand All @@ -34,6 +37,8 @@ defineRender(() => (
<div v-for={(i, index) in list} v-memo={[foo === i]} key={index}>
{i}
</div>

<Child v-slot={props}>{props}</Child>
</>
))
</script>
Expand Down
5 changes: 5 additions & 0 deletions docs/zh-CN/features/jsx-directive.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@
| v-once | :white_check_mark: | :x: | |
| v-memo | :white_check_mark: | :x: | |
| v-html | :white_check_mark: | :white_check_mark: | |
| v-slot | :white_check_mark: | :white_check_mark: | :white_check_mark: |

## Usage

```vue
<script setup lang="tsx">
import Child from './Child.vue'

const { foo, list } = defineProps<{
foo: number
list: number[]
Expand All @@ -34,6 +37,8 @@ defineRender(() => (
<div v-for={(i, index) in list} v-memo={[foo === i]} key={index}>
{i}
</div>

<Child v-slot={props}>{props}</Child>
</>
))
</script>
Expand Down
33 changes: 24 additions & 9 deletions packages/jsx-directive/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { transformVIf } from './v-if'
import { transformVFor } from './v-for'
import { transformVMemo } from './v-memo'
import { transformVHtml } from './v-html'
import { transformVSlot } from './v-slot'

export type JsxDirectiveNode = {
node: JSXElement
Expand Down Expand Up @@ -53,7 +54,7 @@ export function transformJsxDirective(

const s = new MagicString(code)
for (const { ast, offset } of asts) {
if (!/\sv-(if|for|memo|once|html)/.test(s.sliceNode(ast, { offset })))
if (!/\sv-(if|for|memo|once|html|slot)/.test(s.sliceNode(ast, { offset })))
continue

const vIfMap = new Map<Node, JsxDirectiveNode[]>()
Expand All @@ -62,14 +63,14 @@ export function transformJsxDirective(
vForAttribute?: JSXAttribute
})[] = []
const vHtmlNodes: JsxDirectiveNode[] = []
const vSlotSet = new Set<JSXElement>()
walkAST<Node>(ast, {
enter(node, parent) {
if (node.type !== 'JSXElement') return

let vIfAttribute
let vForAttribute
let vMemoAttribute
let vHtmlAttribute
for (const attribute of node.openingElement.attributes) {
if (attribute.type !== 'JSXAttribute') continue
if (
Expand All @@ -79,7 +80,26 @@ export function transformJsxDirective(
if (attribute.name.name === 'v-for') vForAttribute = attribute
if (['v-memo', 'v-once'].includes(`${attribute.name.name}`))
vMemoAttribute = attribute
if (attribute.name.name === 'v-html') vHtmlAttribute = attribute
if (attribute.name.name === 'v-html') {
vHtmlNodes.push({
node,
attribute,
})
}
if (
(attribute.name.type === 'JSXNamespacedName'
? attribute.name.namespace
: attribute.name
).name === 'v-slot'
) {
vSlotSet.add(
node.openingElement.name.type === 'JSXIdentifier' &&
node.openingElement.name.name === 'template' &&
parent?.type === 'JSXElement'
? parent
: node
)
}
}

if (vIfAttribute) {
Expand All @@ -105,19 +125,14 @@ export function transformJsxDirective(
vForAttribute,
})
}
if (vHtmlAttribute) {
vHtmlNodes.push({
node,
attribute: vHtmlAttribute,
})
}
},
})

vIfMap.forEach((nodes) => transformVIf(nodes, s, offset))
transformVFor(vForNodes, s, offset)
version >= 3.2 && transformVMemo(vMemoNodes, s, offset)
transformVHtml(vHtmlNodes, s, offset, version)
transformVSlot(Array.from(vSlotSet), s, offset, version)
}

return generateTransform(s, id)
Expand Down
112 changes: 112 additions & 0 deletions packages/jsx-directive/src/core/v-slot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { type MagicString } from '@vue-macros/common'
import { type JSXElement } from '@babel/types'

export function transformVSlot(
nodes: JSXElement[],
s: MagicString,
offset = 0,
version: number
) {
nodes.reverse().forEach((node) => {
const attribute = node.openingElement.attributes.find(
(attribute) =>
attribute.type === 'JSXAttribute' &&
(attribute.name.type === 'JSXNamespacedName'
? attribute.name.namespace
: attribute.name
).name === 'v-slot'
)

const slots =
attribute?.type === 'JSXAttribute'
? {
[`${
attribute.name.type === 'JSXNamespacedName'
? attribute.name.name.name
: 'default'
}`]: {
isTemplateTag: false,
expressionContainer: attribute.value,
children: node.children,
},
}
: {}
if (!attribute) {
for (const child of node.children) {
let name = 'default'
let expressionContainer
const isTemplateTag =
child.type === 'JSXElement' &&
child.openingElement.name.type === 'JSXIdentifier' &&
child.openingElement.name.name === 'template'

if (child.type === 'JSXElement') {
for (const attr of child.openingElement.attributes) {
if (attr.type !== 'JSXAttribute') continue
if (isTemplateTag) {
name =
attr.name.type === 'JSXNamespacedName'
? attr.name.name.name
: 'default'
}

if (
(attr.name.type === 'JSXNamespacedName'
? attr.name.namespace
: attr.name
).name === 'v-slot'
)
expressionContainer = attr.value
}
}

slots[name] ??= {
isTemplateTag,
expressionContainer,
children: [child],
}
if (!slots[name].isTemplateTag) {
slots[name].expressionContainer = expressionContainer
slots[name].isTemplateTag = isTemplateTag
if (isTemplateTag) {
slots[name].children = [child]
} else {
slots[name].children.push(child)
}
}
}
}

const result = `${
version < 3 ? 'scopedSlots' : 'v-slots'
}={{${Object.entries(slots)
.map(
([name, { expressionContainer, children }]) =>
`'${name}': (${
expressionContainer?.type === 'JSXExpressionContainer'
? s.sliceNode(expressionContainer.expression, { offset })
: ''
}) => ${version < 3 ? '<span>' : '<>'}${children
.map((child) => {
const result = s.sliceNode(
child.type === 'JSXElement' &&
child.openingElement.name.type === 'JSXIdentifier' &&
child.openingElement.name.name === 'template'
? child.children
: child,
{ offset }
)
s.removeNode(child, { offset })
return result
})
.join('')}${version < 3 ? '</span>' : '</>'}`
)
.join(',')}}}`

if (attribute) {
s.overwriteNode(attribute, result, { offset })
} else {
s.appendLeft(node.openingElement.end! + offset - 1, ` ${result}`)
}
})
}
33 changes: 33 additions & 0 deletions packages/jsx-directive/tests/__snapshots__/v-slot.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`jsx-vue-directive > vue 2.7 v-slot > ./fixtures/v-slot/index.vue 1`] = `
"<script setup lang=\\"tsx\\">
import Child from './child.vue'

defineRender(() => (
<div>
<Child scopedSlots={{'bottom': ({ foo }) => <span>
{foo}
<Child scopedSlots={{'default': () => <span>default</span>}}></Child>
</span>}}></Child>
</div>
))
</script>
"
`;

exports[`jsx-vue-directive > vue 3 v-slot > ./fixtures/v-slot/index.vue 1`] = `
"<script setup lang=\\"tsx\\">
import Child from './child.vue'

defineRender(() => (
<div>
<Child v-slots={{'bottom': ({ foo }) => <>
{foo}
<Child v-slots={{'default': () => <>default</>}}></Child>
</>}}></Child>
</div>
))
</script>
"
`;
13 changes: 13 additions & 0 deletions packages/jsx-directive/tests/fixtures/v-slot/child.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script setup lang="tsx">
defineSlots<{
default: () => any
bottom: (props: { foo: 1 }) => any
}>()
</script>

<template>
<span>
<slot />
<slot name="bottom" v-bind="{ foo: 1 }" />
</span>
</template>
12 changes: 12 additions & 0 deletions packages/jsx-directive/tests/fixtures/v-slot/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script setup lang="tsx">
import Child from './child.vue'

defineRender(() => (
<div>
<Child v-slot:bottom={{ foo }}>
{foo}
<Child v-slot>default</Child>
</Child>
</div>
))
</script>
25 changes: 25 additions & 0 deletions packages/jsx-directive/tests/v-slot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe } from 'vitest'
import { testFixtures } from '@vue-macros/test-utils'
import { transformJsxDirective } from '../src/api'

describe('jsx-vue-directive', () => {
describe('vue 3 v-slot', async () => {
await testFixtures(
import.meta.glob('./fixtures/v-slot/index.{vue,jsx,tsx}', {
eager: true,
as: 'raw',
}),
(_, id, code) => transformJsxDirective(code, id, 3)?.code
)
})

describe('vue 2.7 v-slot', async () => {
await testFixtures(
import.meta.glob('./fixtures/v-slot/index.{vue,jsx,tsx}', {
eager: true,
as: 'raw',
}),
(_, id, code) => transformJsxDirective(code, id, 2.7)?.code
)
})
})
Loading