Skip to content

Commit

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

* chore: add changeset

* feat: add __VLS_CamelCase
  • Loading branch information
zhiyuanzmj committed Dec 27, 2023
1 parent b34d3be commit 7559395
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/flat-candles-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vue-macros/volar": patch
---

support dynamic argument for v-model
5 changes: 4 additions & 1 deletion packages/volar/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,13 @@ export function getModelsType(codes: Segment<FileRangeCapabilities>[]) {
getPropsType(codes)

codes.push(`
type __VLS_CamelCase<S extends string> = S extends \`\${infer F}-\${infer RF}\${infer R}\`
? \`\${F}\${Uppercase<RF>}\${__VLS_CamelCase<R>}\`
: S;
type __VLS_RemoveUpdatePrefix<T> = T extends \`update:modelValue\`
? never
: T extends \`update:\${infer R}\`
? \`\${R}\`
? __VLS_CamelCase<R>
: T;
type __VLS_getModels<T> = T extends object
? {
Expand Down
72 changes: 49 additions & 23 deletions packages/volar/src/jsx-directive/v-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,49 +14,75 @@ export function transformVModel({
sfc,
source,
}: TransformOptions & { nodes: JsxDirective[] }) {
let firstNamespacedNode:
| (JsxDirective & { attributeName: string })
| undefined
let firstNamespacedNode: JsxDirective | undefined
const result: Segment<FileRangeCapabilities>[] = []
for (const { attribute, node } of nodes) {
if (attribute.name.getText(sfc[source]?.ast).startsWith('v-model:')) {
const isArrayExpression =
attribute.initializer &&
ts.isJsxExpression(attribute.initializer) &&
attribute.initializer.expression &&
ts.isArrayLiteralExpression(attribute.initializer.expression)

if (
attribute.name.getText(sfc[source]?.ast).startsWith('v-model:') ||
isArrayExpression
) {
const attributeName = camelize(
attribute.name
.getText(sfc[source]?.ast)
.slice(8)
.split(' ')[0]
.split('_')[0],
)
firstNamespacedNode ??= { attribute, node, attributeName }
firstNamespacedNode ??= { attribute, node }
if (firstNamespacedNode.attribute !== attribute) {
replaceSourceRange(
codes,
source,
attribute.getStart(sfc[source]?.ast),
attribute.getEnd(),
`onUpdate:${attributeName}={() => {}}`,
)
result.push(',')
}

result.push(
firstNamespacedNode.attribute !== attribute ? ',' : '',
[
if (isArrayExpression) {
const { elements } = attribute.initializer.expression
if (elements[1] && !ts.isArrayLiteralExpression(elements[1])) {
if (!ts.isStringLiteral(elements[1])) result.push('[`${')
result.push([
elements[1].getText(sfc[source]?.ast),
source,
elements[1].getStart(sfc[source]?.ast),
FileRangeCapabilities.full,
])
if (!ts.isStringLiteral(elements[1])) result.push('}`]')
} else {
result.push('modelValue')
}

if (elements[0])
result.push([
`:${elements[0].getText(sfc[source]?.ast)}`,
source,
elements[0].getStart(sfc[source]?.ast),
FileRangeCapabilities.full,
])
} else {
result.push([
attributeName,
source,
[attribute.name.getStart(sfc[source]?.ast) + 8, attribute.name.end],
FileRangeCapabilities.full,
],
attribute.initializer && attributeName
? [
`:${attribute.initializer
.getText(sfc[source]?.ast)
.slice(1, -1)}`,
source,
attribute.initializer.getStart(sfc[source]?.ast),
FileRangeCapabilities.full,
]
: '',
)
])

if (attribute.initializer && attributeName)
result.push([
`:${attribute.initializer.getText(sfc[source]?.ast).slice(1, -1)}`,
source,
attribute.initializer.getStart(sfc[source]?.ast),
FileRangeCapabilities.full,
])
}
} else {
replaceSourceRange(
codes,
Expand All @@ -76,7 +102,7 @@ export function transformVModel({
}

if (!firstNamespacedNode) return
const { node, attribute, attributeName } = firstNamespacedNode
const { node, attribute } = firstNamespacedNode
getModelsType(codes)

const tagName = ts.isJsxSelfClosingElement(node)
Expand All @@ -89,7 +115,7 @@ export function transformVModel({
source,
attribute.getStart(sfc[source]?.ast),
attribute.getEnd(),
`onUpdate:${attributeName}={() => {}} {...{`,
`{...{`,
...result,
`} satisfies __VLS_getModels<typeof ${tagName}>}`,
)
Expand Down
4 changes: 2 additions & 2 deletions playground/vue3/src/examples/jsx-directive/v-model/child.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ defineProps<{
defineModels<{
modelValue: number
title: number
title?: number
bottom: number
}>()
const value = $ref('')
defineRender(() => (
<>
<slots.title v-model:value_trim={value}></slots.title>
<slots.title v-model:value_trim={value} onUpdate:value={() => {}} />
<slots.default value={value}></slots.default>
</>
Expand Down
10 changes: 8 additions & 2 deletions playground/vue3/src/examples/jsx-directive/v-model/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import Child from './child.vue'
const foo = $ref(1)
const bar = $ref('')
const baz = $computed(() => (foo === 0 ? 'title' : 'bottom'))
defineRender(() => (
<Child bar={bar} v-model:title={foo} v-model:bottom={foo} v-model={foo}>
<Child
bar={bar}
v-model={[foo, foo === 1 ? 'bottom' : 'title']}
v-model={[foo, baz]}
v-model:bottom={foo}
v-model={foo}
>
<template v-slot:title={{ value, ...emits }}>
<input
value={value}
Expand Down

0 comments on commit 7559395

Please sign in to comment.