Skip to content

Commit

Permalink
feat(useVModel): support clone option (#2022)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
edumudu and antfu committed Sep 5, 2022
1 parent eb05403 commit 369e177
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 19 deletions.
1 change: 0 additions & 1 deletion packages/core/useCloned/index.md
Expand Up @@ -50,4 +50,3 @@ const original = ref({ key: 'value' })

const { cloned, sync } = useCloned(original, { clone: klona })
```

4 changes: 3 additions & 1 deletion packages/core/useCloned/index.ts
Expand Up @@ -29,7 +29,9 @@ export interface UseClonedReturn<T> {
sync: () => void
}

function cloneFnJSON<T>(source: T): T {
export type CloneFn<F, T = F> = (x: F) => T

export function cloneFnJSON<T>(source: T): T {
return JSON.parse(JSON.stringify(source))
}

Expand Down
10 changes: 4 additions & 6 deletions 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<T> {
snapshot: T
timestamp: number
}

export type CloneFn<F, T = F> = (x: F) => T

export interface UseManualRefHistoryOptions<Raw, Serialized = Raw> {
/**
* 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
Expand Down Expand Up @@ -99,18 +98,17 @@ export interface UseManualRefHistoryReturn<Raw, Serialized> {
reset: () => void
}

const fnClone = <F, T>(v: F): T => JSON.parse(JSON.stringify(v))
const fnBypass = <F, T>(v: F) => v as unknown as T
const fnSetSource = <F>(source: Ref<F>, value: F) => source.value = value

type FnCloneOrBypass<F, T> = (v: F) => T

function defaultDump<R, S>(clone?: boolean | CloneFn<R>) {
return (clone ? isFunction(clone) ? clone : fnClone : fnBypass) as unknown as FnCloneOrBypass<R, S>
return (clone ? isFunction(clone) ? clone : cloneFnJSON : fnBypass) as unknown as FnCloneOrBypass<R, S>
}

function defaultParse<R, S>(clone?: boolean | CloneFn<R>) {
return (clone ? isFunction(clone) ? clone : fnClone : fnBypass) as unknown as FnCloneOrBypass<S, R>
return (clone ? isFunction(clone) ? clone : cloneFnJSON : fnBypass) as unknown as FnCloneOrBypass<S, R>
}

/**
Expand Down
3 changes: 2 additions & 1 deletion 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<Raw, Serialized = Raw> extends ConfigurableEventFilter {
Expand Down
52 changes: 52 additions & 0 deletions packages/core/useVModel/index.test.ts
Expand Up @@ -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 },
})
})
})
45 changes: 35 additions & 10 deletions 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<T> {
/**
Expand Down Expand Up @@ -29,6 +31,14 @@ export interface UseVModelOptions<T> {
* @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<T>
}

/**
Expand All @@ -46,6 +56,7 @@ export function useVModel<P extends object, K extends keyof P, Name extends stri
options: UseVModelOptions<P[K]> = {},
) {
const {
clone = false,
passive = false,
eventName,
deep = false,
Expand All @@ -71,19 +82,33 @@ export function useVModel<P extends object, K extends keyof P, Name extends stri

event = eventName || event || `update:${key!.toString()}`

const getValue = () => 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<P[K]>(getValue()!)
const initialValue = getValue()
const proxy = ref<P[K]>(initialValue!)

watch(() => props[key!], v => proxy.value = v as UnwrapRef<P[K]>)
watch(
() => props[key!],
v => proxy.value = cloneFn(v) as UnwrapRef<P[K]>,
)

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
}
Expand Down

0 comments on commit 369e177

Please sign in to comment.