export default {
props: {
"modelValue": { required: true },
+ "modelModifiers": {},
"count": {},
+ "countModifiers": {},
"toString": { type: Function },
+ "toStringModifiers": {},
},
emits: ["update:modelValue", "update:count", "update:toString"],
setup(__props, { expose: __expose }) {
}"
`;
+exports[`defineModel() > get / set transformers 1`] = `
+"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
+
+export default /*#__PURE__*/_defineComponent({
+ props: {
+ "modelValue": {
+ required: true
+ },
+ "modelModifiers": {},
+ },
+ emits: ["update:modelValue"],
+ setup(__props, { expose: __expose }) {
+ __expose();
+
+ const modelValue = _useModel(__props, "modelValue", { get(v) { return v - 1 }, set: (v) => { return v + 1 }, })
+
+return { modelValue }
+}
+
+})"
+`;
+
+exports[`defineModel() > get / set transformers 2`] = `
+"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
+
+export default /*#__PURE__*/_defineComponent({
+ props: {
+ "modelValue": {
+ default: 0,
+ required: true,
+ },
+ "modelModifiers": {},
+ },
+ emits: ["update:modelValue"],
+ setup(__props, { expose: __expose }) {
+ __expose();
+
+ const modelValue = _useModel(__props, "modelValue", { get(v) { return v - 1 }, set: (v) => { return v + 1 }, })
+
+return { modelValue }
+}
+
+})"
+`;
+
exports[`defineModel() > w/ array props 1`] = `
"import { useModel as _useModel, mergeModels as _mergeModels } from 'vue'
export default {
props: /*#__PURE__*/_mergeModels(['foo', 'bar'], {
"count": {},
+ "countModifiers": {},
}),
emits: ["update:count"],
setup(__props, { expose: __expose }) {
export default {
props: /*#__PURE__*/_mergeModels({ foo: String }, {
"modelValue": { default: 0 },
+ "modelModifiers": {},
}),
emits: /*#__PURE__*/_mergeModels(['change'], ["update:modelValue"]),
setup(__props, { expose: __expose }) {
}"
`;
-exports[`defineModel() > w/ local flag 1`] = `
-"import { useModel as _useModel } from 'vue'
-const local = true
-
-export default {
- props: {
- "modelValue": { local: true, default: 1 },
- "bar": { [key]: true },
- "baz": { ...x },
- "qux": x,
- "foo2": { local: true, ...x },
- "hoist": { local },
- },
- emits: ["update:modelValue", "update:bar", "update:baz", "update:qux", "update:foo2", "update:hoist"],
- setup(__props, { expose: __expose }) {
- __expose();
-
- const foo = _useModel(__props, "modelValue", { local: true })
- const bar = _useModel(__props, "bar", { [key]: true })
- const baz = _useModel(__props, "baz", { ...x })
- const qux = _useModel(__props, "qux", x)
-
- const foo2 = _useModel(__props, "foo2", { local: true })
-
- const hoist = _useModel(__props, "hoist", { local })
-
-return { foo, bar, baz, qux, foo2, local, hoist }
-}
-
-}"
-`;
-
exports[`defineModel() > w/ types, basic usage 1`] = `
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
export default /*#__PURE__*/_defineComponent({
props: {
"modelValue": { type: [Boolean, String] },
+ "modelModifiers": {},
"count": { type: Number },
+ "countModifiers": {},
"disabled": { type: Number, ...{ required: false } },
+ "disabledModifiers": {},
"any": { type: Boolean, skipCheck: true },
+ "anyModifiers": {},
},
emits: ["update:modelValue", "update:count", "update:disabled", "update:any"],
setup(__props, { expose: __expose }) {
export default /*#__PURE__*/_defineComponent({
props: {
"modelValue": { type: Boolean },
+ "modelModifiers": {},
"fn": {},
+ "fnModifiers": {},
"fnWithDefault": { type: Function, ...{ default: () => null } },
+ "fnWithDefaultModifiers": {},
"str": {},
+ "strModifiers": {},
"optional": { required: false },
+ "optionalModifiers": {},
},
emits: ["update:modelValue", "update:fn", "update:fnWithDefault", "update:str", "update:optional"],
setup(__props, { expose: __expose }) {
assertCode(content)
expect(content).toMatch(`props: /*#__PURE__*/_mergeModels(['foo', 'bar'], {
"count": {},
+ "countModifiers": {},
})`)
expect(content).toMatch(`const count = _useModel(__props, "count")`)
expect(content).not.toMatch('defineModel')
})
})
- test('w/ local flag', () => {
- const { content } = compile(
- `<script setup>
- const foo = defineModel({ local: true, default: 1 })
- const bar = defineModel('bar', { [key]: true })
- const baz = defineModel('baz', { ...x })
- const qux = defineModel('qux', x)
-
- const foo2 = defineModel('foo2', { local: true, ...x })
-
- const local = true
- const hoist = defineModel('hoist', { local })
- </script>`,
- )
- assertCode(content)
- expect(content).toMatch(`_useModel(__props, "modelValue", { local: true })`)
- expect(content).toMatch(`_useModel(__props, "bar", { [key]: true })`)
- expect(content).toMatch(`_useModel(__props, "baz", { ...x })`)
- expect(content).toMatch(`_useModel(__props, "qux", x)`)
- expect(content).toMatch(`_useModel(__props, "foo2", { local: true })`)
- expect(content).toMatch(`_useModel(__props, "hoist", { local })`)
- })
-
test('w/ types, basic usage', () => {
const { content, bindings } = compile(
`
)
assertCode(content)
expect(content).toMatch('"modelValue": { type: [Boolean, String] }')
+ expect(content).toMatch('"modelModifiers": {}')
expect(content).toMatch('"count": { type: Number }')
expect(content).toMatch(
'"disabled": { type: Number, ...{ required: false } }',
optional: BindingTypes.SETUP_REF,
})
})
+
+ test('get / set transformers', () => {
+ const { content } = compile(
+ `
+ <script setup lang="ts">
+ const modelValue = defineModel({
+ get(v) { return v - 1 },
+ set: (v) => { return v + 1 },
+ required: true
+ })
+ </script>
+ `,
+ )
+ assertCode(content)
+ expect(content).toMatch(/"modelValue": {\s+required: true,?\s+}/m)
+ expect(content).toMatch(
+ `_useModel(__props, "modelValue", { get(v) { return v - 1 }, set: (v) => { return v + 1 }, })`,
+ )
+
+ const { content: content2 } = compile(
+ `
+ <script setup lang="ts">
+ const modelValue = defineModel({
+ default: 0,
+ get(v) { return v - 1 },
+ required: true,
+ set: (v) => { return v + 1 },
+ })
+ </script>
+ `,
+ )
+ assertCode(content2)
+ expect(content2).toMatch(
+ /"modelValue": {\s+default: 0,\s+required: true,?\s+}/m,
+ )
+ expect(content2).toMatch(
+ `_useModel(__props, "modelValue", { get(v) { return v - 1 }, set: (v) => { return v + 1 }, })`,
+ )
+ })
})
-import type { LVal, Node, ObjectProperty, TSType } from '@babel/types'
+import type { LVal, Node, TSType } from '@babel/types'
import type { ScriptCompileContext } from './context'
import { inferRuntimeType } from './resolveType'
import {
ctx.error(`duplicate model name ${JSON.stringify(modelName)}`, node)
}
- const optionsString = options && ctx.getString(options)
-
- ctx.modelDecls[modelName] = {
- type,
- options: optionsString,
- identifier:
- declId && declId.type === 'Identifier' ? declId.name : undefined,
- }
- // register binding type
- ctx.bindingMetadata[modelName] = BindingTypes.PROPS
-
+ let optionsString = options && ctx.getString(options)
let runtimeOptions = ''
+ let transformOptions = ''
+
if (options) {
if (options.type === 'ObjectExpression') {
- const local = options.properties.find(
- p =>
- p.type === 'ObjectProperty' &&
- ((p.key.type === 'Identifier' && p.key.name === 'local') ||
- (p.key.type === 'StringLiteral' && p.key.value === 'local')),
- ) as ObjectProperty
-
- if (local) {
- runtimeOptions = `{ ${ctx.getString(local)} }`
- } else {
- for (const p of options.properties) {
- if (p.type === 'SpreadElement' || p.computed) {
- runtimeOptions = optionsString!
- break
- }
+ for (let i = options.properties.length - 1; i >= 0; i--) {
+ const p = options.properties[i]
+ if (p.type === 'SpreadElement' || p.computed) {
+ runtimeOptions = optionsString!
+ break
+ }
+ if (
+ (p.type === 'ObjectProperty' || p.type === 'ObjectMethod') &&
+ ((p.key.type === 'Identifier' &&
+ (p.key.name === 'get' || p.key.name === 'set')) ||
+ (p.key.type === 'StringLiteral' &&
+ (p.key.value === 'get' || p.key.value === 'set')))
+ ) {
+ transformOptions = ctx.getString(p) + ', ' + transformOptions
+
+ // remove transform option from prop options to avoid duplicates
+ const offset = p.start! - options.start!
+ const next = options.properties[i + 1]
+ const end = (next ? next.start! : options.end! - 1) - options.start!
+ optionsString =
+ optionsString.slice(0, offset) + optionsString.slice(end)
}
}
+ if (!runtimeOptions && transformOptions) {
+ runtimeOptions = `{ ${transformOptions} }`
+ }
} else {
runtimeOptions = optionsString!
}
}
+ ctx.modelDecls[modelName] = {
+ type,
+ options: optionsString,
+ identifier:
+ declId && declId.type === 'Identifier' ? declId.name : undefined,
+ }
+ // register binding type
+ ctx.bindingMetadata[modelName] = BindingTypes.PROPS
+
ctx.s.overwrite(
ctx.startOffset! + node.start!,
ctx.startOffset! + node.end!,
decl = options || (runtimeType ? `{ ${codegenOptions} }` : '{}')
}
modelPropsDecl += `\n ${JSON.stringify(name)}: ${decl},`
+
+ // also generate modifiers prop
+ const modifierPropName = JSON.stringify(
+ name === 'modelValue' ? `modelModifiers` : `${name}Modifiers`,
+ )
+ modelPropsDecl += `\n ${modifierPropName}: {},`
}
return `{${modelPropsDecl}\n }`
}
const inferredRequired = defineModel({ default: 123, required: true })
expectType<Ref<number>>(inferredRequired)
+ // modifiers
+ const [_, modifiers] = defineModel<string>()
+ expectType<true | undefined>(modifiers.foo)
+
+ // limit supported modifiers
+ const [__, typedModifiers] = defineModel<string, 'trim' | 'capitalize'>()
+ expectType<true | undefined>(typedModifiers.trim)
+ expectType<true | undefined>(typedModifiers.capitalize)
+ // @ts-expect-error
+ typedModifiers.foo
+
+ // transformers with type
+ defineModel<string>({
+ get(val) {
+ return val.toLowerCase()
+ },
+ set(val) {
+ return val.toUpperCase()
+ },
+ })
+ // transformers with runtime type
+ defineModel({
+ type: String,
+ get(val) {
+ return val.toLowerCase()
+ },
+ set(val) {
+ return val.toUpperCase()
+ },
+ })
+
// @ts-expect-error type / default mismatch
defineModel<string>({ default: 123 })
// @ts-expect-error unknown props option
expect(slotRender).toBeCalledTimes(2)
expect(serializeInner(root)).toBe('<div>bar</div>')
})
+
+ test('with modifiers & transformers', async () => {
+ let childMsg: Ref<string>
+ let childModifiers: Record<string, true | undefined>
+
+ const compRender = vi.fn()
+ const Comp = defineComponent({
+ props: ['msg', 'msgModifiers'],
+ emits: ['update:msg'],
+ setup(props) {
+ ;[childMsg, childModifiers] = useModel(props, 'msg', {
+ get(val) {
+ return val.toLowerCase()
+ },
+ set(val) {
+ if (childModifiers.upper) {
+ return val.toUpperCase()
+ }
+ },
+ })
+ return () => {
+ compRender()
+ return childMsg.value
+ }
+ },
+ })
+
+ const msg = ref('HI')
+ const Parent = defineComponent({
+ setup() {
+ return () =>
+ h(Comp, {
+ msg: msg.value,
+ msgModifiers: { upper: true },
+ 'onUpdate:msg': val => {
+ msg.value = val
+ },
+ })
+ },
+ })
+
+ const root = nodeOps.createElement('div')
+ render(h(Parent), root)
+
+ // should be lowered
+ expect(serializeInner(root)).toBe('hi')
+
+ // child update
+ childMsg!.value = 'Hmm'
+
+ await nextTick()
+ expect(childMsg!.value).toBe('hmm')
+ expect(serializeInner(root)).toBe('hmm')
+ // parent should get uppercase value
+ expect(msg.value).toBe('HMM')
+
+ // parent update
+ msg.value = 'Ughh'
+ await nextTick()
+ expect(serializeInner(root)).toBe('ughh')
+ expect(msg.value).toBe('Ughh')
+
+ // child update again
+ childMsg!.value = 'ughh'
+ await nextTick()
+ expect(msg.value).toBe('UGHH')
+ })
})
test('createPropsRestProxy', () => {
import {
+ EMPTY_OBJ,
type LooseRequired,
type Prettify,
type UnionToIntersection,
return null as any
}
+export type ModelRef<T, M extends string | number | symbol = string> = Ref<T> &
+ [ModelRef<T, M>, Record<M, true | undefined>]
+
/**
* Vue `<script setup>` compiler macro for declaring a
* two-way binding prop that can be consumed via `v-model` from the parent
* const count = defineModel<number>('count', { default: 0 })
* ```
*/
-export function defineModel<T>(
- options: { required: true } & PropOptions<T>,
-): Ref<T>
-export function defineModel<T>(
- options: { default: any } & PropOptions<T>,
-): Ref<T>
-export function defineModel<T>(options?: PropOptions<T>): Ref<T | undefined>
-export function defineModel<T>(
+export function defineModel<T, M extends string | number | symbol = string>(
+ options: { required: true } & PropOptions<T> & UseModelOptions<T>,
+): ModelRef<T, M>
+export function defineModel<T, M extends string | number | symbol = string>(
+ options: { default: any } & PropOptions<T> & UseModelOptions<T>,
+): ModelRef<T, M>
+export function defineModel<T, M extends string | number | symbol = string>(
+ options?: PropOptions<T> & UseModelOptions<T>,
+): ModelRef<T | undefined, M>
+export function defineModel<T, M extends string | number | symbol = string>(
name: string,
- options: { required: true } & PropOptions<T>,
-): Ref<T>
-export function defineModel<T>(
+ options: { required: true } & PropOptions<T> & UseModelOptions<T>,
+): ModelRef<T, M>
+export function defineModel<T, M extends string | number | symbol = string>(
name: string,
- options: { default: any } & PropOptions<T>,
-): Ref<T>
-export function defineModel<T>(
+ options: { default: any } & PropOptions<T> & UseModelOptions<T>,
+): ModelRef<T, M>
+export function defineModel<T, M extends string | number | symbol = string>(
name: string,
- options?: PropOptions<T>,
-): Ref<T | undefined>
+ options?: PropOptions<T> & UseModelOptions<T>,
+): ModelRef<T | undefined, M>
export function defineModel(): any {
if (__DEV__) {
warnRuntimeUsage('defineModel')
return getContext().attrs
}
-export function useModel<T extends Record<string, any>, K extends keyof T>(
- props: T,
- name: K,
-): Ref<T[K]>
-export function useModel(props: Record<string, any>, name: string): Ref {
+type UseModelOptions<T = any> = {
+ get?: (v: T) => any
+ set?: (v: T) => any
+}
+
+export function useModel<
+ M extends string | number | symbol,
+ T extends Record<string, any>,
+ K extends keyof T,
+>(props: T, name: K, options?: UseModelOptions<T[K]>): ModelRef<T[K], M>
+export function useModel(
+ props: Record<string, any>,
+ name: string,
+ options: UseModelOptions = EMPTY_OBJ,
+): Ref {
const i = getCurrentInstance()!
if (__DEV__ && !i) {
warn(`useModel() called without active instance.`)
return ref() as any
}
- return customRef((track, trigger) => {
+ const res = customRef((track, trigger) => {
let localValue: any
watchSyncEffect(() => {
const propValue = props[name]
return {
get() {
track()
- return localValue
+ return options.get ? options.get(localValue) : localValue
},
set(value) {
const rawProps = i.vnode!.props
localValue = value
trigger()
}
- i.emit(`update:${name}`, value)
+ i.emit(`update:${name}`, options.set ? options.set(value) : value)
},
}
})
+
+ const modifierKey =
+ name === 'modelValue' ? 'modelModifiers' : `${name}Modifiers`
+
+ // @ts-expect-error
+ res[Symbol.iterator] = () => {
+ let i = 0
+ return {
+ next() {
+ if (i < 2) {
+ return { value: i++ ? props[modifierKey] : res, done: false }
+ } else {
+ return { done: true }
+ }
+ },
+ }
+ }
+
+ return res
}
function getContext(): SetupContext {
export { nextTick } from './scheduler'
export { defineComponent } from './apiDefineComponent'
export { defineAsyncComponent } from './apiAsyncComponent'
-export { useAttrs, useSlots, type DefineProps } from './apiSetupHelpers'
+export { useAttrs, useSlots } from './apiSetupHelpers'
// <script setup> API ----------------------------------------------------------
defineModel,
withDefaults,
useModel,
+ type DefineProps,
+ type ModelRef,
} from './apiSetupHelpers'
/**