Skip to content

Commit

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

* chore: add changeset
  • Loading branch information
zhiyuanzmj committed Mar 19, 2024
1 parent b76b938 commit 3e790d5
Show file tree
Hide file tree
Showing 12 changed files with 268 additions and 139 deletions.
6 changes: 6 additions & 0 deletions .changeset/brave-steaks-collect.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-model
85 changes: 54 additions & 31 deletions docs/features/jsx-directive.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,67 @@ Vue built-in directives for JSX.
| `v-once` | :white_check_mark: | :x: | / |
| `v-memo` | :white_check_mark: | :x: | / |

## Usage

### `v-on`

::: warning

`v-on` only supports binding to an object of event / listener pairs without an argument.

:::

## Usage
```tsx
<form v-on={{ submit }} />
```

### `v-if`, `v-else-if`, `v-else`

```tsx
<div v-if={foo === 0}>
<div v-if={foo === 0}>0-0</div>
<div v-else-if={foo === 1}>0-1</div>
<div v-else>0-2</div>
</div>
```

### `v-for`, `v-memo`

```tsx
<div v-for={(item, index) in list} key={index} v-memo={[foo === item]}>
{item}
</div>
```

### `v-slot`

```tsx
<Child>
default slot
<template v-slot:bottom={{ bar }}>
<span>{bar}</span>
</template>
</Child>
```

## Dynamic Arguments

It is 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} />
```

## Modifiers

Modifiers are special postfixes denoted by a `_`, which indicate that a directive should be bound in some special way.

```vue
<script setup lang="tsx">
import Child from './Child.vue'
const { foo, list } = defineProps<{
foo: number
list: number[]
}>()
defineRender(() => (
<form onSubmit_prevent>
<div v-if={foo === 0}>
<div v-if={foo === 0}>0-0</div>
<div v-else-if={foo === 1}>0-1</div>
<div v-else>0-2</div>
</div>
<div v-for={(i, index) in list} v-memo={[foo === i]} key={index}>
{i}
</div>
<Child v-on={{ submit: () => {} }}>
default slot
<template v-slot:bottom={{ bar }}>
<span>{bar}</span>
</template>
</Child>
</form>
))
</script>
```tsx
<form onSubmit_prevent>
<input v-model_number={value} />
</form>
```

## Volar Configuration
Expand Down
85 changes: 54 additions & 31 deletions docs/zh-CN/features/jsx-directive.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,67 @@
| `v-once` | :white_check_mark: | :x: | / |
| `v-memo` | :white_check_mark: | :x: | / |

## 用法

### `v-on`

::: warning

`v-on` 仅支持绑定不带参数的事件/监听器对的对象。

:::

## 用法
```tsx
<form v-on={{ submit }} />
```

### `v-if`, `v-else-if`, `v-else`

```tsx
<div v-if={foo === 0}>
<div v-if={foo === 0}>0-0</div>
<div v-else-if={foo === 1}>0-1</div>
<div v-else>0-2</div>
</div>
```

### `v-for`, `v-memo`

```tsx
<div v-for={(item, index) in list} key={index} v-memo={[foo === item]}>
{item}
</div>
```

### `v-slot`

```tsx
<Child>
default slot
<template v-slot:bottom={{ bar }}>
<span>{bar}</span>
</template>
</Child>
```

## 动态参数

在指令参数上也可以使用一个 JavaScript 表达式,需要包含在一对 `$` 内:

`v-model`

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

## 修饰符

修饰符是以 `_` 开头的特殊后缀,表明指令需要以一些特殊的方式被绑定。

