Skip to content

Commit f74785b

Browse files
committed
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

0 commit comments

Comments
 (0)