mirror of https://github.com/vuejs/core.git
feat(defineModel): support modifiers and transformers
This commit is contained in:
parent
d7bb32f9a3
commit
a772031ea8
|
@ -6,8 +6,11 @@ exports[`defineModel() > basic usage 1`] = `
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
"modelValue": { required: true },
|
"modelValue": { required: true },
|
||||||
|
"modelModifiers": {},
|
||||||
"count": {},
|
"count": {},
|
||||||
|
"countModifiers": {},
|
||||||
"toString": { type: Function },
|
"toString": { type: Function },
|
||||||
|
"toStringModifiers": {},
|
||||||
},
|
},
|
||||||
emits: ["update:modelValue", "update:count", "update:toString"],
|
emits: ["update:modelValue", "update:count", "update:toString"],
|
||||||
setup(__props, { expose: __expose }) {
|
setup(__props, { expose: __expose }) {
|
||||||
|
@ -23,12 +26,58 @@ return { modelValue, c, toString }
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
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`] = `
|
exports[`defineModel() > w/ array props 1`] = `
|
||||||
"import { useModel as _useModel, mergeModels as _mergeModels } from 'vue'
|
"import { useModel as _useModel, mergeModels as _mergeModels } from 'vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: /*#__PURE__*/_mergeModels(['foo', 'bar'], {
|
props: /*#__PURE__*/_mergeModels(['foo', 'bar'], {
|
||||||
"count": {},
|
"count": {},
|
||||||
|
"countModifiers": {},
|
||||||
}),
|
}),
|
||||||
emits: ["update:count"],
|
emits: ["update:count"],
|
||||||
setup(__props, { expose: __expose }) {
|
setup(__props, { expose: __expose }) {
|
||||||
|
@ -49,6 +98,7 @@ exports[`defineModel() > w/ defineProps and defineEmits 1`] = `
|
||||||
export default {
|
export default {
|
||||||
props: /*#__PURE__*/_mergeModels({ foo: String }, {
|
props: /*#__PURE__*/_mergeModels({ foo: String }, {
|
||||||
"modelValue": { default: 0 },
|
"modelValue": { default: 0 },
|
||||||
|
"modelModifiers": {},
|
||||||
}),
|
}),
|
||||||
emits: /*#__PURE__*/_mergeModels(['change'], ["update:modelValue"]),
|
emits: /*#__PURE__*/_mergeModels(['change'], ["update:modelValue"]),
|
||||||
setup(__props, { expose: __expose }) {
|
setup(__props, { expose: __expose }) {
|
||||||
|
@ -64,47 +114,19 @@ return { count }
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
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`] = `
|
exports[`defineModel() > w/ types, basic usage 1`] = `
|
||||||
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
|
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
|
||||||
|
|
||||||
export default /*#__PURE__*/_defineComponent({
|
export default /*#__PURE__*/_defineComponent({
|
||||||
props: {
|
props: {
|
||||||
"modelValue": { type: [Boolean, String] },
|
"modelValue": { type: [Boolean, String] },
|
||||||
|
"modelModifiers": {},
|
||||||
"count": { type: Number },
|
"count": { type: Number },
|
||||||
|
"countModifiers": {},
|
||||||
"disabled": { type: Number, ...{ required: false } },
|
"disabled": { type: Number, ...{ required: false } },
|
||||||
|
"disabledModifiers": {},
|
||||||
"any": { type: Boolean, skipCheck: true },
|
"any": { type: Boolean, skipCheck: true },
|
||||||
|
"anyModifiers": {},
|
||||||
},
|
},
|
||||||
emits: ["update:modelValue", "update:count", "update:disabled", "update:any"],
|
emits: ["update:modelValue", "update:count", "update:disabled", "update:any"],
|
||||||
setup(__props, { expose: __expose }) {
|
setup(__props, { expose: __expose }) {
|
||||||
|
@ -127,10 +149,15 @@ exports[`defineModel() > w/ types, production mode 1`] = `
|
||||||
export default /*#__PURE__*/_defineComponent({
|
export default /*#__PURE__*/_defineComponent({
|
||||||
props: {
|
props: {
|
||||||
"modelValue": { type: Boolean },
|
"modelValue": { type: Boolean },
|
||||||
|
"modelModifiers": {},
|
||||||
"fn": {},
|
"fn": {},
|
||||||
|
"fnModifiers": {},
|
||||||
"fnWithDefault": { type: Function, ...{ default: () => null } },
|
"fnWithDefault": { type: Function, ...{ default: () => null } },
|
||||||
|
"fnWithDefaultModifiers": {},
|
||||||
"str": {},
|
"str": {},
|
||||||
|
"strModifiers": {},
|
||||||
"optional": { required: false },
|
"optional": { required: false },
|
||||||
|
"optionalModifiers": {},
|
||||||
},
|
},
|
||||||
emits: ["update:modelValue", "update:fn", "update:fnWithDefault", "update:str", "update:optional"],
|
emits: ["update:modelValue", "update:fn", "update:fnWithDefault", "update:str", "update:optional"],
|
||||||
setup(__props, { expose: __expose }) {
|
setup(__props, { expose: __expose }) {
|
||||||
|
|
|
@ -69,6 +69,7 @@ describe('defineModel()', () => {
|
||||||
assertCode(content)
|
assertCode(content)
|
||||||
expect(content).toMatch(`props: /*#__PURE__*/_mergeModels(['foo', 'bar'], {
|
expect(content).toMatch(`props: /*#__PURE__*/_mergeModels(['foo', 'bar'], {
|
||||||
"count": {},
|
"count": {},
|
||||||
|
"countModifiers": {},
|
||||||
})`)
|
})`)
|
||||||
expect(content).toMatch(`const count = _useModel(__props, "count")`)
|
expect(content).toMatch(`const count = _useModel(__props, "count")`)
|
||||||
expect(content).not.toMatch('defineModel')
|
expect(content).not.toMatch('defineModel')
|
||||||
|
@ -79,29 +80,6 @@ describe('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', () => {
|
test('w/ types, basic usage', () => {
|
||||||
const { content, bindings } = compile(
|
const { content, bindings } = compile(
|
||||||
`
|
`
|
||||||
|
@ -115,6 +93,7 @@ describe('defineModel()', () => {
|
||||||
)
|
)
|
||||||
assertCode(content)
|
assertCode(content)
|
||||||
expect(content).toMatch('"modelValue": { type: [Boolean, String] }')
|
expect(content).toMatch('"modelValue": { type: [Boolean, String] }')
|
||||||
|
expect(content).toMatch('"modelModifiers": {}')
|
||||||
expect(content).toMatch('"count": { type: Number }')
|
expect(content).toMatch('"count": { type: Number }')
|
||||||
expect(content).toMatch(
|
expect(content).toMatch(
|
||||||
'"disabled": { type: Number, ...{ required: false } }',
|
'"disabled": { type: Number, ...{ required: false } }',
|
||||||
|
@ -176,4 +155,43 @@ describe('defineModel()', () => {
|
||||||
optional: BindingTypes.SETUP_REF,
|
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 }, })`,
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { LVal, Node, ObjectProperty, TSType } from '@babel/types'
|
import type { LVal, Node, TSType } from '@babel/types'
|
||||||
import type { ScriptCompileContext } from './context'
|
import type { ScriptCompileContext } from './context'
|
||||||
import { inferRuntimeType } from './resolveType'
|
import { inferRuntimeType } from './resolveType'
|
||||||
import {
|
import {
|
||||||
|
@ -45,7 +45,42 @@ export function processDefineModel(
|
||||||
ctx.error(`duplicate model name ${JSON.stringify(modelName)}`, node)
|
ctx.error(`duplicate model name ${JSON.stringify(modelName)}`, node)
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionsString = options && ctx.getString(options)
|
let optionsString = options && ctx.getString(options)
|
||||||
|
let runtimeOptions = ''
|
||||||
|
let transformOptions = ''
|
||||||
|
|
||||||
|
if (options) {
|
||||||
|
if (options.type === 'ObjectExpression') {
|
||||||
|
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] = {
|
ctx.modelDecls[modelName] = {
|
||||||
type,
|
type,
|
||||||
|
@ -56,31 +91,6 @@ export function processDefineModel(
|
||||||
// register binding type
|
// register binding type
|
||||||
ctx.bindingMetadata[modelName] = BindingTypes.PROPS
|
ctx.bindingMetadata[modelName] = BindingTypes.PROPS
|
||||||
|
|
||||||
let runtimeOptions = ''
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
runtimeOptions = optionsString!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.s.overwrite(
|
ctx.s.overwrite(
|
||||||
ctx.startOffset! + node.start!,
|
ctx.startOffset! + node.start!,
|
||||||
ctx.startOffset! + node.end!,
|
ctx.startOffset! + node.end!,
|
||||||
|
@ -133,6 +143,12 @@ export function genModelProps(ctx: ScriptCompileContext) {
|
||||||
decl = options || (runtimeType ? `{ ${codegenOptions} }` : '{}')
|
decl = options || (runtimeType ? `{ ${codegenOptions} }` : '{}')
|
||||||
}
|
}
|
||||||
modelPropsDecl += `\n ${JSON.stringify(name)}: ${decl},`
|
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 }`
|
return `{${modelPropsDecl}\n }`
|
||||||
}
|
}
|
||||||
|
|
|
@ -314,6 +314,37 @@ describe('defineModel', () => {
|
||||||
const inferredRequired = defineModel({ default: 123, required: true })
|
const inferredRequired = defineModel({ default: 123, required: true })
|
||||||
expectType<Ref<number>>(inferredRequired)
|
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
|
// @ts-expect-error type / default mismatch
|
||||||
defineModel<string>({ default: 123 })
|
defineModel<string>({ default: 123 })
|
||||||
// @ts-expect-error unknown props option
|
// @ts-expect-error unknown props option
|
||||||
|
|
|
@ -513,6 +513,73 @@ describe('SFC <script setup> helpers', () => {
|
||||||
expect(slotRender).toBeCalledTimes(2)
|
expect(slotRender).toBeCalledTimes(2)
|
||||||
expect(serializeInner(root)).toBe('<div>bar</div>')
|
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', () => {
|
test('createPropsRestProxy', () => {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
EMPTY_OBJ,
|
||||||
type LooseRequired,
|
type LooseRequired,
|
||||||
type Prettify,
|
type Prettify,
|
||||||
type UnionToIntersection,
|
type UnionToIntersection,
|
||||||
|
@ -218,6 +219,9 @@ export function defineSlots<
|
||||||
return null as any
|
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
|
* Vue `<script setup>` compiler macro for declaring a
|
||||||
* two-way binding prop that can be consumed via `v-model` from the parent
|
* two-way binding prop that can be consumed via `v-model` from the parent
|
||||||
|
@ -251,25 +255,27 @@ export function defineSlots<
|
||||||
* const count = defineModel<number>('count', { default: 0 })
|
* const count = defineModel<number>('count', { default: 0 })
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function defineModel<T>(
|
export function defineModel<T, M extends string | number | symbol = string>(
|
||||||
options: { required: true } & PropOptions<T>,
|
options: { required: true } & PropOptions<T> & UseModelOptions<T>,
|
||||||
): Ref<T>
|
): ModelRef<T, M>
|
||||||
export function defineModel<T>(
|
export function defineModel<T, M extends string | number | symbol = string>(
|
||||||
options: { default: any } & PropOptions<T>,
|
options: { default: any } & PropOptions<T> & UseModelOptions<T>,
|
||||||
): Ref<T>
|
): ModelRef<T, M>
|
||||||
export function defineModel<T>(options?: PropOptions<T>): Ref<T | undefined>
|
export function defineModel<T, M extends string | number | symbol = string>(
|
||||||
export function defineModel<T>(
|
options?: PropOptions<T> & UseModelOptions<T>,
|
||||||
|
): ModelRef<T | undefined, M>
|
||||||
|
export function defineModel<T, M extends string | number | symbol = string>(
|
||||||
name: string,
|
name: string,
|
||||||
options: { required: true } & PropOptions<T>,
|
options: { required: true } & PropOptions<T> & UseModelOptions<T>,
|
||||||
): Ref<T>
|
): ModelRef<T, M>
|
||||||
export function defineModel<T>(
|
export function defineModel<T, M extends string | number | symbol = string>(
|
||||||
name: string,
|
name: string,
|
||||||
options: { default: any } & PropOptions<T>,
|
options: { default: any } & PropOptions<T> & UseModelOptions<T>,
|
||||||
): Ref<T>
|
): ModelRef<T, M>
|
||||||
export function defineModel<T>(
|
export function defineModel<T, M extends string | number | symbol = string>(
|
||||||
name: string,
|
name: string,
|
||||||
options?: PropOptions<T>,
|
options?: PropOptions<T> & UseModelOptions<T>,
|
||||||
): Ref<T | undefined>
|
): ModelRef<T | undefined, M>
|
||||||
export function defineModel(): any {
|
export function defineModel(): any {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
warnRuntimeUsage('defineModel')
|
warnRuntimeUsage('defineModel')
|
||||||
|
@ -348,11 +354,21 @@ export function useAttrs(): SetupContext['attrs'] {
|
||||||
return getContext().attrs
|
return getContext().attrs
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useModel<T extends Record<string, any>, K extends keyof T>(
|
type UseModelOptions<T = any> = {
|
||||||
props: T,
|
get?: (v: T) => any
|
||||||
name: K,
|
set?: (v: T) => any
|
||||||
): Ref<T[K]>
|
}
|
||||||
export function useModel(props: Record<string, any>, name: string): Ref {
|
|
||||||
|
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()!
|
const i = getCurrentInstance()!
|
||||||
if (__DEV__ && !i) {
|
if (__DEV__ && !i) {
|
||||||
warn(`useModel() called without active instance.`)
|
warn(`useModel() called without active instance.`)
|
||||||
|
@ -364,7 +380,7 @@ export function useModel(props: Record<string, any>, name: string): Ref {
|
||||||
return ref() as any
|
return ref() as any
|
||||||
}
|
}
|
||||||
|
|
||||||
return customRef((track, trigger) => {
|
const res = customRef((track, trigger) => {
|
||||||
let localValue: any
|
let localValue: any
|
||||||
watchSyncEffect(() => {
|
watchSyncEffect(() => {
|
||||||
const propValue = props[name]
|
const propValue = props[name]
|
||||||
|
@ -376,7 +392,7 @@ export function useModel(props: Record<string, any>, name: string): Ref {
|
||||||
return {
|
return {
|
||||||
get() {
|
get() {
|
||||||
track()
|
track()
|
||||||
return localValue
|
return options.get ? options.get(localValue) : localValue
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
const rawProps = i.vnode!.props
|
const rawProps = i.vnode!.props
|
||||||
|
@ -384,10 +400,29 @@ export function useModel(props: Record<string, any>, name: string): Ref {
|
||||||
localValue = value
|
localValue = value
|
||||||
trigger()
|
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 {
|
function getContext(): SetupContext {
|
||||||
|
|
|
@ -60,7 +60,7 @@ export { provide, inject, hasInjectionContext } from './apiInject'
|
||||||
export { nextTick } from './scheduler'
|
export { nextTick } from './scheduler'
|
||||||
export { defineComponent } from './apiDefineComponent'
|
export { defineComponent } from './apiDefineComponent'
|
||||||
export { defineAsyncComponent } from './apiAsyncComponent'
|
export { defineAsyncComponent } from './apiAsyncComponent'
|
||||||
export { useAttrs, useSlots, type DefineProps } from './apiSetupHelpers'
|
export { useAttrs, useSlots } from './apiSetupHelpers'
|
||||||
|
|
||||||
// <script setup> API ----------------------------------------------------------
|
// <script setup> API ----------------------------------------------------------
|
||||||
|
|
||||||
|
@ -74,6 +74,8 @@ export {
|
||||||
defineModel,
|
defineModel,
|
||||||
withDefaults,
|
withDefaults,
|
||||||
useModel,
|
useModel,
|
||||||
|
type DefineProps,
|
||||||
|
type ModelRef,
|
||||||
} from './apiSetupHelpers'
|
} from './apiSetupHelpers'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue