ComputedRef,
shallowReactive,
nextTick,
- ref
+ ref,
+ Ref,
+ watch
} from '@vue/runtime-test'
import {
defineEmits,
foo.value = 'bar'
}
+ const compRender = vi.fn()
const Comp = defineComponent({
props: ['modelValue'],
emits: ['update:modelValue'],
setup(props) {
foo = useModel(props, 'modelValue')
- },
- render() {}
+ return () => {
+ compRender()
+ return foo.value
+ }
+ }
})
const msg = ref('')
expect(foo.value).toBe('')
expect(msg.value).toBe('')
expect(setValue).not.toBeCalled()
+ expect(compRender).toBeCalledTimes(1)
+ expect(serializeInner(root)).toBe('')
// update from child
update()
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('local', async () => {
+ 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', { local: true })
- },
- render() {}
+ 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('default value', async () => {
const inc = () => {
count.value++
}
+
+ const compRender = vi.fn()
const Comp = defineComponent({
props: { count: { default: 0 } },
emits: ['update:count'],
setup(props) {
- count = useModel(props, 'count', { local: true })
- },
- render() {}
+ 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')
})
})
Prettify,
UnionToIntersection,
extend,
- LooseRequired
+ LooseRequired,
+ hasChanged
} from '@vue/shared'
import {
getCurrentInstance,
} from './componentProps'
import { warn } from './warning'
import { SlotsType, StrictUnwrapSlotsType } from './componentSlots'
-import { Ref, ref } from '@vue/reactivity'
-import { watch, watchSyncEffect } from './apiWatch'
+import { Ref, customRef, ref } from '@vue/reactivity'
+import { watchSyncEffect } from '.'
// dev only
const warnRuntimeUsage = (method: string) =>
* 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`.
+ * If the parent did not provide the corresponding v-model props, the returned
+ * ref can still be used and will behave like a normal local ref.
*
* @example
* ```ts
*
* // 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
+ options: { required: true } & PropOptions<T>
): Ref<T>
export function defineModel<T>(
- options: { default: any } & PropOptions<T> & DefineModelOptions
+ options: { default: any } & PropOptions<T>
): Ref<T>
-export function defineModel<T>(
- options?: PropOptions<T> & DefineModelOptions
-): Ref<T | undefined>
+export function defineModel<T>(options?: PropOptions<T>): Ref<T | undefined>
export function defineModel<T>(
name: string,
- options: { required: true } & PropOptions<T> & DefineModelOptions
+ options: { required: true } & PropOptions<T>
): Ref<T>
export function defineModel<T>(
name: string,
- options: { default: any } & PropOptions<T> & DefineModelOptions
+ options: { default: any } & PropOptions<T>
): Ref<T>
export function defineModel<T>(
name: string,
- options?: PropOptions<T> & DefineModelOptions
+ options?: PropOptions<T>
): Ref<T | undefined>
export function defineModel(): any {
if (__DEV__) {
}
}
-interface DefineModelOptions {
- local?: boolean
-}
-
type NotUndefined<T> = T extends undefined ? never : T
type InferDefaults<T> = {
export function useModel<T extends Record<string, any>, K extends keyof T>(
props: T,
- name: K,
- options?: { local?: boolean }
+ name: K
): Ref<T[K]>
-export function useModel(
- props: Record<string, any>,
- name: string,
- options?: { local?: boolean }
-): Ref {
+export function useModel(props: Record<string, any>, name: string): Ref {
const i = getCurrentInstance()!
if (__DEV__ && !i) {
warn(`useModel() called without active instance.`)
return ref() as any
}
- if (options && options.local) {
- const proxy = ref<any>(props[name])
- watchSyncEffect(() => {
- proxy.value = props[name]
- })
-
- watch(
- proxy,
- value => {
- if (value !== props[name]) {
- i.emit(`update:${name}`, value)
- }
- },
- { flush: 'sync' }
- )
+ let localValue: any
+ watchSyncEffect(() => {
+ localValue = props[name]
+ })
- return proxy
- } else {
- return {
- __v_isRef: true,
- get value() {
- return props[name]
- },
- set value(value) {
- i.emit(`update:${name}`, value)
+ return customRef((track, trigger) => ({
+ get() {
+ track()
+ return localValue
+ },
+ set(value) {
+ const rawProps = i.vnode!.props
+ if (!(rawProps && name in rawProps) && hasChanged(value, localValue)) {
+ localValue = value
+ trigger()
}
- } as any
- }
+ i.emit(`update:${name}`, value)
+ }
+ }))
}
function getContext(): SetupContext {