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>
60 changes: 60 additions & 0 deletions packages/core/useCloned/index.md
@@ -0,0 +1,60 @@
---
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 } = useCloned(originData)

originData.key = 'some new value'

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

```

## 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'
```
85 changes: 85 additions & 0 deletions packages/core/useCloned/index.test.ts
@@ -0,0 +1,85 @@
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 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)
})

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

const { cloned } = useCloned(data, { watchOptions: { immediate: false, deep: false } })

await nextTick()

// test immediate: false
expect(cloned.value).toEqual({})

data.value.test = 'not valid'

await nextTick()

// test deep: false
expect(cloned.value).toEqual({})

data.value = { test: 'valid' }

await nextTick()

expect(cloned.value).toEqual(data.value)
})
})
41 changes: 41 additions & 0 deletions packages/core/useCloned/index.ts
@@ -0,0 +1,41 @@
import type { MaybeRef } from '@vueuse/shared'
import type { WatchOptions } 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: T, cloned: T) => T
/**
* Options for watcher
*
* @default { immediate: true, deep: true }
*/
watchOptions?: WatchOptions
}

export function useCloned<T extends Record<any, any> = Record<any, any>>(source: MaybeRef<T>, options: UseClonedOptions = {}) {
antfu marked this conversation as resolved.
Show resolved Hide resolved
const cloned = ref<T>({} as T)

const { manual, cloneFunction, watchOptions = { immediate: true, deep: true } } = options

if (!manual && isRef(source))
watch(source, sync, watchOptions)
else
sync()

function defaultCloning() {
Copy link
Contributor

Choose a reason for hiding this comment

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

actually there's a native deep clone api called: structuredClone, and supported quite well on different browsers. Use it if exists on window.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

actually there's a native deep clone api called: structuredClone, and supported quite well on different browsers. Use it if exists on window.

Hm, I like it, I'll add this one. Thank you!

Copy link
Member

Choose a reason for hiding this comment

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

structuredClone is not available in all browsers. We should let users pass it explicitly instead of using the current fallback, which will lead to different behavior on different browsers implicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

structuredClone is not available in all browsers. We should let users pass it explicitly instead of using the current fallback, which will lead to different behavior on different browsers implicitly.

removed structuredClone

return JSON.parse(JSON.stringify(unref(source)))
}

function sync() {
cloned.value = cloneFunction ? cloneFunction(unref(source), cloned.value) : defaultCloning()
}

return { cloned, sync }
}