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(useCloned): new function #2045

Merged
merged 12 commits into from Sep 5, 2022
1 change: 1 addition & 0 deletions packages/core/index.ts
Expand Up @@ -18,6 +18,7 @@ export * from './useBroadcastChannel'
export * from './useBrowserLocation'
export * from './useCached'
export * from './useClipboard'
export * from './useCloned'
export * from './useColorMode'
export * from './useConfirmDialog'
export * from './useCssVar'
Expand Down
16 changes: 16 additions & 0 deletions packages/core/useCloned/demo.vue
@@ -0,0 +1,16 @@
<script setup lang="ts">
import { useCloned } from '@vueuse/core'

const template = { fruit: 'banana', drink: 'water' }

const { cloned, sync } = useCloned(template)
</script>

<template>
<input v-model="cloned.fruit" type="text">
<input v-model="cloned.drink" type="text">

<button @click="sync()">
reset
</button>
</template>
61 changes: 61 additions & 0 deletions packages/core/useCloned/index.md
@@ -0,0 +1,61 @@
---
category: Utilities
---

# useCloned

Reactive partial clone.
chaii3 marked this conversation as resolved.
Show resolved Hide resolved

## Usage
```ts
import { useCloned } from '@vueuse/core/useCloned'
chaii3 marked this conversation as resolved.
Show resolved Hide resolved

const originData = ref({
key: 'value'
})

const { cloned, stop } = useCloned(originData)

originData.key = 'some new value'

console.log(cloned.value.key) // 'some new value'

stop()
```

## Manual cloning
```ts
import { useCloned } from '@vueuse/core/useCloned'

const data = ref({
key: 'value'
})

const { cloned, sync } = useCloned(data, { manual: true })

data.key = 'manual'

console.log(cloned.value.key) // 'value'

sync()

console.log(cloned.value.key)// 'manual'
```

## Custom clone function usage
```ts
import { useCloned } from '@vueuse/core/useCloned'

const data = ref({
key: 'value'
})

const { cloned, sync } = useCloned(data, {
cloneFunction: (source, cloned) => ({ ...source, isCloned: true })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should show a case of using deepclone or something instead of show the example for adding extra properties (which kinda violate the idea of cloneing)

})

data.value.key = 'clone it'

console.log(cloned.value.isCloned) // true
console.log(cloned.value.key) // 'clone it'
```
76 changes: 76 additions & 0 deletions packages/core/useCloned/index.test.ts
@@ -0,0 +1,76 @@
import { useCloned } from '@vueuse/core'
import { expect } from 'vitest'
import { nextTick, ref } from 'vue-demi'

describe('useCloned', () => {
it('works with simple objects', () => {
const data = { test: 'test' }

const { cloned, sync } = useCloned(data)

expect(cloned.value).toEqual(data)

cloned.value = { test: 'failed' }

sync()

expect(cloned.value).toEqual(data)
})

it('works with refs', async () => {
const data = ref({ test: 'test' })

const { cloned } = useCloned(data)

data.value.test = 'success'

await nextTick()

expect(cloned.value).toEqual(data.value)
})

it('works with refs and manual sync', async () => {
const data = ref({ test: 'test' })

const { cloned, sync } = useCloned(data, { manual: true })

data.value.test = 'success'

expect(cloned.value).not.toEqual(data.value)

sync()

expect(cloned.value).toEqual(data.value)
})

it('works like partial cloning', async () => {
const data = ref({ test: 'test' })

const { cloned } = useCloned<Record<string, unknown>>(data)

cloned.value.check = 'value'

data.value.test = 'partial'

await nextTick()

expect(cloned.value.check).toBe('value')
expect(cloned.value.test).toBe('partial')
})

it('works with custom clone function', async () => {
const data = ref({ test: 'test' })

const { cloned } = useCloned<Record<string, any>>(data, { cloneFunction: (source, cloned) => ({ ...cloned, ...source, proxyTest: true }) })

cloned.value.check = 'value'

data.value.test = 'partial'

await nextTick()

expect(cloned.value.check).toBe('value')
expect(cloned.value.test).toBe('partial')
expect(cloned.value.proxyTest).toBe(true)
})
})
43 changes: 43 additions & 0 deletions packages/core/useCloned/index.ts
@@ -0,0 +1,43 @@
import type { MaybeRef } from '@vueuse/shared'
import type { Ref, WatchStopHandle } from 'vue-demi'
import { isRef, ref, unref, watch } from 'vue-demi'

export interface UseClonedOptions<T extends Record<any, any> = Record<any, any>> {
antfu marked this conversation as resolved.
Show resolved Hide resolved
/**
* sync source only by function
*/
manual?: boolean
/**
* Custom clone function should return new value for cloned data
*/
cloneFunction?: (source: Partial<T>, cloned: Partial<T>) => Partial<T> | T
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cloneFunction?: (source: Partial<T>, cloned: Partial<T>) => Partial<T> | T
cloneFunction?: (source: T) => | T

And we could default it to JSON.parse(JSON.stringify(source))

}

export function useCloned<T extends Record<any, any> = Record<any, any>>(source: Ref<Partial<T>>, options: UseClonedOptions & { manual: true }): { cloned: Ref<T>; sync: () => void }
export function useCloned<T extends Record<any, any> = Record<any, any>>(source: Ref<Partial<T>>, options?: UseClonedOptions & { manual: false } | Omit<UseClonedOptions, 'manual'>): { cloned: Ref<T>; sync: () => void; stop: WatchStopHandle }
export function useCloned<T extends Record<any, any> = Record<any, any>>(source: Partial<T>): { cloned: Ref<T>; sync: () => void; stop: WatchStopHandle }
export function useCloned<T extends Record<any, any> = Record<any, any>>(source: MaybeRef<Partial<T>>, options: UseClonedOptions = {}) {
const cloned = ref<T>({} as T)

const { manual, cloneFunction } = options

let stopWatcher: undefined | WatchStopHandle

if (!manual && isRef(source))
stopWatcher = watch(source, sync, { immediate: true, deep: true })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make the watch options configurable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make the watch options configurable.

Should we set immediate: true and deep: true by defaults ? I think we should

else
sync()

function sync() {
if (cloneFunction) {
cloned.value = cloneFunction(unref(source), cloned.value)

return
}

for (const key in unref(source))
cloned.value[key] = unref(source)[key as keyof T]
}

return { cloned, sync, ...(stopWatcher && { stop: stopWatcher }) } as any
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return { cloned, sync, ...(stopWatcher && { stop: stopWatcher }) } as any
return { cloned, sync }

We don't expose the stop function, ppl could use effectScope to capture it if needed.

}