type Ref,
type Slots,
type VNode,
+ defineComponent,
defineEmits,
defineModel,
defineProps,
defineSlots,
toRefs,
useAttrs,
+ useModel,
useSlots,
withDefaults,
} from 'vue'
import { describe, expectType } from './utils'
-import { defineComponent } from 'vue'
-import { useModel } from 'vue'
describe('defineProps w/ type declaration', () => {
// type declaration
import {
type ComponentInternalInstance,
type ComputedRef,
- Fragment,
- type Ref,
type SetupContext,
Suspense,
computed,
createApp,
- createBlock,
- createElementBlock,
- createElementVNode,
- createVNode,
defineComponent,
getCurrentInstance,
h,
- nextTick,
nodeOps,
onMounted,
- openBlock,
- ref,
render,
serializeInner,
shallowReactive,
- watch,
} from '@vue/runtime-test'
import {
createPropsRestProxy,
mergeDefaults,
mergeModels,
useAttrs,
- useModel,
useSlots,
withAsyncContext,
withDefaults,
})
})
- describe('useModel', () => {
- test('basic', async () => {
- let foo: any
- const update = () => {
- foo.value = 'bar'
- }
-
- const compRender = vi.fn()
- const Comp = defineComponent({
- props: ['modelValue'],
- emits: ['update:modelValue'],
- setup(props) {
- foo = useModel(props, 'modelValue')
- return () => {
- compRender()
- return foo.value
- }
- },
- })
-
- 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()
- expect(compRender).toBeCalledTimes(1)
- expect(serializeInner(root)).toBe('')
-
- // update from child
- update()
-
- await nextTick()
- expect(msg.value).toBe('bar')
- expect(foo.value).toBe('bar')
- expect(setValue).toBeCalledTimes(1)
- expect(compRender).toBeCalledTimes(2)
- expect(serializeInner(root)).toBe('bar')
-
- // update from parent
- msg.value = 'qux'
- expect(msg.value).toBe('qux')
-
- await nextTick()
- expect(msg.value).toBe('qux')
- expect(foo.value).toBe('qux')
- expect(setValue).toBeCalledTimes(1)
- expect(compRender).toBeCalledTimes(3)
- expect(serializeInner(root)).toBe('qux')
- })
-
- test('without parent value (local mutation)', async () => {
- let foo: any
- const update = () => {
- foo.value = 'bar'
- }
-
- const compRender = vi.fn()
- const Comp = defineComponent({
- props: ['foo'],
- emits: ['update:foo'],
- setup(props) {
- foo = useModel(props, 'foo')
- return () => {
- compRender()
- return foo.value
- }
- },
- })
-
- const root = nodeOps.createElement('div')
- const updateFoo = vi.fn()
- render(h(Comp, { 'onUpdate:foo': updateFoo }), root)
- expect(compRender).toBeCalledTimes(1)
- expect(serializeInner(root)).toBe('<!---->')
-
- expect(foo.value).toBeUndefined()
- update()
- // when parent didn't provide value, local mutation is enabled
- expect(foo.value).toBe('bar')
-
- await nextTick()
- expect(updateFoo).toBeCalledTimes(1)
- expect(compRender).toBeCalledTimes(2)
- expect(serializeInner(root)).toBe('bar')
- })
-
- test('without parent listener (local mutation)', async () => {
- let foo: any
- const update = () => {
- foo.value = 'bar'
- }
-
- const compRender = vi.fn()
- const Comp = defineComponent({
- props: ['foo'],
- emits: ['update:foo'],
- setup(props) {
- foo = useModel(props, 'foo')
- return () => {
- compRender()
- return foo.value
- }
- },
- })
-
- const root = nodeOps.createElement('div')
- // provide initial value
- render(h(Comp, { foo: 'initial' }), root)
- expect(compRender).toBeCalledTimes(1)
- expect(serializeInner(root)).toBe('initial')
-
- expect(foo.value).toBe('initial')
- update()
- // when parent didn't provide value, local mutation is enabled
- expect(foo.value).toBe('bar')
-
- await nextTick()
- expect(compRender).toBeCalledTimes(2)
- expect(serializeInner(root)).toBe('bar')
- })
-
- test('kebab-case v-model (should not be local)', async () => {
- let foo: any
-
- const compRender = vi.fn()
- const Comp = defineComponent({
- props: ['fooBar'],
- emits: ['update:fooBar'],
- setup(props) {
- foo = useModel(props, 'fooBar')
- return () => {
- compRender()
- return foo.value
- }
- },
- })
-
- const updateFooBar = vi.fn()
- const root = nodeOps.createElement('div')
- // v-model:foo-bar compiles to foo-bar and onUpdate:fooBar
- render(
- h(Comp, { 'foo-bar': 'initial', 'onUpdate:fooBar': updateFooBar }),
- root,
- )
- expect(compRender).toBeCalledTimes(1)
- expect(serializeInner(root)).toBe('initial')
-
- expect(foo.value).toBe('initial')
- foo.value = 'bar'
- // should not be using local mode, so nothing should actually change
- expect(foo.value).toBe('initial')
-
- await nextTick()
- expect(compRender).toBeCalledTimes(1)
- expect(updateFooBar).toBeCalledTimes(1)
- expect(updateFooBar).toHaveBeenCalledWith('bar')
- expect(foo.value).toBe('initial')
- expect(serializeInner(root)).toBe('initial')
- })
-
- test('kebab-case update listener (should not be local)', async () => {
- let foo: any
-
- const compRender = vi.fn()
- const Comp = defineComponent({
- props: ['fooBar'],
- emits: ['update:fooBar'],
- setup(props) {
- foo = useModel(props, 'fooBar')
- return () => {
- compRender()
- return foo.value
- }
- },
- })
-
- const updateFooBar = vi.fn()
- const root = nodeOps.createElement('div')
- // The template compiler won't create hyphenated listeners, but it could have been passed manually
- render(
- h(Comp, { 'foo-bar': 'initial', 'onUpdate:foo-bar': updateFooBar }),
- root,
- )
- expect(compRender).toBeCalledTimes(1)
- expect(serializeInner(root)).toBe('initial')
-
- expect(foo.value).toBe('initial')
- foo.value = 'bar'
- // should not be using local mode, so nothing should actually change
- expect(foo.value).toBe('initial')
-
- await nextTick()
- expect(compRender).toBeCalledTimes(1)
- expect(updateFooBar).toBeCalledTimes(1)
- expect(updateFooBar).toHaveBeenCalledWith('bar')
- expect(foo.value).toBe('initial')
- expect(serializeInner(root)).toBe('initial')
- })
-
- test('default value', async () => {
- let count: any
- const inc = () => {
- count.value++
- }
-
- const compRender = vi.fn()
- const Comp = defineComponent({
- props: { count: { default: 0 } },
- emits: ['update:count'],
- setup(props) {
- count = useModel(props, 'count')
- return () => {
- compRender()
- return count.value
- }
- },
- })
-
- const root = nodeOps.createElement('div')
- const updateCount = vi.fn()
- render(h(Comp, { 'onUpdate:count': updateCount }), root)
- expect(compRender).toBeCalledTimes(1)
- expect(serializeInner(root)).toBe('0')
-
- expect(count.value).toBe(0)
-
- inc()
- // when parent didn't provide value, local mutation is enabled
- expect(count.value).toBe(1)
-
- await nextTick()
-
- expect(updateCount).toBeCalledTimes(1)
- expect(compRender).toBeCalledTimes(2)
- expect(serializeInner(root)).toBe('1')
- })
-
- test('parent limiting child value', async () => {
- let childCount: Ref<number>
-
- const compRender = vi.fn()
- const Comp = defineComponent({
- props: ['count'],
- emits: ['update:count'],
- setup(props) {
- childCount = useModel(props, 'count')
- return () => {
- compRender()
- return childCount.value
- }
- },
- })
-
- const Parent = defineComponent({
- setup() {
- const count = ref(0)
- watch(count, () => {
- if (count.value < 0) {
- count.value = 0
- }
- })
- return () =>
- h(Comp, {
- count: count.value,
- 'onUpdate:count': val => {
- count.value = val
- },
- })
- },
- })
-
- const root = nodeOps.createElement('div')
- render(h(Parent), root)
- expect(serializeInner(root)).toBe('0')
-
- // child update
- childCount!.value = 1
- // not yet updated
- expect(childCount!.value).toBe(0)
-
- await nextTick()
- expect(childCount!.value).toBe(1)
- expect(serializeInner(root)).toBe('1')
-
- // child update to invalid value
- childCount!.value = -1
- // not yet updated
- expect(childCount!.value).toBe(1)
-
- await nextTick()
- // limited to 0 by parent
- expect(childCount!.value).toBe(0)
- expect(serializeInner(root)).toBe('0')
- })
-
- test('has parent value -> no parent value', async () => {
- let childCount: Ref<number>
-
- const compRender = vi.fn()
- const Comp = defineComponent({
- props: ['count'],
- emits: ['update:count'],
- setup(props) {
- childCount = useModel(props, 'count')
- return () => {
- compRender()
- return childCount.value
- }
- },
- })
-
- const toggle = ref(true)
- const Parent = defineComponent({
- setup() {
- const count = ref(0)
- return () =>
- toggle.value
- ? h(Comp, {
- count: count.value,
- 'onUpdate:count': val => {
- count.value = val
- },
- })
- : h(Comp)
- },
- })
-
- const root = nodeOps.createElement('div')
- render(h(Parent), root)
- expect(serializeInner(root)).toBe('0')
-
- // child update
- childCount!.value = 1
- // not yet updated
- expect(childCount!.value).toBe(0)
-
- await nextTick()
- expect(childCount!.value).toBe(1)
- expect(serializeInner(root)).toBe('1')
-
- // parent change
- toggle.value = false
-
- await nextTick()
- // localValue should be reset
- expect(childCount!.value).toBeUndefined()
- expect(serializeInner(root)).toBe('<!---->')
-
- // child local mutation should continue to work
- childCount!.value = 2
- expect(childCount!.value).toBe(2)
-
- await nextTick()
- expect(serializeInner(root)).toBe('2')
- })
-
- // #9838
- test('pass modelValue to slot (optimized mode) ', async () => {
- let foo: any
- const update = () => {
- foo.value = 'bar'
- }
-
- const Comp = {
- render(this: any) {
- return this.$slots.default()
- },
- }
-
- const childRender = vi.fn()
- const slotRender = vi.fn()
- const Child = defineComponent({
- props: ['modelValue'],
- emits: ['update:modelValue'],
- setup(props) {
- foo = useModel(props, 'modelValue')
- return () => {
- childRender()
- return (
- openBlock(),
- createElementBlock(Fragment, null, [
- createVNode(Comp, null, {
- default: () => {
- slotRender()
- return createElementVNode('div', null, foo.value)
- },
- _: 1 /* STABLE */,
- }),
- ])
- )
- }
- },
- })
-
- const msg = ref('')
- const setValue = vi.fn(v => (msg.value = v))
- const root = nodeOps.createElement('div')
- createApp({
- render() {
- return (
- openBlock(),
- createBlock(
- Child,
- {
- modelValue: msg.value,
- 'onUpdate:modelValue': setValue,
- },
- null,
- 8 /* PROPS */,
- ['modelValue'],
- )
- )
- },
- }).mount(root)
-
- expect(foo.value).toBe('')
- expect(msg.value).toBe('')
- expect(setValue).not.toBeCalled()
- expect(childRender).toBeCalledTimes(1)
- expect(slotRender).toBeCalledTimes(1)
- expect(serializeInner(root)).toBe('<div></div>')
-
- // update from child
- update()
-
- await nextTick()
- expect(msg.value).toBe('bar')
- expect(foo.value).toBe('bar')
- expect(setValue).toBeCalledTimes(1)
- expect(childRender).toBeCalledTimes(2)
- 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', () => {
const original = shallowReactive({
foo: 1,
--- /dev/null
+import {
+ Fragment,
+ type Ref,
+ createApp,
+ createBlock,
+ createElementBlock,
+ createElementVNode,
+ createVNode,
+ defineComponent,
+ h,
+ nextTick,
+ nodeOps,
+ openBlock,
+ ref,
+ render,
+ serializeInner,
+ watch,
+} from '@vue/runtime-test'
+import { useModel } from '../../src/helpers/useModel'
+
+describe('useModel', () => {
+ test('basic', async () => {
+ let foo: any
+ const update = () => {
+ foo.value = 'bar'
+ }
+
+ const compRender = vi.fn()
+ const Comp = defineComponent({
+ props: ['modelValue'],
+ emits: ['update:modelValue'],
+ setup(props) {
+ foo = useModel(props, 'modelValue')
+ return () => {
+ compRender()
+ return foo.value
+ }
+ },
+ })
+
+ 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()
+ expect(compRender).toBeCalledTimes(1)
+ expect(serializeInner(root)).toBe('')
+
+ // update from child
+ update()
+
+ await nextTick()
+ expect(msg.value).toBe('bar')
+ expect(foo.value).toBe('bar')
+ expect(setValue).toBeCalledTimes(1)
+ expect(compRender).toBeCalledTimes(2)
+ expect(serializeInner(root)).toBe('bar')
+
+ // update from parent
+ msg.value = 'qux'
+ expect(msg.value).toBe('qux')
+
+ await nextTick()
+ expect(msg.value).toBe('qux')
+ expect(foo.value).toBe('qux')
+ expect(setValue).toBeCalledTimes(1)
+ expect(compRender).toBeCalledTimes(3)
+ expect(serializeInner(root)).toBe('qux')
+ })
+
+ test('without parent value (local mutation)', async () => {
+ let foo: any
+ const update = () => {
+ foo.value = 'bar'
+ }
+
+ const compRender = vi.fn()
+ const Comp = defineComponent({
+ props: ['foo'],
+ emits: ['update:foo'],
+ setup(props) {
+ foo = useModel(props, 'foo')
+ return () => {
+ compRender()
+ return foo.value
+ }
+ },
+ })
+
+ const root = nodeOps.createElement('div')
+ const updateFoo = vi.fn()
+ render(h(Comp, { 'onUpdate:foo': updateFoo }), root)
+ expect(compRender).toBeCalledTimes(1)
+ expect(serializeInner(root)).toBe('<!---->')
+
+ expect(foo.value).toBeUndefined()
+ update()
+ // when parent didn't provide value, local mutation is enabled
+ expect(foo.value).toBe('bar')
+
+ await nextTick()
+ expect(updateFoo).toBeCalledTimes(1)
+ expect(compRender).toBeCalledTimes(2)
+ expect(serializeInner(root)).toBe('bar')
+ })
+
+ test('without parent listener (local mutation)', async () => {
+ let foo: any
+ const update = () => {
+ foo.value = 'bar'
+ }
+
+ const compRender = vi.fn()
+ const Comp = defineComponent({
+ props: ['foo'],
+ emits: ['update:foo'],
+ setup(props) {
+ foo = useModel(props, 'foo')
+ return () => {
+ compRender()
+ return foo.value
+ }
+ },
+ })
+
+ const root = nodeOps.createElement('div')
+ // provide initial value
+ render(h(Comp, { foo: 'initial' }), root)
+ expect(compRender).toBeCalledTimes(1)
+ expect(serializeInner(root)).toBe('initial')
+
+ expect(foo.value).toBe('initial')
+ update()
+ // when parent didn't provide value, local mutation is enabled
+ expect(foo.value).toBe('bar')
+
+ await nextTick()
+ expect(compRender).toBeCalledTimes(2)
+ expect(serializeInner(root)).toBe('bar')
+ })
+
+ test('kebab-case v-model (should not be local)', async () => {
+ let foo: any
+
+ const compRender = vi.fn()
+ const Comp = defineComponent({
+ props: ['fooBar'],
+ emits: ['update:fooBar'],
+ setup(props) {
+ foo = useModel(props, 'fooBar')
+ return () => {
+ compRender()
+ return foo.value
+ }
+ },
+ })
+
+ const updateFooBar = vi.fn()
+ const root = nodeOps.createElement('div')
+ // v-model:foo-bar compiles to foo-bar and onUpdate:fooBar
+ render(
+ h(Comp, { 'foo-bar': 'initial', 'onUpdate:fooBar': updateFooBar }),
+ root,
+ )
+ expect(compRender).toBeCalledTimes(1)
+ expect(serializeInner(root)).toBe('initial')
+
+ expect(foo.value).toBe('initial')
+ foo.value = 'bar'
+ // should not be using local mode, so nothing should actually change
+ expect(foo.value).toBe('initial')
+
+ await nextTick()
+ expect(compRender).toBeCalledTimes(1)
+ expect(updateFooBar).toBeCalledTimes(1)
+ expect(updateFooBar).toHaveBeenCalledWith('bar')
+ expect(foo.value).toBe('initial')
+ expect(serializeInner(root)).toBe('initial')
+ })
+
+ test('kebab-case update listener (should not be local)', async () => {
+ let foo: any
+
+ const compRender = vi.fn()
+ const Comp = defineComponent({
+ props: ['fooBar'],
+ emits: ['update:fooBar'],
+ setup(props) {
+ foo = useModel(props, 'fooBar')
+ return () => {
+ compRender()
+ return foo.value
+ }
+ },
+ })
+
+ const updateFooBar = vi.fn()
+ const root = nodeOps.createElement('div')
+ // The template compiler won't create hyphenated listeners, but it could have been passed manually
+ render(
+ h(Comp, { 'foo-bar': 'initial', 'onUpdate:foo-bar': updateFooBar }),
+ root,
+ )
+ expect(compRender).toBeCalledTimes(1)
+ expect(serializeInner(root)).toBe('initial')
+
+ expect(foo.value).toBe('initial')
+ foo.value = 'bar'
+ // should not be using local mode, so nothing should actually change
+ expect(foo.value).toBe('initial')
+
+ await nextTick()
+ expect(compRender).toBeCalledTimes(1)
+ expect(updateFooBar).toBeCalledTimes(1)
+ expect(updateFooBar).toHaveBeenCalledWith('bar')
+ expect(foo.value).toBe('initial')
+ expect(serializeInner(root)).toBe('initial')
+ })
+
+ test('default value', async () => {
+ let count: any
+ const inc = () => {
+ count.value++
+ }
+
+ const compRender = vi.fn()
+ const Comp = defineComponent({
+ props: { count: { default: 0 } },
+ emits: ['update:count'],
+ setup(props) {
+ count = useModel(props, 'count')
+ return () => {
+ compRender()
+ return count.value
+ }
+ },
+ })
+
+ const root = nodeOps.createElement('div')
+ const updateCount = vi.fn()
+ render(h(Comp, { 'onUpdate:count': updateCount }), root)
+ expect(compRender).toBeCalledTimes(1)
+ expect(serializeInner(root)).toBe('0')
+
+ expect(count.value).toBe(0)
+
+ inc()
+ // when parent didn't provide value, local mutation is enabled
+ expect(count.value).toBe(1)
+
+ await nextTick()
+
+ expect(updateCount).toBeCalledTimes(1)
+ expect(compRender).toBeCalledTimes(2)
+ expect(serializeInner(root)).toBe('1')
+ })
+
+ test('parent limiting child value', async () => {
+ let childCount: Ref<number>
+
+ const compRender = vi.fn()
+ const Comp = defineComponent({
+ props: ['count'],
+ emits: ['update:count'],
+ setup(props) {
+ childCount = useModel(props, 'count')
+ return () => {
+ compRender()
+ return childCount.value
+ }
+ },
+ })
+
+ const Parent = defineComponent({
+ setup() {
+ const count = ref(0)
+ watch(count, () => {
+ if (count.value < 0) {
+ count.value = 0
+ }
+ })
+ return () =>
+ h(Comp, {
+ count: count.value,
+ 'onUpdate:count': val => {
+ count.value = val
+ },
+ })
+ },
+ })
+
+ const root = nodeOps.createElement('div')
+ render(h(Parent), root)
+ expect(serializeInner(root)).toBe('0')
+
+ // child update
+ childCount!.value = 1
+ // not yet updated
+ expect(childCount!.value).toBe(0)
+
+ await nextTick()
+ expect(childCount!.value).toBe(1)
+ expect(serializeInner(root)).toBe('1')
+
+ // child update to invalid value
+ childCount!.value = -1
+ // not yet updated
+ expect(childCount!.value).toBe(1)
+
+ await nextTick()
+ // limited to 0 by parent
+ expect(childCount!.value).toBe(0)
+ expect(serializeInner(root)).toBe('0')
+ })
+
+ test('has parent value -> no parent value', async () => {
+ let childCount: Ref<number>
+
+ const compRender = vi.fn()
+ const Comp = defineComponent({
+ props: ['count'],
+ emits: ['update:count'],
+ setup(props) {
+ childCount = useModel(props, 'count')
+ return () => {
+ compRender()
+ return childCount.value
+ }
+ },
+ })
+
+ const toggle = ref(true)
+ const Parent = defineComponent({
+ setup() {
+ const count = ref(0)
+ return () =>
+ toggle.value
+ ? h(Comp, {
+ count: count.value,
+ 'onUpdate:count': val => {
+ count.value = val
+ },
+ })
+ : h(Comp)
+ },
+ })
+
+ const root = nodeOps.createElement('div')
+ render(h(Parent), root)
+ expect(serializeInner(root)).toBe('0')
+
+ // child update
+ childCount!.value = 1
+ // not yet updated
+ expect(childCount!.value).toBe(0)
+
+ await nextTick()
+ expect(childCount!.value).toBe(1)
+ expect(serializeInner(root)).toBe('1')
+
+ // parent change
+ toggle.value = false
+
+ await nextTick()
+ // localValue should be reset
+ expect(childCount!.value).toBeUndefined()
+ expect(serializeInner(root)).toBe('<!---->')
+
+ // child local mutation should continue to work
+ childCount!.value = 2
+ expect(childCount!.value).toBe(2)
+
+ await nextTick()
+ expect(serializeInner(root)).toBe('2')
+ })
+
+ // #9838
+ test('pass modelValue to slot (optimized mode) ', async () => {
+ let foo: any
+ const update = () => {
+ foo.value = 'bar'
+ }
+
+ const Comp = {
+ render(this: any) {
+ return this.$slots.default()
+ },
+ }
+
+ const childRender = vi.fn()
+ const slotRender = vi.fn()
+ const Child = defineComponent({
+ props: ['modelValue'],
+ emits: ['update:modelValue'],
+ setup(props) {
+ foo = useModel(props, 'modelValue')
+ return () => {
+ childRender()
+ return (
+ openBlock(),
+ createElementBlock(Fragment, null, [
+ createVNode(Comp, null, {
+ default: () => {
+ slotRender()
+ return createElementVNode('div', null, foo.value)
+ },
+ _: 1 /* STABLE */,
+ }),
+ ])
+ )
+ }
+ },
+ })
+
+ const msg = ref('')
+ const setValue = vi.fn(v => (msg.value = v))
+ const root = nodeOps.createElement('div')
+ createApp({
+ render() {
+ return (
+ openBlock(),
+ createBlock(
+ Child,
+ {
+ modelValue: msg.value,
+ 'onUpdate:modelValue': setValue,
+ },
+ null,
+ 8 /* PROPS */,
+ ['modelValue'],
+ )
+ )
+ },
+ }).mount(root)
+
+ expect(foo.value).toBe('')
+ expect(msg.value).toBe('')
+ expect(setValue).not.toBeCalled()
+ expect(childRender).toBeCalledTimes(1)
+ expect(slotRender).toBeCalledTimes(1)
+ expect(serializeInner(root)).toBe('<div></div>')
+
+ // update from child
+ update()
+
+ await nextTick()
+ expect(msg.value).toBe('bar')
+ expect(foo.value).toBe('bar')
+ expect(setValue).toBeCalledTimes(1)
+ expect(childRender).toBeCalledTimes(2)
+ 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')
+ })
+})
import {
- EMPTY_OBJ,
type LooseRequired,
type Prettify,
type UnionToIntersection,
- camelize,
extend,
- hasChanged,
- hyphenate,
isArray,
isFunction,
isPromise,
ComponentObjectPropsOptions,
ComponentPropsOptions,
ExtractPropTypes,
- NormalizedProps,
PropOptions,
} from './componentProps'
import { warn } from './warning'
import type { SlotsType, StrictUnwrapSlotsType } from './componentSlots'
-import { type Ref, customRef, ref } from '@vue/reactivity'
-import { watchSyncEffect } from '.'
+import type { Ref } from '@vue/reactivity'
// dev only
const warnRuntimeUsage = (method: string) =>
export type ModelRef<T, M extends string | number | symbol = string> = Ref<T> &
[ModelRef<T, M>, Record<M, true | undefined>]
+export type DefineModelOptions<T = any> = {
+ get?: (v: T) => any
+ set?: (v: T) => any
+}
+
/**
* Vue `<script setup>` compiler macro for declaring a
* two-way binding prop that can be consumed via `v-model` from the parent
* ```
*/
export function defineModel<T, M extends string | number | symbol = string>(
- options: { required: true } & PropOptions<T> & UseModelOptions<T>,
+ options: { required: true } & PropOptions<T> & DefineModelOptions<T>,
): ModelRef<T, M>
export function defineModel<T, M extends string | number | symbol = string>(
- options: { default: any } & PropOptions<T> & UseModelOptions<T>,
+ options: { default: any } & PropOptions<T> & DefineModelOptions<T>,
): ModelRef<T, M>
export function defineModel<T, M extends string | number | symbol = string>(
- options?: PropOptions<T> & UseModelOptions<T>,
+ options?: PropOptions<T> & DefineModelOptions<T>,
): ModelRef<T | undefined, M>
export function defineModel<T, M extends string | number | symbol = string>(
name: string,
- options: { required: true } & PropOptions<T> & UseModelOptions<T>,
+ options: { required: true } & PropOptions<T> & DefineModelOptions<T>,
): ModelRef<T, M>
export function defineModel<T, M extends string | number | symbol = string>(
name: string,
- options: { default: any } & PropOptions<T> & UseModelOptions<T>,
+ options: { default: any } & PropOptions<T> & DefineModelOptions<T>,
): ModelRef<T, M>
export function defineModel<T, M extends string | number | symbol = string>(
name: string,
- options?: PropOptions<T> & UseModelOptions<T>,
+ options?: PropOptions<T> & DefineModelOptions<T>,
): ModelRef<T | undefined, M>
export function defineModel(): any {
if (__DEV__) {
return getContext().attrs
}
-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
- }
-
- if (__DEV__ && !(i.propsOptions[0] as NormalizedProps)[name]) {
- warn(`useModel() called with prop "${name}" which is not declared.`)
- return ref() as any
- }
-
- const camelizedName = camelize(name)
- const hyphenatedName = hyphenate(name)
-
- const res = customRef((track, trigger) => {
- let localValue: any
- watchSyncEffect(() => {
- const propValue = props[name]
- if (hasChanged(localValue, propValue)) {
- localValue = propValue
- trigger()
- }
- })
- return {
- get() {
- track()
- return options.get ? options.get(localValue) : localValue
- },
- set(value) {
- const rawProps = i.vnode!.props
- if (
- !(
- rawProps &&
- // check if parent has passed v-model
- (name in rawProps ||
- camelizedName in rawProps ||
- hyphenatedName in rawProps) &&
- (`onUpdate:${name}` in rawProps ||
- `onUpdate:${camelizedName}` in rawProps ||
- `onUpdate:${hyphenatedName}` in rawProps)
- ) &&
- hasChanged(value, localValue)
- ) {
- localValue = value
- trigger()
- }
- 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 {
const i = getCurrentInstance()!
if (__DEV__ && !i) {
--- /dev/null
+import { type Ref, customRef, ref } from '@vue/reactivity'
+import { EMPTY_OBJ, camelize, hasChanged, hyphenate } from '@vue/shared'
+import type { DefineModelOptions, ModelRef } from '../apiSetupHelpers'
+import { getCurrentInstance } from '../component'
+import { warn } from '../warning'
+import type { NormalizedProps } from '../componentProps'
+import { watchSyncEffect } from '../apiWatch'
+
+export function useModel<
+ M extends string | number | symbol,
+ T extends Record<string, any>,
+ K extends keyof T,
+>(props: T, name: K, options?: DefineModelOptions<T[K]>): ModelRef<T[K], M>
+export function useModel(
+ props: Record<string, any>,
+ name: string,
+ options: DefineModelOptions = EMPTY_OBJ,
+): 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
+ }
+
+ const camelizedName = camelize(name)
+ const hyphenatedName = hyphenate(name)
+
+ const res = customRef((track, trigger) => {
+ let localValue: any
+ watchSyncEffect(() => {
+ const propValue = props[name]
+ if (hasChanged(localValue, propValue)) {
+ localValue = propValue
+ trigger()
+ }
+ })
+ return {
+ get() {
+ track()
+ return options.get ? options.get(localValue) : localValue
+ },
+ set(value) {
+ const rawProps = i.vnode!.props
+ if (
+ !(
+ rawProps &&
+ // check if parent has passed v-model
+ (name in rawProps ||
+ camelizedName in rawProps ||
+ hyphenatedName in rawProps) &&
+ (`onUpdate:${name}` in rawProps ||
+ `onUpdate:${camelizedName}` in rawProps ||
+ `onUpdate:${hyphenatedName}` in rawProps)
+ ) &&
+ hasChanged(value, localValue)
+ ) {
+ localValue = value
+ trigger()
+ }
+ 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
+}
export { defineComponent } from './apiDefineComponent'
export { defineAsyncComponent } from './apiAsyncComponent'
export { useAttrs, useSlots } from './apiSetupHelpers'
+export { useModel } from './helpers/useModel'
// <script setup> API ----------------------------------------------------------
defineSlots,
defineModel,
withDefaults,
- useModel,
type DefineProps,
type ModelRef,
} from './apiSetupHelpers'