```vue
<script setup lang="tsx">
import Child from './Child.vue'
const { foo, list } = defineProps<{
foo: number
list: number[]
}>()
defineRender(() => (
<form onSubmit_prevent>
<div v-if={foo === 0}>
<div v-if={foo === 0}>0-0</div>
<div v-else-if={foo === 1}>0-1</div>
<div v-else>0-2</div>
</div>
<div v-for={(i, index) in list} v-memo={[foo === i]} key={index}>
{i}
</div>
<Child v-on={{ submit: () => {} }}>
default slot
<template v-slot:bottom={{ bar }}>
<span>{bar}</span>
</template>
</Child>
</form>
))
</script>
```tsx
<form onSubmit_prevent>
<input v-model_number={value} />
</form>
```

## Volar 配置
Expand Down
30 changes: 12 additions & 18 deletions packages/jsx-directive/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { transformVIf } from './v-if'
import { transformVFor } from './v-for'
import { transformVMemo } from './v-memo'
import { transformVHtml } from './v-html'
import { transformVModel } from './v-model'
import { type VSlotMap, transformVSlot } from './v-slot'
import { transformVOn, transformVOnWithModifiers } from './v-on'
import type { JSXAttribute, JSXElement, Node, Program } from '@babel/types'
Expand Down Expand Up @@ -79,44 +80,37 @@ export function transformJsxDirective(
['v-if', 'v-else-if', 'v-else'].includes(`${attribute.name.name}`)
) {
vIfAttribute = attribute
}

if (attribute.name.name === 'v-for') {
} else if (attribute.name.name === 'v-for') {
vForAttribute = attribute
}

if (['v-memo', 'v-once'].includes(`${attribute.name.name}`)) {
} else if (['v-memo', 'v-once'].includes(`${attribute.name.name}`)) {
vMemoAttribute = attribute
}

if (attribute.name.name === 'v-html') {
} else if (attribute.name.name === 'v-html') {
vHtmlNodes.push({
node,
attribute,
})
}

if (
} else if (
(attribute.name.type === 'JSXNamespacedName'
? attribute.name.namespace
: attribute.name
).name === 'v-slot'
) {
vSlotAttribute = attribute
}

if (attribute.name.name === 'v-on') {
} else if (attribute.name.name === 'v-on') {
vOnNodes.push({
node,
attribute,
})
}

if (/^on[A-Z]\S*_\S+/.test(`${attribute.name.name}`)) {
} else if (/^on[A-Z]\S*_\S+/.test(`${attribute.name.name}`)) {
vOnWithModifiers.push({
node,
attribute,
})
} else if (
attribute.name.type === 'JSXNamespacedName' &&
attribute.name.namespace.name === 'v-model'
) {
transformVModel(attribute, s, offset)
}
}

Expand Down
31 changes: 31 additions & 0 deletions packages/jsx-directive/src/core/v-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { type MagicString, importHelperFn } from '@vue-macros/common'
import type { JSXAttribute } from '@babel/types'

export function transformVModel(
attribute: JSXAttribute,
s: MagicString,
offset: number,
) {
if (
attribute.name.type === 'JSXNamespacedName' &&
attribute.value?.type === 'JSXExpressionContainer'
) {
const matched = attribute.name.name.name.match(/^\$(.*)\$(?:_(.*))?/)
if (!matched) return

let [, argument, modifiers] = matched
const value = s.sliceNode(attribute.value.expression, { offset })
argument = `${importHelperFn(s, offset, 'unref')}(${argument})`
modifiers = modifiers
? `, [${argument} + "Modifiers"]: { ${modifiers
.split('_')
.map((key) => `${key}: true`)
.join(', ')} }`
: ''
s.overwriteNode(
attribute,
`{...{[${argument}]: ${value}, ["onUpdate:" + ${argument}]: $event => ${value} = $event${modifiers}}}`,
{ offset },
)
}
}
18 changes: 18 additions & 0 deletions packages/jsx-directive/tests/__snapshots__/v-model.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`jsx-vue-directive > v-model > ./fixtures/v-model/index.vue 1`] = `
"<script setup lang="tsx">
import { unref as __MACROS_unref } from "vue";
import { ref } from 'vue'
let foo = ref('')
const value = ref('value')
defineRender(() => (
<div>
<input {...{[__MACROS_unref(value)]: foo, ["onUpdate:" + __MACROS_unref(value)]: $event => foo = $event, [__MACROS_unref(value) + "Modifiers"]: { trim: true, number: true }}} />
{foo}
</div>
))
</script>
"
`;
12 changes: 12 additions & 0 deletions packages/jsx-directive/tests/fixtures/v-model/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script setup lang="tsx">
import { ref } from 'vue'
let foo = ref('')
const value = ref('value')
defineRender(() => (
<div>
<input v-model:$value$_trim_number={foo} />
{foo}
</div>
))
</script>
15 changes: 15 additions & 0 deletions packages/jsx-directive/tests/v-model.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { describe } from 'vitest'
import { testFixtures } from '@vue-macros/test-utils'
import { transformJsxDirective } from '../src/api'

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

0 comments on commit 3e790d5

Please sign in to comment.