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 4 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
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 },
})
})
})
18 changes: 16 additions & 2 deletions packages/core/useVModel/index.ts
@@ -1,7 +1,9 @@
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'

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

export interface UseVModelOptions<T> {
/**
* When passive is set to `true`, it will use `watch` to sync with props and ref.
Expand Down Expand Up @@ -29,8 +31,17 @@ export interface UseVModelOptions<T> {
* @default undefined
*/
defaultValue?: T
/**
* Clone when getting the value from props, shortcut for: JSON.parse(JSON.stringify(value)).
* Default to false
*
* @default false
*/
clone?: boolean | CloneFn<T>
}

const defaultCloneFn = <F, T = F>(v: F): T => JSON.parse(JSON.stringify(v))

/**
* Shorthand for v-model binding, props + emit -> ref
*
Expand All @@ -46,6 +57,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 @@ -72,9 +84,11 @@ 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]) => isFunction(clone) ? clone(val) : defaultCloneFn(val)

if (passive) {
const proxy = ref<P[K]>(getValue()!)
const initialValue = getValue()
const proxy = ref<P[K]>(clone && initialValue ? cloneFn(initialValue) : initialValue!)

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

Expand Down