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 })
|
||||
// @ts-expect-error unknown props option
|
||||
defineModel({ foo: 123 })
|
||||
|
||||
// accept defineModel-only options
|
||||
defineModel({ local: true })
|
||||
defineModel('foo', { local: true })
|
||||
})
|
||||
|
||||
describe('useModel', () => {
|
||||
|
|
|
@ -14,7 +14,9 @@ import {
|
|||
ComputedRef,
|
||||
shallowReactive,
|
||||
nextTick,
|
||||
ref
|
||||
ref,
|
||||
Ref,
|
||||
watch
|
||||
} from '@vue/runtime-test'
|
||||
import {
|
||||
defineEmits,
|
||||
|
@ -184,13 +186,17 @@ describe('SFC <script setup> helpers', () => {
|
|||
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('')
|
||||
|
@ -206,6 +212,8 @@ describe('SFC <script setup> helpers', () => {
|
|||
expect(foo.value).toBe('')
|
||||
expect(msg.value).toBe('')
|
||||
expect(setValue).not.toBeCalled()
|
||||
expect(compRender).toBeCalledTimes(1)
|
||||
expect(serializeInner(root)).toBe('')
|
||||
|
||||
// update from child
|
||||
update()
|
||||
|
@ -214,42 +222,55 @@ describe('SFC <script setup> helpers', () => {
|
|||
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 () => {
|
||||
|
@ -257,25 +278,156 @@ describe('SFC <script setup> helpers', () => {
|
|||
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')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@ import {
|
|||
Prettify,
|
||||
UnionToIntersection,
|
||||
extend,
|
||||
LooseRequired
|
||||
LooseRequired,
|
||||
hasChanged
|
||||
} from '@vue/shared'
|
||||
import {
|
||||
getCurrentInstance,
|
||||
|
@ -30,8 +31,8 @@ import {
|
|||
} 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) =>
|
||||
|
@ -227,9 +228,8 @@ export function defineSlots<
|
|||
* 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
|
||||
|
@ -246,32 +246,26 @@ export function defineSlots<
|
|||
*
|
||||
* // 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__) {
|
||||
|
@ -279,10 +273,6 @@ export function defineModel(): any {
|
|||
}
|
||||
}
|
||||
|
||||
interface DefineModelOptions {
|
||||
local?: boolean
|
||||
}
|
||||
|
||||
type NotUndefined<T> = T extends undefined ? never : 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>(
|
||||
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.`)
|
||||
|
@ -376,34 +361,25 @@ export function useModel(
|
|||
return ref() as any
|
||||
}
|
||||
|
||||
if (options && options.local) {
|
||||
const proxy = ref<any>(props[name])
|
||||
watchSyncEffect(() => {
|
||||
proxy.value = props[name]
|
||||
})
|
||||
let localValue: any
|
||||
watchSyncEffect(() => {
|
||||
localValue = props[name]
|
||||
})
|
||||
|
||||
watch(
|
||||
proxy,
|
||||
value => {
|
||||
if (value !== props[name]) {
|
||||
i.emit(`update:${name}`, value)
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' }
|
||||
)
|
||||
|
||||
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 {
|
||||
|
|
Loading…
Reference in New Issue