feat(sfc): introduce `defineModel` macro and `useModel` helper (#8018)
This commit is contained in:
parent
3b02c27e6d
commit
14f3d747a3
|
@ -653,6 +653,100 @@ return { }
|
|||
}"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > defineModel() > basic usage 1`] = `
|
||||
"import { useModel as _useModel } from 'vue'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
\\"modelValue\\": { required: true },
|
||||
\\"count\\": {},
|
||||
},
|
||||
emits: [\\"update:modelValue\\", \\"update:count\\"],
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
|
||||
const modelValue = _useModel(__props, \\"modelValue\\")
|
||||
const c = _useModel(__props, \\"count\\")
|
||||
|
||||
return { modelValue, c }
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > defineModel() > w/ array props 1`] = `
|
||||
"import { useModel as _useModel, mergeModels as _mergeModels } from 'vue'
|
||||
|
||||
export default {
|
||||
props: _mergeModels(['foo', 'bar'], {
|
||||
\\"count\\": {},
|
||||
}),
|
||||
emits: [\\"update:count\\"],
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
|
||||
|
||||
const count = _useModel(__props, \\"count\\")
|
||||
|
||||
return { count }
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > defineModel() > w/ defineProps and defineEmits 1`] = `
|
||||
"import { useModel as _useModel, mergeModels as _mergeModels } from 'vue'
|
||||
|
||||
export default {
|
||||
props: _mergeModels({ foo: String }, {
|
||||
\\"modelValue\\": { default: 0 },
|
||||
}),
|
||||
emits: _mergeModels(['change'], [\\"update:modelValue\\"]),
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
|
||||
|
||||
|
||||
const count = _useModel(__props, \\"modelValue\\")
|
||||
|
||||
return { count }
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > 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[`SFC compile <script setup> > defineOptions() > basic usage 1`] = `
|
||||
"export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, {
|
||||
setup(__props, { expose: __expose }) {
|
||||
|
@ -1596,6 +1690,58 @@ return { emit }
|
|||
})"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > with TypeScript > defineModel() > basic usage 1`] = `
|
||||
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
|
||||
|
||||
export default /*#__PURE__*/_defineComponent({
|
||||
props: {
|
||||
\\"modelValue\\": { type: [Boolean, String] },
|
||||
\\"count\\": { type: Number },
|
||||
\\"disabled\\": { type: Number, ...{ required: false } },
|
||||
\\"any\\": { type: Boolean, skipCheck: true },
|
||||
},
|
||||
emits: [\\"update:modelValue\\", \\"update:count\\", \\"update:disabled\\", \\"update:any\\"],
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
|
||||
const modelValue = _useModel(__props, \\"modelValue\\")
|
||||
const count = _useModel(__props, \\"count\\")
|
||||
const disabled = _useModel(__props, \\"disabled\\")
|
||||
const any = _useModel(__props, \\"any\\")
|
||||
|
||||
return { modelValue, count, disabled, any }
|
||||
}
|
||||
|
||||
})"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > with TypeScript > defineModel() > w/ production mode 1`] = `
|
||||
"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
|
||||
|
||||
export default /*#__PURE__*/_defineComponent({
|
||||
props: {
|
||||
\\"modelValue\\": { type: Boolean },
|
||||
\\"fn\\": {},
|
||||
\\"fnWithDefault\\": { type: Function, ...{ default: () => null } },
|
||||
\\"str\\": {},
|
||||
\\"optional\\": { required: false },
|
||||
},
|
||||
emits: [\\"update:modelValue\\", \\"update:fn\\", \\"update:fnWithDefault\\", \\"update:str\\", \\"update:optional\\"],
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
|
||||
const modelValue = _useModel(__props, \\"modelValue\\")
|
||||
const fn = _useModel(__props, \\"fn\\")
|
||||
const fnWithDefault = _useModel(__props, \\"fnWithDefault\\")
|
||||
const str = _useModel(__props, \\"str\\")
|
||||
const optional = _useModel(__props, \\"optional\\")
|
||||
|
||||
return { modelValue, fn, fnWithDefault, str, optional }
|
||||
}
|
||||
|
||||
})"
|
||||
`;
|
||||
|
||||
exports[`SFC compile <script setup> > with TypeScript > defineProps w/ TS assertion 1`] = `
|
||||
"import { defineComponent as _defineComponent } from 'vue'
|
||||
|
||||
|
@ -1961,7 +2107,7 @@ export default /*#__PURE__*/_defineComponent({
|
|||
foo: { type: Function },
|
||||
bar: { type: Boolean },
|
||||
baz: { type: [Boolean, Function] },
|
||||
qux: null
|
||||
qux: {}
|
||||
}, { ...defaults }),
|
||||
setup(__props: any, { expose: __expose }) {
|
||||
__expose();
|
||||
|
@ -2063,7 +2209,7 @@ exports[`SFC compile <script setup> > with TypeScript > withDefaults (static) w/
|
|||
|
||||
export default /*#__PURE__*/_defineComponent({
|
||||
props: {
|
||||
foo: null,
|
||||
foo: {},
|
||||
bar: { type: Boolean },
|
||||
baz: { type: [Boolean, Function], default: true },
|
||||
qux: { default: 'hi' }
|
||||
|
|
|
@ -123,7 +123,7 @@ export default /*#__PURE__*/_defineComponent({
|
|||
props: {
|
||||
foo: { default: 1 },
|
||||
bar: { default: () => ({}) },
|
||||
baz: null,
|
||||
baz: {},
|
||||
boola: { type: Boolean },
|
||||
boolb: { type: [Boolean, Number] },
|
||||
func: { type: Function, default: () => {} }
|
||||
|
|
|
@ -306,6 +306,48 @@ const myEmit = defineEmits(['foo', 'bar'])
|
|||
'[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should emit an error with declaring props/emits/slots/expose', () => {
|
||||
expect(() =>
|
||||
compile(`
|
||||
<script setup>
|
||||
defineOptions({ props: ['foo'] })
|
||||
</script>
|
||||
`)
|
||||
).toThrowError(
|
||||
'[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead'
|
||||
)
|
||||
|
||||
expect(() =>
|
||||
compile(`
|
||||
<script setup>
|
||||
defineOptions({ emits: ['update'] })
|
||||
</script>
|
||||
`)
|
||||
).toThrowError(
|
||||
'[@vue/compiler-sfc] defineOptions() cannot be used to declare emits. Use defineEmits() instead'
|
||||
)
|
||||
|
||||
expect(() =>
|
||||
compile(`
|
||||
<script setup>
|
||||
defineOptions({ expose: ['foo'] })
|
||||
</script>
|
||||
`)
|
||||
).toThrowError(
|
||||
'[@vue/compiler-sfc] defineOptions() cannot be used to declare expose. Use defineExpose() instead'
|
||||
)
|
||||
|
||||
expect(() =>
|
||||
compile(`
|
||||
<script setup lang="ts">
|
||||
defineOptions({ slots: Object })
|
||||
</script>
|
||||
`)
|
||||
).toThrowError(
|
||||
'[@vue/compiler-sfc] defineOptions() cannot be used to declare slots. Use defineSlots() instead'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test('defineExpose()', () => {
|
||||
|
@ -323,6 +365,109 @@ defineExpose({ foo: 123 })
|
|||
expect(content).toMatch(/\b__expose\(\{ foo: 123 \}\)/)
|
||||
})
|
||||
|
||||
describe('defineModel()', () => {
|
||||
test('basic usage', () => {
|
||||
const { content, bindings } = compile(
|
||||
`
|
||||
<script setup>
|
||||
const modelValue = defineModel({ required: true })
|
||||
const c = defineModel('count')
|
||||
</script>
|
||||
`,
|
||||
{ defineModel: true }
|
||||
)
|
||||
assertCode(content)
|
||||
expect(content).toMatch('props: {')
|
||||
expect(content).toMatch('"modelValue": { required: true },')
|
||||
expect(content).toMatch('"count": {},')
|
||||
expect(content).toMatch('emits: ["update:modelValue", "update:count"],')
|
||||
expect(content).toMatch(
|
||||
`const modelValue = _useModel(__props, "modelValue")`
|
||||
)
|
||||
expect(content).toMatch(`const c = _useModel(__props, "count")`)
|
||||
expect(content).toMatch(`return { modelValue, c }`)
|
||||
expect(content).not.toMatch('defineModel')
|
||||
|
||||
expect(bindings).toStrictEqual({
|
||||
modelValue: BindingTypes.SETUP_REF,
|
||||
count: BindingTypes.PROPS,
|
||||
c: BindingTypes.SETUP_REF
|
||||
})
|
||||
})
|
||||
|
||||
test('w/ defineProps and defineEmits', () => {
|
||||
const { content, bindings } = compile(
|
||||
`
|
||||
<script setup>
|
||||
defineProps({ foo: String })
|
||||
defineEmits(['change'])
|
||||
const count = defineModel({ default: 0 })
|
||||
</script>
|
||||
`,
|
||||
{ defineModel: true }
|
||||
)
|
||||
assertCode(content)
|
||||
expect(content).toMatch(`props: _mergeModels({ foo: String }`)
|
||||
expect(content).toMatch(`"modelValue": { default: 0 }`)
|
||||
expect(content).toMatch(`const count = _useModel(__props, "modelValue")`)
|
||||
expect(content).not.toMatch('defineModel')
|
||||
expect(bindings).toStrictEqual({
|
||||
count: BindingTypes.SETUP_REF,
|
||||
foo: BindingTypes.PROPS,
|
||||
modelValue: BindingTypes.PROPS
|
||||
})
|
||||
})
|
||||
|
||||
test('w/ array props', () => {
|
||||
const { content, bindings } = compile(
|
||||
`
|
||||
<script setup>
|
||||
defineProps(['foo', 'bar'])
|
||||
const count = defineModel('count')
|
||||
</script>
|
||||
`,
|
||||
{ defineModel: true }
|
||||
)
|
||||
assertCode(content)
|
||||
expect(content).toMatch(`props: _mergeModels(['foo', 'bar'], {
|
||||
"count": {},
|
||||
})`)
|
||||
expect(content).toMatch(`const count = _useModel(__props, "count")`)
|
||||
expect(content).not.toMatch('defineModel')
|
||||
expect(bindings).toStrictEqual({
|
||||
foo: BindingTypes.PROPS,
|
||||
bar: BindingTypes.PROPS,
|
||||
count: BindingTypes.SETUP_REF
|
||||
})
|
||||
})
|
||||
|
||||
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>`,
|
||||
{ defineModel: true }
|
||||
)
|
||||
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('<script> after <script setup> the script content not end with `\\n`', () => {
|
||||
const { content } = compile(`
|
||||
<script setup>
|
||||
|
@ -1391,7 +1536,7 @@ const emit = defineEmits(['a', 'b'])
|
|||
expect(content).toMatch(`const props = __props`)
|
||||
|
||||
// foo has no default value, the Function can be dropped
|
||||
expect(content).toMatch(`foo: null`)
|
||||
expect(content).toMatch(`foo: {}`)
|
||||
expect(content).toMatch(`bar: { type: Boolean }`)
|
||||
expect(content).toMatch(
|
||||
`baz: { type: [Boolean, Function], default: true }`
|
||||
|
@ -1469,7 +1614,7 @@ const emit = defineEmits(['a', 'b'])
|
|||
foo: { type: Function },
|
||||
bar: { type: Boolean },
|
||||
baz: { type: [Boolean, Function] },
|
||||
qux: null
|
||||
qux: {}
|
||||
}, { ...defaults })`.trim()
|
||||
)
|
||||
})
|
||||
|
@ -1679,6 +1824,86 @@ const emit = defineEmits(['a', 'b'])
|
|||
})
|
||||
})
|
||||
|
||||
describe('defineModel()', () => {
|
||||
test('basic usage', () => {
|
||||
const { content, bindings } = compile(
|
||||
`
|
||||
<script setup lang="ts">
|
||||
const modelValue = defineModel<boolean | string>()
|
||||
const count = defineModel<number>('count')
|
||||
const disabled = defineModel<number>('disabled', { required: false })
|
||||
const any = defineModel<any | boolean>('any')
|
||||
</script>
|
||||
`,
|
||||
{ defineModel: true }
|
||||
)
|
||||
assertCode(content)
|
||||
expect(content).toMatch('"modelValue": { type: [Boolean, String] }')
|
||||
expect(content).toMatch('"count": { type: Number }')
|
||||
expect(content).toMatch(
|
||||
'"disabled": { type: Number, ...{ required: false } }'
|
||||
)
|
||||
expect(content).toMatch('"any": { type: Boolean, skipCheck: true }')
|
||||
expect(content).toMatch(
|
||||
'emits: ["update:modelValue", "update:count", "update:disabled", "update:any"]'
|
||||
)
|
||||
|
||||
expect(content).toMatch(
|
||||
`const modelValue = _useModel(__props, "modelValue")`
|
||||
)
|
||||
expect(content).toMatch(`const count = _useModel(__props, "count")`)
|
||||
expect(content).toMatch(
|
||||
`const disabled = _useModel(__props, "disabled")`
|
||||
)
|
||||
expect(content).toMatch(`const any = _useModel(__props, "any")`)
|
||||
|
||||
expect(bindings).toStrictEqual({
|
||||
modelValue: BindingTypes.SETUP_REF,
|
||||
count: BindingTypes.SETUP_REF,
|
||||
disabled: BindingTypes.SETUP_REF,
|
||||
any: BindingTypes.SETUP_REF
|
||||
})
|
||||
})
|
||||
|
||||
test('w/ production mode', () => {
|
||||
const { content, bindings } = compile(
|
||||
`
|
||||
<script setup lang="ts">
|
||||
const modelValue = defineModel<boolean>()
|
||||
const fn = defineModel<() => void>('fn')
|
||||
const fnWithDefault = defineModel<() => void>('fnWithDefault', { default: () => null })
|
||||
const str = defineModel<string>('str')
|
||||
const optional = defineModel<string>('optional', { required: false })
|
||||
</script>
|
||||
`,
|
||||
{ defineModel: true, isProd: true }
|
||||
)
|
||||
assertCode(content)
|
||||
expect(content).toMatch('"modelValue": { type: Boolean }')
|
||||
expect(content).toMatch('"fn": {}')
|
||||
expect(content).toMatch(
|
||||
'"fnWithDefault": { type: Function, ...{ default: () => null } },'
|
||||
)
|
||||
expect(content).toMatch('"str": {}')
|
||||
expect(content).toMatch('"optional": { required: false }')
|
||||
expect(content).toMatch(
|
||||
'emits: ["update:modelValue", "update:fn", "update:fnWithDefault", "update:str", "update:optional"]'
|
||||
)
|
||||
expect(content).toMatch(
|
||||
`const modelValue = _useModel(__props, "modelValue")`
|
||||
)
|
||||
expect(content).toMatch(`const fn = _useModel(__props, "fn")`)
|
||||
expect(content).toMatch(`const str = _useModel(__props, "str")`)
|
||||
expect(bindings).toStrictEqual({
|
||||
modelValue: BindingTypes.SETUP_REF,
|
||||
fn: BindingTypes.SETUP_REF,
|
||||
fnWithDefault: BindingTypes.SETUP_REF,
|
||||
str: BindingTypes.SETUP_REF,
|
||||
optional: BindingTypes.SETUP_REF
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('runtime Enum', () => {
|
||||
const { content, bindings } = compile(
|
||||
`<script setup lang="ts">
|
||||
|
|
|
@ -131,17 +131,17 @@ describe('sfc props transform', () => {
|
|||
`,
|
||||
{ isProd: true }
|
||||
)
|
||||
assertCode(content)
|
||||
// literals can be used as-is, non-literals are always returned from a
|
||||
// function
|
||||
expect(content).toMatch(`props: {
|
||||
foo: { default: 1 },
|
||||
bar: { default: () => ({}) },
|
||||
baz: null,
|
||||
baz: {},
|
||||
boola: { type: Boolean },
|
||||
boolb: { type: [Boolean, Number] },
|
||||
func: { type: Function, default: () => {} }
|
||||
}`)
|
||||
assertCode(content)
|
||||
})
|
||||
|
||||
test('aliasing', () => {
|
||||
|
|
|
@ -68,6 +68,7 @@ const DEFINE_EXPOSE = 'defineExpose'
|
|||
const WITH_DEFAULTS = 'withDefaults'
|
||||
const DEFINE_OPTIONS = 'defineOptions'
|
||||
const DEFINE_SLOTS = 'defineSlots'
|
||||
const DEFINE_MODEL = 'defineModel'
|
||||
|
||||
const isBuiltInDir = makeMap(
|
||||
`once,memo,if,for,else,else-if,slot,text,html,on,bind,model,show,cloak,is`
|
||||
|
@ -119,13 +120,16 @@ export interface SFCScriptCompileOptions {
|
|||
* options passed to `compiler-dom`.
|
||||
*/
|
||||
templateOptions?: Partial<SFCTemplateCompileOptions>
|
||||
|
||||
/**
|
||||
* Hoist <script setup> static constants.
|
||||
* - Only enables when one `<script setup>` exists.
|
||||
* @default true
|
||||
*/
|
||||
hoistStatic?: boolean
|
||||
/**
|
||||
* (**Experimental**) Enable macro `defineModel`
|
||||
*/
|
||||
defineModel?: boolean
|
||||
}
|
||||
|
||||
export interface ImportBinding {
|
||||
|
@ -150,6 +154,11 @@ type PropsDeclType = FromNormalScript<TSTypeLiteral | TSInterfaceBody>
|
|||
type EmitsDeclType = FromNormalScript<
|
||||
TSFunctionType | TSTypeLiteral | TSInterfaceBody
|
||||
>
|
||||
interface ModelDecl {
|
||||
type: TSType | undefined
|
||||
options: string | undefined
|
||||
identifier: string | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile `<script setup>`
|
||||
|
@ -164,6 +173,7 @@ export function compileScript(
|
|||
// feature flags
|
||||
// TODO remove in 3.4
|
||||
const enableReactivityTransform = !!options.reactivityTransform
|
||||
const enableDefineModel = !!options.defineModel
|
||||
const isProd = !!options.isProd
|
||||
const genSourceMap = options.sourceMap !== false
|
||||
const hoistStatic = options.hoistStatic !== false && !script
|
||||
|
@ -314,6 +324,7 @@ export function compileScript(
|
|||
let hasDefaultExportRender = false
|
||||
let hasDefineOptionsCall = false
|
||||
let hasDefineSlotsCall = false
|
||||
let hasDefineModelCall = false
|
||||
let propsRuntimeDecl: Node | undefined
|
||||
let propsRuntimeDefaults: Node | undefined
|
||||
let propsDestructureDecl: Node | undefined
|
||||
|
@ -325,6 +336,7 @@ export function compileScript(
|
|||
let emitsTypeDecl: EmitsDeclType | undefined
|
||||
let emitIdentifier: string | undefined
|
||||
let optionsRuntimeDecl: Node | undefined
|
||||
let modelDecls: Record<string, ModelDecl> = {}
|
||||
let hasAwait = false
|
||||
let hasInlinedSsrRenderFn = false
|
||||
// props/emits declared via types
|
||||
|
@ -616,6 +628,79 @@ export function compileScript(
|
|||
return true
|
||||
}
|
||||
|
||||
function processDefineModel(node: Node, declId?: LVal): boolean {
|
||||
if (!enableDefineModel || !isCallOf(node, DEFINE_MODEL)) {
|
||||
return false
|
||||
}
|
||||
hasDefineModelCall = true
|
||||
|
||||
const type =
|
||||
(node.typeParameters && node.typeParameters.params[0]) || undefined
|
||||
let modelName: string
|
||||
let options: Node | undefined
|
||||
const arg0 = node.arguments[0] && unwrapTSNode(node.arguments[0])
|
||||
if (arg0 && arg0.type === 'StringLiteral') {
|
||||
modelName = arg0.value
|
||||
options = node.arguments[1]
|
||||
} else {
|
||||
modelName = 'modelValue'
|
||||
options = arg0
|
||||
}
|
||||
|
||||
if (modelDecls[modelName]) {
|
||||
error(`duplicate model name ${JSON.stringify(modelName)}`, node)
|
||||
}
|
||||
|
||||
const optionsString = options
|
||||
? s.slice(startOffset + options.start!, startOffset + options.end!)
|
||||
: undefined
|
||||
|
||||
modelDecls[modelName] = {
|
||||
type,
|
||||
options: optionsString,
|
||||
identifier:
|
||||
declId && declId.type === 'Identifier' ? declId.name : undefined
|
||||
}
|
||||
|
||||
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 = `{ ${s.slice(
|
||||
startOffset + local.start!,
|
||||
startOffset + local.end!
|
||||
)} }`
|
||||
} else {
|
||||
for (const p of options.properties) {
|
||||
if (p.type === 'SpreadElement' || p.computed) {
|
||||
runtimeOptions = optionsString!
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
runtimeOptions = optionsString!
|
||||
}
|
||||
}
|
||||
|
||||
s.overwrite(
|
||||
startOffset + node.start!,
|
||||
startOffset + node.end!,
|
||||
`${helper('useModel')}(__props, ${JSON.stringify(modelName)}${
|
||||
runtimeOptions ? `, ${runtimeOptions}` : ``
|
||||
})`
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function getAstBody(): Statement[] {
|
||||
return scriptAst
|
||||
? [...scriptSetupAst.body, ...scriptAst.body]
|
||||
|
@ -883,18 +968,25 @@ export function compileScript(
|
|||
)
|
||||
}
|
||||
|
||||
function genRuntimeProps(props: Record<string, PropTypeData>) {
|
||||
const keys = Object.keys(props)
|
||||
if (!keys.length) {
|
||||
return ``
|
||||
function concatStrings(strs: Array<string | null | undefined | false>) {
|
||||
return strs.filter((s): s is string => !!s).join(', ')
|
||||
}
|
||||
|
||||
function genRuntimeProps() {
|
||||
function genPropsFromTS() {
|
||||
const keys = Object.keys(typeDeclaredProps)
|
||||
if (!keys.length) return
|
||||
|
||||
const hasStaticDefaults = hasStaticWithDefaults()
|
||||
const scriptSetupSource = scriptSetup!.content
|
||||
let propsDecls = `{
|
||||
${keys
|
||||
.map(key => {
|
||||
let defaultString: string | undefined
|
||||
const destructured = genDestructuredDefaultValue(key, props[key].type)
|
||||
const destructured = genDestructuredDefaultValue(
|
||||
key,
|
||||
typeDeclaredProps[key].type
|
||||
)
|
||||
if (destructured) {
|
||||
defaultString = `default: ${destructured.valueString}${
|
||||
destructured.needSkipFactory ? `, skipFactory: true` : ``
|
||||
|
@ -924,13 +1016,14 @@ export function compileScript(
|
|||
}
|
||||
}
|
||||
|
||||
const { type, required, skipCheck } = props[key]
|
||||
const { type, required, skipCheck } = typeDeclaredProps[key]
|
||||
if (!isProd) {
|
||||
return `${key}: { type: ${toRuntimeTypeString(
|
||||
type
|
||||
)}, required: ${required}${skipCheck ? ', skipCheck: true' : ''}${
|
||||
defaultString ? `, ${defaultString}` : ``
|
||||
} }`
|
||||
return `${key}: { ${concatStrings([
|
||||
`type: ${toRuntimeTypeString(type)}`,
|
||||
`required: ${required}`,
|
||||
skipCheck && 'skipCheck: true',
|
||||
defaultString
|
||||
])} }`
|
||||
} else if (
|
||||
type.some(
|
||||
el =>
|
||||
|
@ -941,12 +1034,13 @@ export function compileScript(
|
|||
// #4783 for boolean, should keep the type
|
||||
// #7111 for function, if default value exists or it's not static, should keep it
|
||||
// in production
|
||||
return `${key}: { type: ${toRuntimeTypeString(type)}${
|
||||
defaultString ? `, ${defaultString}` : ``
|
||||
} }`
|
||||
return `${key}: { ${concatStrings([
|
||||
`type: ${toRuntimeTypeString(type)}`,
|
||||
defaultString
|
||||
])} }`
|
||||
} else {
|
||||
// production: checks are useless
|
||||
return `${key}: ${defaultString ? `{ ${defaultString} }` : 'null'}`
|
||||
return `${key}: ${defaultString ? `{ ${defaultString} }` : `{}`}`
|
||||
}
|
||||
})
|
||||
.join(',\n ')}\n }`
|
||||
|
@ -958,7 +1052,86 @@ export function compileScript(
|
|||
)})`
|
||||
}
|
||||
|
||||
return `\n props: ${propsDecls},`
|
||||
return propsDecls
|
||||
}
|
||||
|
||||
function genModels() {
|
||||
if (!hasDefineModelCall) return
|
||||
|
||||
let modelPropsDecl = ''
|
||||
for (const [name, { type, options }] of Object.entries(modelDecls)) {
|
||||
let skipCheck = false
|
||||
|
||||
let runtimeTypes = type && inferRuntimeType(type, declaredTypes)
|
||||
if (runtimeTypes) {
|
||||
const hasUnknownType = runtimeTypes.includes(UNKNOWN_TYPE)
|
||||
|
||||
runtimeTypes = runtimeTypes.filter(el => {
|
||||
if (el === UNKNOWN_TYPE) return false
|
||||
return isProd
|
||||
? el === 'Boolean' || (el === 'Function' && options)
|
||||
: true
|
||||
})
|
||||
skipCheck = !isProd && hasUnknownType && runtimeTypes.length > 0
|
||||
}
|
||||
|
||||
let runtimeType =
|
||||
(runtimeTypes &&
|
||||
runtimeTypes.length > 0 &&
|
||||
toRuntimeTypeString(runtimeTypes)) ||
|
||||
undefined
|
||||
|
||||
const codegenOptions = concatStrings([
|
||||
runtimeType && `type: ${runtimeType}`,
|
||||
skipCheck && 'skipCheck: true'
|
||||
])
|
||||
|
||||
let decl: string
|
||||
if (runtimeType && options) {
|
||||
decl = isTS
|
||||
? `{ ${codegenOptions}, ...${options} }`
|
||||
: `Object.assign({ ${codegenOptions} }, ${options})`
|
||||
} else {
|
||||
decl = options || (runtimeType ? `{ ${codegenOptions} }` : '{}')
|
||||
}
|
||||
modelPropsDecl += `\n ${JSON.stringify(name)}: ${decl},`
|
||||
}
|
||||
return `{${modelPropsDecl}\n }`
|
||||
}
|
||||
|
||||
let propsDecls: undefined | string
|
||||
if (propsRuntimeDecl) {
|
||||
propsDecls = scriptSetup!.content
|
||||
.slice(propsRuntimeDecl.start!, propsRuntimeDecl.end!)
|
||||
.trim()
|
||||
if (propsDestructureDecl) {
|
||||
const defaults: string[] = []
|
||||
for (const key in propsDestructuredBindings) {
|
||||
const d = genDestructuredDefaultValue(key)
|
||||
if (d)
|
||||
defaults.push(
|
||||
`${key}: ${d.valueString}${
|
||||
d.needSkipFactory ? `, __skip_${key}: true` : ``
|
||||
}`
|
||||
)
|
||||
}
|
||||
if (defaults.length) {
|
||||
propsDecls = `${helper(
|
||||
`mergeDefaults`
|
||||
)}(${propsDecls}, {\n ${defaults.join(',\n ')}\n})`
|
||||
}
|
||||
}
|
||||
} else if (propsTypeDecl) {
|
||||
propsDecls = genPropsFromTS()
|
||||
}
|
||||
|
||||
const modelsDecls = genModels()
|
||||
|
||||
if (propsDecls && modelsDecls) {
|
||||
return `${helper('mergeModels')}(${propsDecls}, ${modelsDecls})`
|
||||
} else {
|
||||
return modelsDecls || propsDecls
|
||||
}
|
||||
}
|
||||
|
||||
function genDestructuredDefaultValue(
|
||||
|
@ -1058,6 +1231,34 @@ export function compileScript(
|
|||
}
|
||||
}
|
||||
|
||||
function genRuntimeEmits() {
|
||||
function genEmitsFromTS() {
|
||||
return typeDeclaredEmits.size
|
||||
? `[${Array.from(typeDeclaredEmits)
|
||||
.map(k => JSON.stringify(k))
|
||||
.join(', ')}]`
|
||||
: ``
|
||||
}
|
||||
|
||||
let emitsDecl = ''
|
||||
if (emitsRuntimeDecl) {
|
||||
emitsDecl = scriptSetup!.content
|
||||
.slice(emitsRuntimeDecl.start!, emitsRuntimeDecl.end!)
|
||||
.trim()
|
||||
} else if (emitsTypeDecl) {
|
||||
emitsDecl = genEmitsFromTS()
|
||||
}
|
||||
if (hasDefineModelCall) {
|
||||
let modelEmitsDecl = `[${Object.keys(modelDecls)
|
||||
.map(n => JSON.stringify(`update:${n}`))
|
||||
.join(', ')}]`
|
||||
emitsDecl = emitsDecl
|
||||
? `${helper('mergeModels')}(${emitsDecl}, ${modelEmitsDecl})`
|
||||
: modelEmitsDecl
|
||||
}
|
||||
return emitsDecl
|
||||
}
|
||||
|
||||
// 0. parse both <script> and <script setup> blocks
|
||||
const scriptAst =
|
||||
script &&
|
||||
|
@ -1345,6 +1546,8 @@ export function compileScript(
|
|||
callee.end! + startOffset,
|
||||
'__expose'
|
||||
)
|
||||
} else {
|
||||
processDefineModel(expr)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1370,7 +1573,9 @@ export function compileScript(
|
|||
processWithDefaults(init, decl.id)
|
||||
const isDefineEmits =
|
||||
!isDefineProps && processDefineEmits(init, decl.id)
|
||||
!isDefineEmits && processDefineSlots(init, decl.id)
|
||||
!isDefineEmits &&
|
||||
(processDefineSlots(init, decl.id) ||
|
||||
processDefineModel(init, decl.id))
|
||||
|
||||
if (isDefineProps || isDefineEmits) {
|
||||
if (left === 1) {
|
||||
|
@ -1569,6 +1774,9 @@ export function compileScript(
|
|||
for (const key in typeDeclaredProps) {
|
||||
bindingMetadata[key] = BindingTypes.PROPS
|
||||
}
|
||||
for (const key in modelDecls) {
|
||||
bindingMetadata[key] = BindingTypes.PROPS
|
||||
}
|
||||
// props aliases
|
||||
if (propsDestructureDecl) {
|
||||
if (propsDestructureRestId) {
|
||||
|
@ -1787,38 +1995,12 @@ export function compileScript(
|
|||
if (hasInlinedSsrRenderFn) {
|
||||
runtimeOptions += `\n __ssrInlineRender: true,`
|
||||
}
|
||||
if (propsRuntimeDecl) {
|
||||
let declCode = scriptSetup.content
|
||||
.slice(propsRuntimeDecl.start!, propsRuntimeDecl.end!)
|
||||
.trim()
|
||||
if (propsDestructureDecl) {
|
||||
const defaults: string[] = []
|
||||
for (const key in propsDestructuredBindings) {
|
||||
const d = genDestructuredDefaultValue(key)
|
||||
if (d)
|
||||
defaults.push(
|
||||
`${key}: ${d.valueString}${
|
||||
d.needSkipFactory ? `, __skip_${key}: true` : ``
|
||||
}`
|
||||
)
|
||||
}
|
||||
if (defaults.length) {
|
||||
declCode = `${helper(
|
||||
`mergeDefaults`
|
||||
)}(${declCode}, {\n ${defaults.join(',\n ')}\n})`
|
||||
}
|
||||
}
|
||||
runtimeOptions += `\n props: ${declCode},`
|
||||
} else if (propsTypeDecl) {
|
||||
runtimeOptions += genRuntimeProps(typeDeclaredProps)
|
||||
}
|
||||
if (emitsRuntimeDecl) {
|
||||
runtimeOptions += `\n emits: ${scriptSetup.content
|
||||
.slice(emitsRuntimeDecl.start!, emitsRuntimeDecl.end!)
|
||||
.trim()},`
|
||||
} else if (emitsTypeDecl) {
|
||||
runtimeOptions += genRuntimeEmits(typeDeclaredEmits)
|
||||
}
|
||||
|
||||
const propsDecl = genRuntimeProps()
|
||||
if (propsDecl) runtimeOptions += `\n props: ${propsDecl},`
|
||||
|
||||
const emitsDecl = genRuntimeEmits()
|
||||
if (emitsDecl) runtimeOptions += `\n emits: ${emitsDecl},`
|
||||
|
||||
let definedOptions = ''
|
||||
if (optionsRuntimeDecl) {
|
||||
|
@ -1958,7 +2140,10 @@ function walkDeclaration(
|
|||
? BindingTypes.SETUP_REACTIVE_CONST
|
||||
: BindingTypes.SETUP_CONST
|
||||
} else if (isConst) {
|
||||
if (isCallOf(init, userImportAliases['ref'])) {
|
||||
if (
|
||||
isCallOf(init, userImportAliases['ref']) ||
|
||||
isCallOf(init, DEFINE_MODEL)
|
||||
) {
|
||||
bindingType = BindingTypes.SETUP_REF
|
||||
} else {
|
||||
bindingType = BindingTypes.SETUP_MAYBE_REF
|
||||
|
@ -2393,14 +2578,6 @@ function extractEventNames(
|
|||
}
|
||||
}
|
||||
|
||||
function genRuntimeEmits(emits: Set<string>) {
|
||||
return emits.size
|
||||
? `\n emits: [${Array.from(emits)
|
||||
.map(p => JSON.stringify(p))
|
||||
.join(', ')}],`
|
||||
: ``
|
||||
}
|
||||
|
||||
function canNeverBeRef(node: Node, userReactiveImport?: string): boolean {
|
||||
if (isCallOf(node, userReactiveImport)) {
|
||||
return true
|
||||
|
|
|
@ -6,9 +6,13 @@ import {
|
|||
withDefaults,
|
||||
Slots,
|
||||
defineSlots,
|
||||
VNode
|
||||
VNode,
|
||||
Ref,
|
||||
defineModel
|
||||
} from 'vue'
|
||||
import { describe, expectType } from './utils'
|
||||
import { defineComponent } from 'vue'
|
||||
import { useModel } from 'vue'
|
||||
|
||||
describe('defineProps w/ type declaration', () => {
|
||||
// type declaration
|
||||
|
@ -202,6 +206,72 @@ describe('defineSlots', () => {
|
|||
expectType<Slots>(slotsUntype)
|
||||
})
|
||||
|
||||
describe('defineModel', () => {
|
||||
// overload 1
|
||||
const modelValueRequired = defineModel<boolean>({ required: true })
|
||||
expectType<Ref<boolean>>(modelValueRequired)
|
||||
|
||||
// overload 2
|
||||
const modelValue = defineModel<string>()
|
||||
expectType<Ref<string | undefined>>(modelValue)
|
||||
modelValue.value = 'new value'
|
||||
|
||||
const modelValueDefault = defineModel<boolean>({ default: true })
|
||||
expectType<Ref<boolean>>(modelValueDefault)
|
||||
|
||||
// overload 3
|
||||
const countRequired = defineModel<number>('count', { required: false })
|
||||
expectType<Ref<number | undefined>>(countRequired)
|
||||
|
||||
// overload 4
|
||||
const count = defineModel<number>('count')
|
||||
expectType<Ref<number | undefined>>(count)
|
||||
|
||||
const countDefault = defineModel<number>('count', { default: 1 })
|
||||
expectType<Ref<number>>(countDefault)
|
||||
|
||||
// infer type from default
|
||||
const inferred = defineModel({ default: 123 })
|
||||
expectType<Ref<number | undefined>>(inferred)
|
||||
const inferredRequired = defineModel({ default: 123, required: true })
|
||||
expectType<Ref<number>>(inferredRequired)
|
||||
|
||||
// @ts-expect-error type / default mismatch
|
||||
defineModel<string>({ default: 123 })
|
||||
// @ts-expect-error unknown props option
|
||||
defineModel({ foo: 123 })
|
||||
|
||||
// accept defineModel-only options
|
||||
defineModel({ local: true })
|
||||
defineModel('foo', { local: true })
|
||||
})
|
||||
|
||||
describe('useModel', () => {
|
||||
defineComponent({
|
||||
props: ['foo'],
|
||||
setup(props) {
|
||||
const r = useModel(props, 'foo')
|
||||
expectType<Ref<any>>(r)
|
||||
|
||||
// @ts-expect-error
|
||||
useModel(props, 'bar')
|
||||
}
|
||||
})
|
||||
|
||||
defineComponent({
|
||||
props: {
|
||||
foo: String,
|
||||
bar: { type: Number, required: true },
|
||||
baz: { type: Boolean }
|
||||
},
|
||||
setup(props) {
|
||||
expectType<Ref<string | undefined>>(useModel(props, 'foo'))
|
||||
expectType<Ref<number>>(useModel(props, 'bar'))
|
||||
expectType<Ref<boolean>>(useModel(props, 'baz'))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('useAttrs', () => {
|
||||
const attrs = useAttrs()
|
||||
expectType<Record<string, unknown>>(attrs)
|
||||
|
|
|
@ -13,7 +13,9 @@ import {
|
|||
Suspense,
|
||||
computed,
|
||||
ComputedRef,
|
||||
shallowReactive
|
||||
shallowReactive,
|
||||
nextTick,
|
||||
ref
|
||||
} from '@vue/runtime-test'
|
||||
import {
|
||||
defineEmits,
|
||||
|
@ -24,7 +26,9 @@ import {
|
|||
useSlots,
|
||||
mergeDefaults,
|
||||
withAsyncContext,
|
||||
createPropsRestProxy
|
||||
createPropsRestProxy,
|
||||
mergeModels,
|
||||
useModel
|
||||
} from '../src/apiSetupHelpers'
|
||||
|
||||
describe('SFC <script setup> helpers', () => {
|
||||
|
@ -133,6 +137,149 @@ describe('SFC <script setup> helpers', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('mergeModels', () => {
|
||||
test('array syntax', () => {
|
||||
expect(mergeModels(['foo', 'bar'], ['baz'])).toMatchObject([
|
||||
'foo',
|
||||
'bar',
|
||||
'baz'
|
||||
])
|
||||
})
|
||||
|
||||
test('object syntax', () => {
|
||||
expect(
|
||||
mergeModels({ foo: null, bar: { required: true } }, ['baz'])
|
||||
).toMatchObject({
|
||||
foo: null,
|
||||
bar: { required: true },
|
||||
baz: {}
|
||||
})
|
||||
|
||||
expect(
|
||||
mergeModels(['baz'], { foo: null, bar: { required: true } })
|
||||
).toMatchObject({
|
||||
foo: null,
|
||||
bar: { required: true },
|
||||
baz: {}
|
||||
})
|
||||
})
|
||||
|
||||
test('overwrite', () => {
|
||||
expect(
|
||||
mergeModels(
|
||||
{ foo: null, bar: { required: true } },
|
||||
{ bar: {}, baz: {} }
|
||||
)
|
||||
).toMatchObject({
|
||||
foo: null,
|
||||
bar: {},
|
||||
baz: {}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('useModel', () => {
|
||||
test('basic', async () => {
|
||||
let foo: any
|
||||
const update = () => {
|
||||
foo.value = 'bar'
|
||||
}
|
||||
|
||||
const Comp = defineComponent({
|
||||
props: ['modelValue'],
|
||||
emits: ['update:modelValue'],
|
||||
setup(props) {
|
||||
foo = useModel(props, 'modelValue')
|
||||
},
|
||||
render() {}
|
||||
})
|
||||
|
||||
const msg = ref('')
|
||||
const setValue = vi.fn(v => (msg.value = v))
|
||||
const root = nodeOps.createElement('div')
|
||||
createApp(() =>
|
||||
h(Comp, {
|
||||
modelValue: msg.value,
|
||||
'onUpdate:modelValue': setValue
|
||||
})
|
||||
).mount(root)
|
||||
|
||||
expect(foo.value).toBe('')
|
||||
expect(msg.value).toBe('')
|
||||
expect(setValue).not.toBeCalled()
|
||||
|
||||
// update from child
|
||||
update()
|
||||
|
||||
await nextTick()
|
||||
expect(msg.value).toBe('bar')
|
||||
expect(foo.value).toBe('bar')
|
||||
expect(setValue).toBeCalledTimes(1)
|
||||
|
||||
// update from parent
|
||||
msg.value = 'qux'
|
||||
|
||||
await nextTick()
|
||||
expect(msg.value).toBe('qux')
|
||||
expect(foo.value).toBe('qux')
|
||||
expect(setValue).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
test('local', async () => {
|
||||
let foo: any
|
||||
const update = () => {
|
||||
foo.value = 'bar'
|
||||
}
|
||||
|
||||
const Comp = defineComponent({
|
||||
props: ['foo'],
|
||||
emits: ['update:foo'],
|
||||
setup(props) {
|
||||
foo = useModel(props, 'foo', { local: true })
|
||||
},
|
||||
render() {}
|
||||
})
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
const updateFoo = vi.fn()
|
||||
render(h(Comp, { 'onUpdate:foo': updateFoo }), root)
|
||||
|
||||
expect(foo.value).toBeUndefined()
|
||||
update()
|
||||
|
||||
expect(foo.value).toBe('bar')
|
||||
|
||||
await nextTick()
|
||||
expect(updateFoo).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
test('default value', async () => {
|
||||
let count: any
|
||||
const inc = () => {
|
||||
count.value++
|
||||
}
|
||||
const Comp = defineComponent({
|
||||
props: { count: { default: 0 } },
|
||||
emits: ['update:count'],
|
||||
setup(props) {
|
||||
count = useModel(props, 'count', { local: true })
|
||||
},
|
||||
render() {}
|
||||
})
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
const updateCount = vi.fn()
|
||||
render(h(Comp, { 'onUpdate:count': updateCount }), root)
|
||||
|
||||
expect(count.value).toBe(0)
|
||||
|
||||
inc()
|
||||
expect(count.value).toBe(1)
|
||||
await nextTick()
|
||||
expect(updateCount).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
test('createPropsRestProxy', () => {
|
||||
const original = shallowReactive({
|
||||
foo: 1,
|
||||
|
|
|
@ -3,7 +3,8 @@ import {
|
|||
isPromise,
|
||||
isFunction,
|
||||
Prettify,
|
||||
UnionToIntersection
|
||||
UnionToIntersection,
|
||||
extend
|
||||
} from '@vue/shared'
|
||||
import {
|
||||
getCurrentInstance,
|
||||
|
@ -12,7 +13,7 @@ import {
|
|||
createSetupContext,
|
||||
unsetCurrentInstance
|
||||
} from './component'
|
||||
import { EmitFn, EmitsOptions } from './componentEmits'
|
||||
import { EmitFn, EmitsOptions, ObjectEmitsOptions } from './componentEmits'
|
||||
import {
|
||||
ComponentOptionsMixin,
|
||||
ComponentOptionsWithoutProps,
|
||||
|
@ -22,10 +23,14 @@ import {
|
|||
import {
|
||||
ComponentPropsOptions,
|
||||
ComponentObjectPropsOptions,
|
||||
ExtractPropTypes
|
||||
ExtractPropTypes,
|
||||
NormalizedProps,
|
||||
PropOptions
|
||||
} from './componentProps'
|
||||
import { warn } from './warning'
|
||||
import { SlotsType, TypedSlots } from './componentSlots'
|
||||
import { Ref, ref } from '@vue/reactivity'
|
||||
import { watch } from './apiWatch'
|
||||
|
||||
// dev only
|
||||
const warnRuntimeUsage = (method: string) =>
|
||||
|
@ -200,11 +205,77 @@ export function defineOptions<
|
|||
|
||||
export function defineSlots<
|
||||
S extends Record<string, any> = Record<string, any>
|
||||
>(): // @ts-expect-error
|
||||
TypedSlots<SlotsType<S>> {
|
||||
>(): TypedSlots<SlotsType<S>> {
|
||||
if (__DEV__) {
|
||||
warnRuntimeUsage(`defineSlots`)
|
||||
}
|
||||
return null as any
|
||||
}
|
||||
|
||||
/**
|
||||
* (**Experimental**) Vue `<script setup>` compiler macro for declaring a
|
||||
* two-way binding prop that can be consumed via `v-model` from the parent
|
||||
* component. This will declare a prop with the same name and a corresponding
|
||||
* `update:propName` event.
|
||||
*
|
||||
* If the first argument is a string, it will be used as the prop name;
|
||||
* Otherwise the prop name will default to "modelValue". In both cases, you
|
||||
* can also pass an additional object which will be used as the prop's options.
|
||||
*
|
||||
* The options object can also specify an additional option, `local`. When set
|
||||
* to `true`, the ref can be locally mutated even if the parent did not pass
|
||||
* the matching `v-model`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // default model (consumed via `v-model`)
|
||||
* const modelValue = defineModel<string>()
|
||||
* modelValue.value = "hello"
|
||||
*
|
||||
* // default model with options
|
||||
* const modelValue = defineModel<stirng>({ required: true })
|
||||
*
|
||||
* // with specified name (consumed via `v-model:count`)
|
||||
* const count = defineModel<number>('count')
|
||||
* count.value++
|
||||
*
|
||||
* // with specified name and default value
|
||||
* const count = defineModel<number>('count', { default: 0 })
|
||||
*
|
||||
* // local mutable model, can be mutated locally
|
||||
* // even if the parent did not pass the matching `v-model`.
|
||||
* const count = defineModel<number>('count', { local: true, default: 0 })
|
||||
* ```
|
||||
*/
|
||||
export function defineModel<T>(
|
||||
options: { required: true } & PropOptions<T> & DefineModelOptions
|
||||
): Ref<T>
|
||||
export function defineModel<T>(
|
||||
options: { default: any } & PropOptions<T> & DefineModelOptions
|
||||
): Ref<T>
|
||||
export function defineModel<T>(
|
||||
options?: PropOptions<T> & DefineModelOptions
|
||||
): Ref<T | undefined>
|
||||
export function defineModel<T>(
|
||||
name: string,
|
||||
options: { required: true } & PropOptions<T> & DefineModelOptions
|
||||
): Ref<T>
|
||||
export function defineModel<T>(
|
||||
name: string,
|
||||
options: { default: any } & PropOptions<T> & DefineModelOptions
|
||||
): Ref<T>
|
||||
export function defineModel<T>(
|
||||
name: string,
|
||||
options?: PropOptions<T> & DefineModelOptions
|
||||
): Ref<T | undefined>
|
||||
export function defineModel(): any {
|
||||
if (__DEV__) {
|
||||
warnRuntimeUsage('defineModel')
|
||||
}
|
||||
}
|
||||
|
||||
interface DefineModelOptions {
|
||||
local?: boolean
|
||||
}
|
||||
|
||||
type NotUndefined<T> = T extends undefined ? never : T
|
||||
|
@ -268,6 +339,55 @@ export function useAttrs(): SetupContext['attrs'] {
|
|||
return getContext().attrs
|
||||
}
|
||||
|
||||
export function useModel<T extends Record<string, any>, K extends keyof T>(
|
||||
props: T,
|
||||
name: K,
|
||||
options?: { local?: boolean }
|
||||
): Ref<T[K]>
|
||||
export function useModel(
|
||||
props: Record<string, any>,
|
||||
name: string,
|
||||
options?: { local?: boolean }
|
||||
): Ref {
|
||||
const i = getCurrentInstance()!
|
||||
if (__DEV__ && !i) {
|
||||
warn(`useModel() called without active instance.`)
|
||||
return ref() as any
|
||||
}
|
||||
|
||||
if (__DEV__ && !(i.propsOptions[0] as NormalizedProps)[name]) {
|
||||
warn(`useModel() called with prop "${name}" which is not declared.`)
|
||||
return ref() as any
|
||||
}
|
||||
|
||||
if (options && options.local) {
|
||||
const proxy = ref<any>(props[name])
|
||||
|
||||
watch(
|
||||
() => props[name],
|
||||
v => (proxy.value = v)
|
||||
)
|
||||
|
||||
watch(proxy, value => {
|
||||
if (value !== props[name]) {
|
||||
i.emit(`update:${name}`, value)
|
||||
}
|
||||
})
|
||||
|
||||
return proxy
|
||||
} else {
|
||||
return {
|
||||
__v_isRef: true,
|
||||
get value() {
|
||||
return props[name]
|
||||
},
|
||||
set value(value) {
|
||||
i.emit(`update:${name}`, value)
|
||||
}
|
||||
} as any
|
||||
}
|
||||
}
|
||||
|
||||
function getContext(): SetupContext {
|
||||
const i = getCurrentInstance()!
|
||||
if (__DEV__ && !i) {
|
||||
|
@ -276,6 +396,15 @@ function getContext(): SetupContext {
|
|||
return i.setupContext || (i.setupContext = createSetupContext(i))
|
||||
}
|
||||
|
||||
function normalizePropsOrEmits(props: ComponentPropsOptions | EmitsOptions) {
|
||||
return isArray(props)
|
||||
? props.reduce(
|
||||
(normalized, p) => ((normalized[p] = {}), normalized),
|
||||
{} as ComponentObjectPropsOptions | ObjectEmitsOptions
|
||||
)
|
||||
: props
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime helper for merging default declarations. Imported by compiled code
|
||||
* only.
|
||||
|
@ -285,12 +414,7 @@ export function mergeDefaults(
|
|||
raw: ComponentPropsOptions,
|
||||
defaults: Record<string, any>
|
||||
): ComponentObjectPropsOptions {
|
||||
const props = isArray(raw)
|
||||
? raw.reduce(
|
||||
(normalized, p) => ((normalized[p] = {}), normalized),
|
||||
{} as ComponentObjectPropsOptions
|
||||
)
|
||||
: raw
|
||||
const props = normalizePropsOrEmits(raw)
|
||||
for (const key in defaults) {
|
||||
if (key.startsWith('__skip')) continue
|
||||
let opt = props[key]
|
||||
|
@ -312,6 +436,20 @@ export function mergeDefaults(
|
|||
return props
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime helper for merging model declarations.
|
||||
* Imported by compiled code only.
|
||||
* @internal
|
||||
*/
|
||||
export function mergeModels(
|
||||
a: ComponentPropsOptions | EmitsOptions,
|
||||
b: ComponentPropsOptions | EmitsOptions
|
||||
) {
|
||||
if (!a || !b) return a || b
|
||||
if (isArray(a) && isArray(b)) return a.concat(b)
|
||||
return extend({}, normalizePropsOrEmits(a), normalizePropsOrEmits(b))
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to create a proxy for the rest element when destructuring props with
|
||||
* defineProps().
|
||||
|
|
|
@ -627,7 +627,7 @@ function validateProp(
|
|||
return
|
||||
}
|
||||
// missing but optional
|
||||
if (value == null && !prop.required) {
|
||||
if (value == null && !required) {
|
||||
return
|
||||
}
|
||||
// type check
|
||||
|
|
|
@ -71,9 +71,12 @@ export {
|
|||
defineExpose,
|
||||
defineOptions,
|
||||
defineSlots,
|
||||
defineModel,
|
||||
withDefaults,
|
||||
useModel,
|
||||
// internal
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
createPropsRestProxy,
|
||||
withAsyncContext
|
||||
} from './apiSetupHelpers'
|
||||
|
|
|
@ -5,6 +5,7 @@ type _defineEmits = typeof defineEmits
|
|||
type _defineExpose = typeof defineExpose
|
||||
type _defineOptions = typeof defineOptions
|
||||
type _defineSlots = typeof defineSlots
|
||||
type _defineModel = typeof defineModel
|
||||
type _withDefaults = typeof withDefaults
|
||||
|
||||
declare global {
|
||||
|
@ -13,5 +14,6 @@ declare global {
|
|||
const defineExpose: _defineExpose
|
||||
const defineOptions: _defineOptions
|
||||
const defineSlots: _defineSlots
|
||||
const defineModel: _defineModel
|
||||
const withDefaults: _withDefaults
|
||||
}
|
||||
|
|
|
@ -37,7 +37,8 @@ const sfcOptions = {
|
|||
script: {
|
||||
inlineTemplate: !useDevMode.value,
|
||||
isProd: !useDevMode.value,
|
||||
reactivityTransform: true
|
||||
reactivityTransform: true,
|
||||
defineModel: true
|
||||
},
|
||||
style: {
|
||||
isProd: !useDevMode.value
|
||||
|
|
Loading…
Reference in New Issue