mirror of https://github.com/vuejs/core.git
feat(defineModel): support local mutation by default, remove local option
ref https://github.com/vuejs/rfcs/discussions/503#discussioncomment-7566278
This commit is contained in:
parent
7e60d1058f
commit
f74785bc4a
|
@ -318,10 +318,6 @@ describe('defineModel', () => {
|
||||||
defineModel<string>({ default: 123 })
|
defineModel<string>({ default: 123 })
|
||||||
// @ts-expect-error unknown props option
|
// @ts-expect-error unknown props option
|
||||||
defineModel({ foo: 123 })
|
defineModel({ foo: 123 })
|
||||||
|
|
||||||
// accept defineModel-only options
|
|
||||||
defineModel({ local: true })
|
|
||||||
defineModel('foo', { local: true })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('useModel', () => {
|
describe('useModel', () => {
|
||||||
|
|
|
@ -14,7 +14,9 @@ import {
|
||||||
ComputedRef,
|
ComputedRef,
|
||||||
shallowReactive,
|
shallowReactive,
|
||||||
nextTick,
|
nextTick,
|
||||||
ref
|
ref,
|
||||||
|
Ref,
|
||||||
|
watch
|
||||||
} from '@vue/runtime-test'
|
} from '@vue/runtime-test'
|
||||||
import {
|
import {
|
||||||
defineEmits,
|
defineEmits,
|
||||||
|
@ -184,13 +186,17 @@ describe('SFC <script setup> helpers', () => {
|
||||||
foo.value = 'bar'
|
foo.value = 'bar'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const compRender = vi.fn()
|
||||||
const Comp = defineComponent({
|
const Comp = defineComponent({
|
||||||
props: ['modelValue'],
|
props: ['modelValue'],
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
setup(props) {
|
setup(props) {
|
||||||
foo = useModel(props, 'modelValue')
|
foo = useModel(props, 'modelValue')
|
||||||
},
|
return () => {
|
||||||
render() {}
|
compRender()
|
||||||
|
return foo.value
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const msg = ref('')
|
const msg = ref('')
|
||||||
|
@ -206,6 +212,8 @@ describe('SFC <script setup> helpers', () => {
|
||||||
expect(foo.value).toBe('')
|
expect(foo.value).toBe('')
|
||||||
expect(msg.value).toBe('')
|
expect(msg.value).toBe('')
|
||||||
expect(setValue).not.toBeCalled()
|
expect(setValue).not.toBeCalled()
|
||||||
|
expect(compRender).toBeCalledTimes(1)
|
||||||
|
expect(serializeInner(root)).toBe('')
|
||||||
|
|
||||||
// update from child
|
// update from child
|
||||||
update()
|
update()
|
||||||
|
@ -214,42 +222,55 @@ describe('SFC <script setup> helpers', () => {
|
||||||
expect(msg.value).toBe('bar')
|
expect(msg.value).toBe('bar')
|
||||||
expect(foo.value).toBe('bar')
|
expect(foo.value).toBe('bar')
|
||||||
expect(setValue).toBeCalledTimes(1)
|
expect(setValue).toBeCalledTimes(1)
|
||||||
|
expect(compRender).toBeCalledTimes(2)
|
||||||
|
expect(serializeInner(root)).toBe('bar')
|
||||||
|
|
||||||
// update from parent
|
// update from parent
|
||||||
msg.value = 'qux'
|
msg.value = 'qux'
|
||||||
|
expect(msg.value).toBe('qux')
|
||||||
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(msg.value).toBe('qux')
|
expect(msg.value).toBe('qux')
|
||||||
expect(foo.value).toBe('qux')
|
expect(foo.value).toBe('qux')
|
||||||
expect(setValue).toBeCalledTimes(1)
|
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
|
let foo: any
|
||||||
const update = () => {
|
const update = () => {
|
||||||
foo.value = 'bar'
|
foo.value = 'bar'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const compRender = vi.fn()
|
||||||
const Comp = defineComponent({
|
const Comp = defineComponent({
|
||||||
props: ['foo'],
|
props: ['foo'],
|
||||||
emits: ['update:foo'],
|
emits: ['update:foo'],
|
||||||
setup(props) {
|
setup(props) {
|
||||||
foo = useModel(props, 'foo', { local: true })
|
foo = useModel(props, 'foo')
|
||||||
},
|
return () => {
|
||||||
render() {}
|
compRender()
|
||||||
|
return foo.value
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const root = nodeOps.createElement('div')
|
const root = nodeOps.createElement('div')
|
||||||
const updateFoo = vi.fn()
|
const updateFoo = vi.fn()
|
||||||
render(h(Comp, { 'onUpdate:foo': updateFoo }), root)
|
render(h(Comp, { 'onUpdate:foo': updateFoo }), root)
|
||||||
|
expect(compRender).toBeCalledTimes(1)
|
||||||
|
expect(serializeInner(root)).toBe('<!---->')
|
||||||
|
|
||||||
expect(foo.value).toBeUndefined()
|
expect(foo.value).toBeUndefined()
|
||||||
update()
|
update()
|
||||||
|
// when parent didn't provide value, local mutation is enabled
|
||||||
expect(foo.value).toBe('bar')
|
expect(foo.value).toBe('bar')
|
||||||
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(updateFoo).toBeCalledTimes(1)
|
expect(updateFoo).toBeCalledTimes(1)
|
||||||
|
expect(compRender).toBeCalledTimes(2)
|
||||||
|
expect(serializeInner(root)).toBe('bar')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('default value', async () => {
|
test('default value', async () => {
|
||||||
|
@ -257,25 +278,156 @@ describe('SFC <script setup> helpers', () => {
|
||||||
const inc = () => {
|
const inc = () => {
|
||||||
count.value++
|
count.value++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const compRender = vi.fn()
|
||||||
const Comp = defineComponent({
|
const Comp = defineComponent({
|
||||||
props: { count: { default: 0 } },
|
props: { count: { default: 0 } },
|
||||||
emits: ['update:count'],
|
emits: ['update:count'],
|
||||||
setup(props) {
|
setup(props) {
|
||||||
count = useModel(props, 'count', { local: true })
|
count = useModel(props, 'count')
|
||||||
},
|
return () => {
|
||||||
render() {}
|
compRender()
|
||||||
|
return count.value
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const root = nodeOps.createElement('div')
|
const root = nodeOps.createElement('div')
|
||||||
const updateCount = vi.fn()
|
const updateCount = vi.fn()
|
||||||
render(h(Comp, { 'onUpdate:count': updateCount }), root)
|
render(h(Comp, { 'onUpdate:count': updateCount }), root)
|
||||||
|
expect(compRender).toBeCalledTimes(1)
|
||||||
|
expect(serializeInner(root)).toBe('0')
|
||||||
|
|
||||||
expect(count.value).toBe(0)
|
expect(count.value).toBe(0)
|
||||||
|
|
||||||
inc()
|
inc()
|
||||||
|
// when parent didn't provide value, local mutation is enabled
|
||||||
expect(count.value).toBe(1)
|
expect(count.value).toBe(1)
|
||||||
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
expect(updateCount).toBeCalledTimes(1)
|
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')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,8 @@ import {
|
||||||
Prettify,
|
Prettify,
|
||||||
UnionToIntersection,
|
UnionToIntersection,
|
||||||
extend,
|
extend,
|
||||||
LooseRequired
|
LooseRequired,
|
||||||
|
hasChanged
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import {
|
import {
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
|
@ -30,8 +31,8 @@ import {
|
||||||
} from './componentProps'
|
} from './componentProps'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
import { SlotsType, StrictUnwrapSlotsType } from './componentSlots'
|
import { SlotsType, StrictUnwrapSlotsType } from './componentSlots'
|
||||||
import { Ref, ref } from '@vue/reactivity'
|
import { Ref, customRef, ref } from '@vue/reactivity'
|
||||||
import { watch, watchSyncEffect } from './apiWatch'
|
import { watchSyncEffect } from '.'
|
||||||
|
|
||||||
// dev only
|
// dev only
|
||||||
const warnRuntimeUsage = (method: string) =>
|
const warnRuntimeUsage = (method: string) =>
|
||||||
|
@ -227,9 +228,8 @@ export function defineSlots<
|
||||||
* Otherwise the prop name will default to "modelValue". In both cases, you
|
* 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.
|
* 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
|
* If the parent did not provide the corresponding v-model props, the returned
|
||||||
* to `true`, the ref can be locally mutated even if the parent did not pass
|
* ref can still be used and will behave like a normal local ref.
|
||||||
* the matching `v-model`.
|
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
|
@ -246,32 +246,26 @@ export function defineSlots<
|
||||||
*
|
*
|
||||||
* // with specified name and default value
|
* // with specified name and default value
|
||||||
* const count = defineModel<number>('count', { default: 0 })
|
* 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>(
|
export function defineModel<T>(
|
||||||
options: { required: true } & PropOptions<T> & DefineModelOptions
|
options: { required: true } & PropOptions<T>
|
||||||
): Ref<T>
|
): Ref<T>
|
||||||
export function defineModel<T>(
|
export function defineModel<T>(
|
||||||
options: { default: any } & PropOptions<T> & DefineModelOptions
|
options: { default: any } & PropOptions<T>
|
||||||
): Ref<T>
|
): Ref<T>
|
||||||
export function defineModel<T>(
|
export function defineModel<T>(options?: PropOptions<T>): Ref<T | undefined>
|
||||||
options?: PropOptions<T> & DefineModelOptions
|
|
||||||
): Ref<T | undefined>
|
|
||||||
export function defineModel<T>(
|
export function defineModel<T>(
|
||||||
name: string,
|
name: string,
|
||||||
options: { required: true } & PropOptions<T> & DefineModelOptions
|
options: { required: true } & PropOptions<T>
|
||||||
): Ref<T>
|
): Ref<T>
|
||||||
export function defineModel<T>(
|
export function defineModel<T>(
|
||||||
name: string,
|
name: string,
|
||||||
options: { default: any } & PropOptions<T> & DefineModelOptions
|
options: { default: any } & PropOptions<T>
|
||||||
): Ref<T>
|
): Ref<T>
|
||||||
export function defineModel<T>(
|
export function defineModel<T>(
|
||||||
name: string,
|
name: string,
|
||||||
options?: PropOptions<T> & DefineModelOptions
|
options?: PropOptions<T>
|
||||||
): Ref<T | undefined>
|
): Ref<T | undefined>
|
||||||
export function defineModel(): any {
|
export function defineModel(): any {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
|
@ -279,10 +273,6 @@ export function defineModel(): any {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DefineModelOptions {
|
|
||||||
local?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type NotUndefined<T> = T extends undefined ? never : T
|
type NotUndefined<T> = T extends undefined ? never : T
|
||||||
|
|
||||||
type InferDefaults<T> = {
|
type InferDefaults<T> = {
|
||||||
|
@ -357,14 +347,9 @@ export function useAttrs(): SetupContext['attrs'] {
|
||||||
|
|
||||||
export function useModel<T extends Record<string, any>, K extends keyof T>(
|
export function useModel<T extends Record<string, any>, K extends keyof T>(
|
||||||
props: T,
|
props: T,
|
||||||
name: K,
|
name: K
|
||||||
options?: { local?: boolean }
|
|
||||||
): Ref<T[K]>
|
): Ref<T[K]>
|
||||||
export function useModel(
|
export function useModel(props: Record<string, any>, name: string): Ref {
|
||||||
props: Record<string, any>,
|
|
||||||
name: string,
|
|
||||||
options?: { local?: boolean }
|
|
||||||
): Ref {
|
|
||||||
const i = getCurrentInstance()!
|
const i = getCurrentInstance()!
|
||||||
if (__DEV__ && !i) {
|
if (__DEV__ && !i) {
|
||||||
warn(`useModel() called without active instance.`)
|
warn(`useModel() called without active instance.`)
|
||||||
|
@ -376,34 +361,25 @@ export function useModel(
|
||||||
return ref() as any
|
return ref() as any
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options && options.local) {
|
let localValue: any
|
||||||
const proxy = ref<any>(props[name])
|
watchSyncEffect(() => {
|
||||||
watchSyncEffect(() => {
|
localValue = props[name]
|
||||||
proxy.value = props[name]
|
})
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
return customRef((track, trigger) => ({
|
||||||
proxy,
|
get() {
|
||||||
value => {
|
track()
|
||||||
if (value !== props[name]) {
|
return localValue
|
||||||
i.emit(`update:${name}`, value)
|
},
|
||||||
}
|
set(value) {
|
||||||
},
|
const rawProps = i.vnode!.props
|
||||||
{ flush: 'sync' }
|
if (!(rawProps && name in rawProps) && hasChanged(value, localValue)) {
|
||||||
)
|
localValue = value
|
||||||
|
trigger()
|
||||||
return proxy
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
__v_isRef: true,
|
|
||||||
get value() {
|
|
||||||
return props[name]
|
|
||||||
},
|
|
||||||
set value(value) {
|
|
||||||
i.emit(`update:${name}`, value)
|
|
||||||
}
|
}
|
||||||
} as any
|
i.emit(`update:${name}`, value)
|
||||||
}
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getContext(): SetupContext {
|
function getContext(): SetupContext {
|
||||||
|
|
Loading…
Reference in New Issue