Skip to content

Commit a772031

Browse files
committed
feat(defineModel): support modifiers and transformers
1 parent d7bb32f commit a772031

File tree

7 files changed

+303
-107
lines changed

7 files changed

+303
-107
lines changed

packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineModel.spec.ts.snap

+59-32
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ exports[`defineModel() > basic usage 1`] = `
66
export default {
77
props: {
88
"modelValue": { required: true },
9+
"modelModifiers": {},
910
"count": {},
11+
"countModifiers": {},
1012
"toString": { type: Function },
13+
"toStringModifiers": {},
1114
},
1215
emits: ["update:modelValue", "update:count", "update:toString"],
1316
setup(__props, { expose: __expose }) {
@@ -23,12 +26,58 @@ return { modelValue, c, toString }
2326
}"
2427
`;
2528

29+
exports[`defineModel() > get / set transformers 1`] = `
30+
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
31+
32+
export default /*#__PURE__*/_defineComponent({
33+
props: {
34+
"modelValue": {
35+
required: true
36+
},
37+
"modelModifiers": {},
38+
},
39+
emits: ["update:modelValue"],
40+
setup(__props, { expose: __expose }) {
41+
__expose();
42+
43+
const modelValue = _useModel(__props, "modelValue", { get(v) { return v - 1 }, set: (v) => { return v + 1 }, })
44+
45+
return { modelValue }
46+
}
47+
48+
})"
49+
`;
50+
51+
exports[`defineModel() > get / set transformers 2`] = `
52+
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
53+
54+
export default /*#__PURE__*/_defineComponent({
55+
props: {
56+
"modelValue": {
57+
default: 0,
58+
required: true,
59+
},
60+
"modelModifiers": {},
61+
},
62+
emits: ["update:modelValue"],
63+
setup(__props, { expose: __expose }) {
64+
__expose();
65+
66+
const modelValue = _useModel(__props, "modelValue", { get(v) { return v - 1 }, set: (v) => { return v + 1 }, })
67+
68+
return { modelValue }
69+
}
70+
71+
})"
72+
`;
73+
2674
exports[`defineModel() > w/ array props 1`] = `
2775
"import { useModel as _useModel, mergeModels as _mergeModels } from 'vue'
2876
2977
export default {
3078
props: /*#__PURE__*/_mergeModels(['foo', 'bar'], {
3179
"count": {},
80+
"countModifiers": {},
3281
}),
3382
emits: ["update:count"],
3483
setup(__props, { expose: __expose }) {
@@ -49,6 +98,7 @@ exports[`defineModel() > w/ defineProps and defineEmits 1`] = `
4998
export default {
5099
props: /*#__PURE__*/_mergeModels({ foo: String }, {
51100
"modelValue": { default: 0 },
101+
"modelModifiers": {},
52102
}),
53103
emits: /*#__PURE__*/_mergeModels(['change'], ["update:modelValue"]),
54104
setup(__props, { expose: __expose }) {
@@ -64,47 +114,19 @@ return { count }
64114
}"
65115
`;
66116

67-
exports[`defineModel() > w/ local flag 1`] = `
68-
"import { useModel as _useModel } from 'vue'
69-
const local = true
70-
71-
export default {
72-
props: {
73-
"modelValue": { local: true, default: 1 },
74-
"bar": { [key]: true },
75-
"baz": { ...x },
76-
"qux": x,
77-
"foo2": { local: true, ...x },
78-
"hoist": { local },
79-
},
80-
emits: ["update:modelValue", "update:bar", "update:baz", "update:qux", "update:foo2", "update:hoist"],
81-
setup(__props, { expose: __expose }) {
82-
__expose();
83-
84-
const foo = _useModel(__props, "modelValue", { local: true })
85-
const bar = _useModel(__props, "bar", { [key]: true })
86-
const baz = _useModel(__props, "baz", { ...x })
87-
const qux = _useModel(__props, "qux", x)
88-
89-
const foo2 = _useModel(__props, "foo2", { local: true })
90-
91-
const hoist = _useModel(__props, "hoist", { local })
92-
93-
return { foo, bar, baz, qux, foo2, local, hoist }
94-
}
95-
96-
}"
97-
`;
98-
99117
exports[`defineModel() > w/ types, basic usage 1`] = `
100118
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
101119
102120
export default /*#__PURE__*/_defineComponent({
103121
props: {
104122
"modelValue": { type: [Boolean, String] },
123+
"modelModifiers": {},
105124
"count": { type: Number },
125+
"countModifiers": {},
106126
"disabled": { type: Number, ...{ required: false } },
127+
"disabledModifiers": {},
107128
"any": { type: Boolean, skipCheck: true },
129+
"anyModifiers": {},
108130
},
109131
emits: ["update:modelValue", "update:count", "update:disabled", "update:any"],
110132
setup(__props, { expose: __expose }) {
@@ -127,10 +149,15 @@ exports[`defineModel() > w/ types, production mode 1`] = `
127149
export default /*#__PURE__*/_defineComponent({
128150
props: {
129151
"modelValue": { type: Boolean },
152+
"modelModifiers": {},
130153
"fn": {},
154+
"fnModifiers": {},
131155
"fnWithDefault": { type: Function, ...{ default: () => null } },
156+
"fnWithDefaultModifiers": {},
132157
"str": {},
158+
"strModifiers": {},
133159
"optional": { required: false },
160+
"optionalModifiers": {},
134161
},
135162
emits: ["update:modelValue", "update:fn", "update:fnWithDefault", "update:str", "update:optional"],
136163
setup(__props, { expose: __expose }) {

packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts

+41-23
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ describe('defineModel()', () => {
6969
assertCode(content)
7070
expect(content).toMatch(`props: /*#__PURE__*/_mergeModels(['foo', 'bar'], {
7171
"count": {},
72+
"countModifiers": {},
7273
})`)
7374
expect(content).toMatch(`const count = _useModel(__props, "count")`)
7475
expect(content).not.toMatch('defineModel')
@@ -79,29 +80,6 @@ describe('defineModel()', () => {
7980
})
8081
})
8182

82-
test('w/ local flag', () => {
83-
const { content } = compile(
84-
`<script setup>
85-
const foo = defineModel({ local: true, default: 1 })
86-
const bar = defineModel('bar', { [key]: true })
87-
const baz = defineModel('baz', { ...x })
88-
const qux = defineModel('qux', x)
89-
90-
const foo2 = defineModel('foo2', { local: true, ...x })
91-
92-
const local = true
93-
const hoist = defineModel('hoist', { local })
94-
</script>`,
95-
)
96-
assertCode(content)
97-
expect(content).toMatch(`_useModel(__props, "modelValue", { local: true })`)
98-
expect(content).toMatch(`_useModel(__props, "bar", { [key]: true })`)
99-
expect(content).toMatch(`_useModel(__props, "baz", { ...x })`)
100-
expect(content).toMatch(`_useModel(__props, "qux", x)`)
101-
expect(content).toMatch(`_useModel(__props, "foo2", { local: true })`)
102-
expect(content).toMatch(`_useModel(__props, "hoist", { local })`)
103-
})
104-
10583
test('w/ types, basic usage', () => {
10684
const { content, bindings } = compile(
10785
`
@@ -115,6 +93,7 @@ describe('defineModel()', () => {
11593
)
11694
assertCode(content)
11795
expect(content).toMatch('"modelValue": { type: [Boolean, String] }')
96+
expect(content).toMatch('"modelModifiers": {}')
11897
expect(content).toMatch('"count": { type: Number }')
11998
expect(content).toMatch(
12099
'"disabled": { type: Number, ...{ required: false } }',
@@ -176,4 +155,43 @@ describe('defineModel()', () => {
176155
optional: BindingTypes.SETUP_REF,
177156
})
178157
})
158+
159+
test('get / set transformers', () => {
160+
const { content } = compile(
161+
`
162+
<script setup lang="ts">
163+
const modelValue = defineModel({
164+
get(v) { return v - 1 },
165+
set: (v) => { return v + 1 },
166+
required: true
167+
})
168+
</script>
169+
`,
170+
)
171+
assertCode(content)
172+
expect(content).toMatch(/"modelValue": {\s+required: true,?\s+}/m)
173+
expect(content).toMatch(
174+
`_useModel(__props, "modelValue", { get(v) { return v - 1 }, set: (v) => { return v + 1 }, })`,
175+
)
176+
177+
const { content: content2 } = compile(
178+
`
179+
<script setup lang="ts">
180+
const modelValue = defineModel({
181+
default: 0,
182+
get(v) { return v - 1 },
183+
required: true,
184+
set: (v) => { return v + 1 },
185+
})
186+
</script>
187+
`,
188+
)
189+
assertCode(content2)
190+
expect(content2).toMatch(
191+
/"modelValue": {\s+default: 0,\s+required: true,?\s+}/m,
192+
)
193+
expect(content2).toMatch(
194+
`_useModel(__props, "modelValue", { get(v) { return v - 1 }, set: (v) => { return v + 1 }, })`,
195+
)
196+
})
179197
})

packages/compiler-sfc/src/script/defineModel.ts

+43-27
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { LVal, Node, ObjectProperty, TSType } from '@babel/types'
1+
import type { LVal, Node, TSType } from '@babel/types'
22
import type { ScriptCompileContext } from './context'
33
import { inferRuntimeType } from './resolveType'
44
import {
@@ -45,42 +45,52 @@ export function processDefineModel(
4545
ctx.error(`duplicate model name ${JSON.stringify(modelName)}`, node)
4646
}
4747

48-
const optionsString = options && ctx.getString(options)
49-
50-
ctx.modelDecls[modelName] = {
51-
type,
52-
options: optionsString,
53-
identifier:
54-
declId && declId.type === 'Identifier' ? declId.name : undefined,
55-
}
56-
// register binding type
57-
ctx.bindingMetadata[modelName] = BindingTypes.PROPS
58-
48+
let optionsString = options && ctx.getString(options)
5949
let runtimeOptions = ''
50+
let transformOptions = ''
51+
6052
if (options) {
6153
if (options.type === 'ObjectExpression') {
62-
const local = options.properties.find(
63-
p =>
64-
p.type === 'ObjectProperty' &&
65-
((p.key.type === 'Identifier' && p.key.name === 'local') ||
66-
(p.key.type === 'StringLiteral' && p.key.value === 'local')),
67-
) as ObjectProperty
68-
69-
if (local) {
70-
runtimeOptions = `{ ${ctx.getString(local)} }`
71-
} else {
72-
for (const p of options.properties) {
73-
if (p.type === 'SpreadElement' || p.computed) {
74-
runtimeOptions = optionsString!
75-
break
76-
}
54+
for (let i = options.properties.length - 1; i >= 0; i--) {
55+
const p = options.properties[i]
56+
if (p.type === 'SpreadElement' || p.computed) {
57+
runtimeOptions = optionsString!
58+
break
59+
}
60+
if (
61+
(p.type === 'ObjectProperty' || p.type === 'ObjectMethod') &&
62+
((p.key.type === 'Identifier' &&
63+
(p.key.name === 'get' || p.key.name === 'set')) ||
64+
(p.key.type === 'StringLiteral' &&
65+
(p.key.value === 'get' || p.key.value === 'set')))
66+
) {
67+
transformOptions = ctx.getString(p) + ', ' + transformOptions
68+
69+
// remove transform option from prop options to avoid duplicates
70+
const offset = p.start! - options.start!
71+
const next = options.properties[i + 1]
72+
const end = (next ? next.start! : options.end! - 1) - options.start!
73+
optionsString =
74+
optionsString.slice(0, offset) + optionsString.slice(end)
7775
}
7876
}
77+
if (!runtimeOptions && transformOptions) {
78+
runtimeOptions = `{ ${transformOptions} }`
79+
}
7980
} else {
8081
runtimeOptions = optionsString!
8182
}
8283
}
8384

85+
ctx.modelDecls[modelName] = {
86+
type,
87+
options: optionsString,
88+
identifier:
89+
declId && declId.type === 'Identifier' ? declId.name : undefined,
90+
}
91+
// register binding type
92+
ctx.bindingMetadata[modelName] = BindingTypes.PROPS
93+
8494
ctx.s.overwrite(
8595
ctx.startOffset! + node.start!,
8696
ctx.startOffset! + node.end!,
@@ -133,6 +143,12 @@ export function genModelProps(ctx: ScriptCompileContext) {
133143
decl = options || (runtimeType ? `{ ${codegenOptions} }` : '{}')
134144
}
135145
modelPropsDecl += `\n ${JSON.stringify(name)}: ${decl},`
146+
147+
// also generate modifiers prop
148+
const modifierPropName = JSON.stringify(
149+
name === 'modelValue' ? `modelModifiers` : `${name}Modifiers`,
150+
)
151+
modelPropsDecl += `\n ${modifierPropName}: {},`
136152
}
137153
return `{${modelPropsDecl}\n }`
138154
}

packages/dts-test/setupHelpers.test-d.ts

+31
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,37 @@ describe('defineModel', () => {
314314
const inferredRequired = defineModel({ default: 123, required: true })
315315
expectType<Ref<number>>(inferredRequired)
316316

317+
// modifiers
318+
const [_, modifiers] = defineModel<string>()
319+
expectType<true | undefined>(modifiers.foo)
320+
321+
// limit supported modifiers
322+
const [__, typedModifiers] = defineModel<string, 'trim' | 'capitalize'>()
323+
expectType<true | undefined>(typedModifiers.trim)
324+
expectType<true | undefined>(typedModifiers.capitalize)
325+
// @ts-expect-error
326+
typedModifiers.foo
327+
328+
// transformers with type
329+
defineModel<string>({
330+
get(val) {
331+
return val.toLowerCase()
332+
},
333+
set(val) {
334+
return val.toUpperCase()
335+
},
336+
})
337+
// transformers with runtime type
338+
defineModel({
339+
type: String,
340+
get(val) {
341+
return val.toLowerCase()
342+
},
343+
set(val) {
344+
return val.toUpperCase()
345+
},
346+
})
347+
317348
// @ts-expect-error type / default mismatch
318349
defineModel<string>({ default: 123 })
319350
// @ts-expect-error unknown props option

0 commit comments

Comments
 (0)