Skip to content

Commit

Permalink
feat(jsx-directive): add v-slot directive (#463)
Browse files Browse the repository at this point in the history
* feat: add v-slot directive

* feat: add volar plugin

* test: add

* docs: add v-slot

* chore: typo

* docs: typo

* chore: add changeset
  • Loading branch information
zhiyuanzmj committed Aug 18, 2023
1 parent eaf04d5 commit 8ad8454
Show file tree
Hide file tree
Showing 16 changed files with 477 additions and 22 deletions.
6 changes: 6 additions & 0 deletions .changeset/afraid-turkeys-grab.md
@@ -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
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
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
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
)
})
})

0 comments on commit 8ad8454

Please sign in to comment.