Skip to content

Commit f74785b

Browse files
committedDec 12, 2023
feat(defineModel): support local mutation by default, remove local option
ref vuejs/rfcs#503 (comment)
1 parent 7e60d10 commit f74785b

File tree

3 files changed

+194
-70
lines changed

3 files changed

+194
-70
lines changed
 

‎packages/dts-test/setupHelpers.test-d.ts

-4
Original file line numberDiff line numberDiff line change
@@ -318,10 +318,6 @@ describe('defineModel', () => {
318318
defineModel<string>({ default: 123 })
319319
// @ts-expect-error unknown props option
320320
defineModel({ foo: 123 })
321-
322-
// accept defineModel-only options
323-
defineModel({ local: true })
324-
defineModel('foo', { local: true })
325321
})
326322

327323
describe('useModel', () => {

‎packages/runtime-core/__tests__/apiSetupHelpers.spec.ts

+163-11
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import {
1414
ComputedRef,
1515
shallowReactive,
1616
nextTick,
17-
ref
17+
ref,
18+
Ref,
19+
watch
1820
} from '@vue/runtime-test'
1921
import {
2022
defineEmits,
@@ -184,13 +186,17 @@ describe('SFC <script setup> helpers', () => {
184186
foo.value = 'bar'
185187
}
186188

189+
const compRender = vi.fn()
187190
const Comp = defineComponent({
188191
props: ['modelValue'],
189192
emits: ['update:modelValue'],
190193
setup(props) {
191194
foo = useModel(props, 'modelValue')
192-
},
193-
render() {}
195+
return () => {
196+
compRender()
197+
return foo.value
198+
}
199+
}
194200
})
195201

196202
const msg = ref('')
@@ -206,6 +212,8 @@ describe('SFC <script setup> helpers', () => {
206212
expect(foo.value).toBe('')
207213
expect(msg.value).toBe('')
208214
expect(setValue).not.toBeCalled()
215+
expect(compRender).toBeCalledTimes(1)
216+
expect(serializeInner(root)).toBe('')
209217

210218
// update from child
211219
update()
@@ -214,68 +222,212 @@ describe('SFC <script setup> helpers', () => {
214222
expect(msg.value).toBe('bar')
215223
expect(foo.value).toBe('bar')
216224
expect(setValue).toBeCalledTimes(1)
225+
expect(compRender).toBeCalledTimes(2)
226+
expect(serializeInner(root)).toBe('bar')
217227

218228
// update from parent
219229
msg.value = 'qux'
230+
expect(msg.value).toBe('qux')
220231

221232
await nextTick()
222233
expect(msg.value).toBe('qux')
223234
expect(foo.value).toBe('qux')
224235
expect(setValue).toBeCalledTimes(1)
236+
expect(compRender).toBeCalledTimes(3)
237+
expect(serializeInner(root)).toBe('qux')
225238
})
226239

227-
test('local', async () => {
240+
test('without parent value (local mutation)', async () => {
228241
let foo: any
229242
const update = () => {
230243
foo.value = 'bar'
231244
}
232245

246+
const compRender = vi.fn()
233247
const Comp = defineComponent({
234248
props: ['foo'],
235249
emits: ['update:foo'],
236250
setup(props) {
237-
foo = useModel(props, 'foo', { local: true })
238-
},
239-
render() {}
251+
foo = useModel(props, 'foo')
252+
return () => {
253+
compRender()
254+
return foo.value
255+
}
256+
}
240257
})
241258

242259
const root = nodeOps.createElement('div')
243260
const updateFoo = vi.fn()
244261
render(h(Comp, { 'onUpdate:foo': updateFoo }), root)
262+
expect(compRender).toBeCalledTimes(1)
263+
expect(serializeInner(root)).toBe('<!---->')
245264

246265
expect(foo.value).toBeUndefined()
247266
update()
248-
267+
// when parent didn't provide value, local mutation is enabled
249268
expect(foo.value).toBe('bar')
250269

251270
await nextTick()
252271
expect(updateFoo).toBeCalledTimes(1)
272+
expect(compRender).toBeCalledTimes(2)
273+
expect(serializeInner(root)).toBe('bar')
253274
})
254275

255276
test('default value', async () => {
256277
let count: any
257278
const inc = () => {
258279
count.value++
259280
}
281+
282+
const compRender = vi.fn()
260283
const Comp = defineComponent({
261284
props: { count: { default: 0 } },
262285
emits: ['update:count'],
263286
setup(props) {
264-
count = useModel(props, 'count', { local: true })
265-
},
266-
render() {}
287+
count = useModel(props, 'count')
288+
return () => {
289+
compRender()
290+
return count.value
291+
}
292+
}
267293
})
268294

269295
const root = nodeOps.createElement('div')
270296
const updateCount = vi.fn()
271297
render(h(Comp, { 'onUpdate:count': updateCount }), root)
298+
expect(compRender).toBeCalledTimes(1)
299+
expect(serializeInner(root)).toBe('0')
272300

273301
expect(count.value).toBe(0)
274302

275303
inc()
304+
// when parent didn't provide value, local mutation is enabled
276305
expect(count.value).toBe(1)
306+
277307
await nextTick()
308+
278309
expect(updateCount).toBeCalledTimes(1)
310+
expect(compRender).toBeCalledTimes(2)
311+
expect(serializeInner(root)).toBe('1')
312+
})
313+
314+
test('parent limiting child value', async () => {
315+
let childCount: Ref<number>
316+
317+
const compRender = vi.fn()
318+
const Comp = defineComponent({
319+
props: ['count'],
320+
emits: ['update:count'],
321+
setup(props) {
322+
childCount = useModel(props, 'count')
323+
return () => {
324+
compRender()
325+
return childCount.value
326+
}
327+
}
328+
})
329+
330+
const Parent = defineComponent({
331+
setup() {
332+
const count = ref(0)
333+
watch(count, () => {
334+
if (count.value < 0) {
335+
count.value = 0
336+
}
337+
})
338+
return () =>
339+
h(Comp, {
340+
count: count.value,
341+
'onUpdate:count': val => {
342+
count.value = val
343+
}
344+
})
345+
}
346+
})
347+
348+
const root = nodeOps.createElement('div')
349+
render(h(Parent), root)
350+
expect(serializeInner(root)).toBe('0')
351+
352+
// child update
353+
childCount!.value = 1
354+
// not yet updated
355+
expect(childCount!.value).toBe(0)
356+
357+
await nextTick()
358+
expect(childCount!.value).toBe(1)
359+
expect(serializeInner(root)).toBe('1')
360+
361+
// child update to invalid value
362+
childCount!.value = -1
363+
// not yet updated
364+
expect(childCount!.value).toBe(1)
365+
366+
await nextTick()
367+
// limited to 0 by parent
368+
expect(childCount!.value).toBe(0)
369+
expect(serializeInner(root)).toBe('0')
370+
})
371+
372+
test('has parent value -> no parent value', async () => {
373+
let childCount: Ref<number>
374+
375+
const compRender = vi.fn()
376+
const Comp = defineComponent({
377+
props: ['count'],
378+
emits: ['update:count'],
379+
setup(props) {
380+
childCount = useModel(props, 'count')
381+
return () => {
382+
compRender()
383+
return childCount.value
384+
}
385+
}
386+
})
387+
388+
const toggle = ref(true)
389+
const Parent = defineComponent({
390+
setup() {
391+
const count = ref(0)
392+
return () =>
393+
toggle.value
394+
? h(Comp, {
395+
count: count.value,
396+
'onUpdate:count': val => {
397+
count.value = val
398+
}
399+
})
400+
: h(Comp)
401+
}
402+
})
403+
404+
const root = nodeOps.createElement('div')
405+
render(h(Parent), root)
406+
expect(serializeInner(root)).toBe('0')
407+
408+
// child update
409+
childCount!.value = 1
410+
// not yet updated
411+
expect(childCount!.value).toBe(0)
412+
413+
await nextTick()
414+
expect(childCount!.value).toBe(1)
415+
expect(serializeInner(root)).toBe('1')
416+
417+
// parent change
418+
toggle.value = false
419+
420+
await nextTick()
421+
// localValue should be reset
422+
expect(childCount!.value).toBeUndefined()
423+
expect(serializeInner(root)).toBe('<!---->')
424+
425+
// child local mutation should continue to work
426+
childCount!.value = 2
427+
expect(childCount!.value).toBe(2)
428+
429+
await nextTick()
430+
expect(serializeInner(root)).toBe('2')
279431
})
280432
})
281433

‎packages/runtime-core/src/apiSetupHelpers.ts

+31-55
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
Prettify,
66
UnionToIntersection,
77
extend,
8-
LooseRequired
8+
LooseRequired,
9+
hasChanged
910
} from '@vue/shared'
1011
import {
1112
getCurrentInstance,
@@ -30,8 +31,8 @@ import {
3031
} from './componentProps'
3132
import { warn } from './warning'
3233
import { SlotsType, StrictUnwrapSlotsType } from './componentSlots'
33-
import { Ref, ref } from '@vue/reactivity'
34-
import { watch, watchSyncEffect } from './apiWatch'
34+
import { Ref, customRef, ref } from '@vue/reactivity'
35+
import { watchSyncEffect } from '.'
3536

3637
// dev only
3738
const warnRuntimeUsage = (method: string) =>
@@ -227,9 +228,8 @@ export function defineSlots<
227228
* Otherwise the prop name will default to "modelValue". In both cases, you
228229
* can also pass an additional object which will be used as the prop's options.
229230
*
230-
* The options object can also specify an additional option, `local`. When set
231-
* to `true`, the ref can be locally mutated even if the parent did not pass
232-
* the matching `v-model`.
231+
* If the parent did not provide the corresponding v-model props, the returned
232+
* ref can still be used and will behave like a normal local ref.
233233
*
234234
* @example
235235
* ```ts
@@ -246,43 +246,33 @@ export function defineSlots<
246246
*
247247
* // with specified name and default value
248248
* const count = defineModel<number>('count', { default: 0 })
249-
*
250-
* // local mutable model, can be mutated locally
251-
* // even if the parent did not pass the matching `v-model`.
252-
* const count = defineModel<number>('count', { local: true, default: 0 })
253249
* ```
254250
*/
255251
export function defineModel<T>(
256-
options: { required: true } & PropOptions<T> & DefineModelOptions
252+
options: { required: true } & PropOptions<T>
257253
): Ref<T>
258254
export function defineModel<T>(
259-
options: { default: any } & PropOptions<T> & DefineModelOptions
255+
options: { default: any } & PropOptions<T>
260256
): Ref<T>
261-
export function defineModel<T>(
262-
options?: PropOptions<T> & DefineModelOptions
263-
): Ref<T | undefined>
257+
export function defineModel<T>(options?: PropOptions<T>): Ref<T | undefined>
264258
export function defineModel<T>(
265259
name: string,
266-
options: { required: true } & PropOptions<T> & DefineModelOptions
260+
options: { required: true } & PropOptions<T>
267261
): Ref<T>
268262
export function defineModel<T>(
269263
name: string,
270-
options: { default: any } & PropOptions<T> & DefineModelOptions
264+
options: { default: any } & PropOptions<T>
271265
): Ref<T>
272266
export function defineModel<T>(
273267
name: string,
274-
options?: PropOptions<T> & DefineModelOptions
268+
options?: PropOptions<T>
275269
): Ref<T | undefined>
276270
export function defineModel(): any {
277271
if (__DEV__) {
278272
warnRuntimeUsage('defineModel')
279273
}
280274
}
281275

282-
interface DefineModelOptions {
283-
local?: boolean
284-
}
285-
286276
type NotUndefined<T> = T extends undefined ? never : T
287277

288278
type InferDefaults<T> = {
@@ -357,14 +347,9 @@ export function useAttrs(): SetupContext['attrs'] {
357347

358348
export function useModel<T extends Record<string, any>, K extends keyof T>(
359349
props: T,
360-
name: K,
361-
options?: { local?: boolean }
350+
name: K
362351
): Ref<T[K]>
363-
export function useModel(
364-
props: Record<string, any>,
365-
name: string,
366-
options?: { local?: boolean }
367-
): Ref {
352+
export function useModel(props: Record<string, any>, name: string): Ref {
368353
const i = getCurrentInstance()!
369354
if (__DEV__ && !i) {
370355
warn(`useModel() called without active instance.`)
@@ -376,34 +361,25 @@ export function useModel(
376361
return ref() as any
377362
}
378363

379-
if (options && options.local) {
380-
const proxy = ref<any>(props[name])
381-
watchSyncEffect(() => {
382-
proxy.value = props[name]
383-
})
384-
385-
watch(
386-
proxy,
387-
value => {
388-
if (value !== props[name]) {
389-
i.emit(`update:${name}`, value)
390-
}
391-
},
392-
{ flush: 'sync' }
393-
)
364+
let localValue: any
365+
watchSyncEffect(() => {
366+
localValue = props[name]
367+
})
394368

395-
return proxy
396-
} else {
397-
return {
398-
__v_isRef: true,
399-
get value() {
400-
return props[name]
401-
},
402-
set value(value) {
403-
i.emit(`update:${name}`, value)
369+
return customRef((track, trigger) => ({
370+
get() {
371+
track()
372+
return localValue
373+
},
374+
set(value) {
375+
const rawProps = i.vnode!.props
376+
if (!(rawProps && name in rawProps) && hasChanged(value, localValue)) {
377+
localValue = value
378+
trigger()
404379
}
405-
} as any
406-
}
380+
i.emit(`update:${name}`, value)
381+
}
382+
}))
407383
}
408384

409385
function getContext(): SetupContext {

0 commit comments

Comments
 (0)
Please sign in to comment.