Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(useVModel): support clone option #2022

Merged
merged 8 commits into from Sep 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 () => {
antfu marked this conversation as resolved.
Show resolved Hide resolved
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