diff --git a/packages/core/useCloned/index.md b/packages/core/useCloned/index.md index 71e4b1cdd22..58bf6fdffdb 100644 --- a/packages/core/useCloned/index.md +++ b/packages/core/useCloned/index.md @@ -50,4 +50,3 @@ const original = ref({ key: 'value' }) const { cloned, sync } = useCloned(original, { clone: klona }) ``` - diff --git a/packages/core/useCloned/index.ts b/packages/core/useCloned/index.ts index 7c95cacd01d..d40080181f0 100644 --- a/packages/core/useCloned/index.ts +++ b/packages/core/useCloned/index.ts @@ -29,7 +29,9 @@ export interface UseClonedReturn { sync: () => void } -function cloneFnJSON(source: T): T { +export type CloneFn = (x: F) => T + +export function cloneFnJSON(source: T): T { return JSON.parse(JSON.stringify(source)) } diff --git a/packages/core/useManualRefHistory/index.ts b/packages/core/useManualRefHistory/index.ts index 35b2b652e35..6aa7a2a31ee 100644 --- a/packages/core/useManualRefHistory/index.ts +++ b/packages/core/useManualRefHistory/index.ts @@ -1,20 +1,19 @@ import { isFunction, timestamp } from '@vueuse/shared' import type { Ref } from 'vue-demi' import { computed, markRaw, ref } from 'vue-demi' +import type { CloneFn } from '../useCloned' +import { cloneFnJSON } from '../useCloned' export interface UseRefHistoryRecord { snapshot: T timestamp: number } -export type CloneFn = (x: F) => T - export interface UseManualRefHistoryOptions { /** * Maximum number of history to be kept. Default to unlimited. */ capacity?: number - /** * Clone when taking a snapshot, shortcut for dump: JSON.parse(JSON.stringify(value)). * Default to false @@ -99,18 +98,17 @@ export interface UseManualRefHistoryReturn { reset: () => void } -const fnClone = (v: F): T => JSON.parse(JSON.stringify(v)) const fnBypass = (v: F) => v as unknown as T const fnSetSource = (source: Ref, value: F) => source.value = value type FnCloneOrBypass = (v: F) => T function defaultDump(clone?: boolean | CloneFn) { - return (clone ? isFunction(clone) ? clone : fnClone : fnBypass) as unknown as FnCloneOrBypass + return (clone ? isFunction(clone) ? clone : cloneFnJSON : fnBypass) as unknown as FnCloneOrBypass } function defaultParse(clone?: boolean | CloneFn) { - return (clone ? isFunction(clone) ? clone : fnClone : fnBypass) as unknown as FnCloneOrBypass + return (clone ? isFunction(clone) ? clone : cloneFnJSON : fnBypass) as unknown as FnCloneOrBypass } /** diff --git a/packages/core/useRefHistory/index.ts b/packages/core/useRefHistory/index.ts index 8c710cad739..e3e17e340c3 100644 --- a/packages/core/useRefHistory/index.ts +++ b/packages/core/useRefHistory/index.ts @@ -1,7 +1,8 @@ import type { ConfigurableEventFilter, Fn } from '@vueuse/shared' import { pausableFilter, watchIgnorable } from '@vueuse/shared' import type { Ref } from 'vue-demi' -import type { CloneFn, UseManualRefHistoryReturn } from '../useManualRefHistory' +import type { CloneFn } from '../useCloned' +import type { UseManualRefHistoryReturn } from '../useManualRefHistory' import { useManualRefHistory } from '../useManualRefHistory' export interface UseRefHistoryOptions extends ConfigurableEventFilter { diff --git a/packages/core/useVModel/index.test.ts b/packages/core/useVModel/index.test.ts index 4b510dd28e9..29fbe796e60 100644 --- a/packages/core/useVModel/index.test.ts +++ b/packages/core/useVModel/index.test.ts @@ -177,4 +177,56 @@ describe('useVModel', () => { expect(emitValue instanceof SomeClass).toBeTruthy() }) + + it('should clone object', async () => { + const emitMock = vitest.fn() + + const props = { + person: { + age: 18, + child: { age: 2 }, + }, + } + + const data = useVModel(props, 'person', emitMock, { passive: true, clone: true }) + const dataDeep = useVModel(props, 'person', emitMock, { passive: true, clone: true, deep: true }) + + data.value.age = 20 + + await nextTick() + expect(props.person).not.toBe(data.value) + expect(props.person).toEqual(expect.objectContaining({ age: 18 })) + + dataDeep.value.child.age = 3 + + expect(props.person).not.toBe(dataDeep.value) + expect(props.person).toEqual(expect.objectContaining({ + child: { age: 2 }, + })) + }) + + it('should deep clone object with clone function', async () => { + const emitMock = vitest.fn() + const clone = vitest.fn(x => JSON.parse(JSON.stringify(x))) + + const props = { + person: { + age: 18, + child: { age: 2 }, + }, + } + + const data = useVModel(props, 'person', emitMock, { passive: true, clone, deep: true }) + + data.value.age = 20 + data.value.child.age = 3 + + await nextTick() + expect(clone).toHaveBeenCalled() + expect(props.person).not.toBe(data.value) + expect(props.person).toEqual({ + age: 18, + child: { age: 2 }, + }) + }) }) diff --git a/packages/core/useVModel/index.ts b/packages/core/useVModel/index.ts index 12ee6a5ad5c..2e7e3af4608 100644 --- a/packages/core/useVModel/index.ts +++ b/packages/core/useVModel/index.ts @@ -1,6 +1,8 @@ -import { isDef } from '@vueuse/shared' +import { isDef, isFunction } from '@vueuse/shared' import type { UnwrapRef } from 'vue-demi' import { computed, getCurrentInstance, isVue2, ref, watch } from 'vue-demi' +import type { CloneFn } from '../useCloned' +import { cloneFnJSON } from '../useCloned' export interface UseVModelOptions { /** @@ -29,6 +31,14 @@ export interface UseVModelOptions { * @default undefined */ defaultValue?: T + /** + * Clone the props. + * Accepts a custom clone function. + * When setting to `true`, it will use `JSON.parse(JSON.stringify(value))` to clone. + * + * @default false + */ + clone?: boolean | CloneFn } /** @@ -46,6 +56,7 @@ export function useVModel

= {}, ) { const { + clone = false, passive = false, eventName, deep = false, @@ -71,19 +82,33 @@ export function useVModel

isDef(props[key!]) ? props[key!] : defaultValue + const cloneFn = (val: P[K]) => !clone + ? val + : isFunction(clone) + ? clone(val) + : cloneFnJSON(val) + + const getValue = () => isDef(props[key!]) + ? cloneFn(props[key!]) + : defaultValue if (passive) { - const proxy = ref(getValue()!) + const initialValue = getValue() + const proxy = ref(initialValue!) - watch(() => props[key!], v => proxy.value = v as UnwrapRef) + watch( + () => props[key!], + v => proxy.value = cloneFn(v) as UnwrapRef, + ) - watch(proxy, (v) => { - if (v !== props[key!] || deep) - _emit(event, v) - }, { - deep, - }) + watch( + proxy, + (v) => { + if (v !== props[key!] || deep) + _emit(event, v) + }, + { deep }, + ) return proxy }