Skip to content

Commit

Permalink
feat(defineModel): support modifiers and transformers
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Dec 26, 2023
1 parent d7bb32f commit a772031
Show file tree
Hide file tree
Showing 7 changed files with 303 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ exports[`defineModel() > basic usage 1`] = `
export default {
props: {
"modelValue": { required: true },
"modelModifiers": {},
"count": {},
"countModifiers": {},
"toString": { type: Function },
"toStringModifiers": {},
},
emits: ["update:modelValue", "update:count", "update:toString"],
setup(__props, { expose: __expose }) {
Expand All @@ -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`] = `
"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 }) {
Expand All @@ -49,6 +98,7 @@ exports[`defineModel() > w/ defineProps and defineEmits 1`] = `
export default {
props: /*#__PURE__*/_mergeModels({ foo: String }, {
"modelValue": { default: 0 },
"modelModifiers": {},
}),
emits: /*#__PURE__*/_mergeModels(['change'], ["update:modelValue"]),
setup(__props, { expose: __expose }) {
Expand All @@ -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`] = `
"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 }) {
Expand All @@ -127,10 +149,15 @@ exports[`defineModel() > w/ types, production mode 1`] = `
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 }) {
Expand Down
64 changes: 41 additions & 23 deletions packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ describe('defineModel()', () => {
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')
Expand All @@ -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', () => {
const { content, bindings } = compile(
`
Expand All @@ -115,6 +93,7 @@ describe('defineModel()', () => {
)
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 } }',
Expand Down Expand Up @@ -176,4 +155,43 @@ describe('defineModel()', () => {
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 }, })`,
)
})
})
70 changes: 43 additions & 27 deletions packages/compiler-sfc/src/script/defineModel.ts
Original file line number Diff line number Diff line change
@@ -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 { inferRuntimeType } from './resolveType'
import {
Expand Down Expand Up @@ -45,42 +45,52 @@ export function processDefineModel(
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!,
Expand Down Expand Up @@ -133,6 +143,12 @@ export function genModelProps(ctx: ScriptCompileContext) {
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 }`
}
31 changes: 31 additions & 0 deletions packages/dts-test/setupHelpers.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,37 @@ describe('defineModel', () => {
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
Expand Down
Loading

0 comments on commit a772031

Please sign in to comment.