Skip to content

Commit

Permalink
feat(useCloned): new function (#2045)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
chaii3 and antfu committed Sep 5, 2022
1 parent db7ffa6 commit 0a0a1da
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 0 deletions.
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>
53 changes: 53 additions & 0 deletions packages/core/useCloned/index.md
@@ -0,0 +1,53 @@
---
category: Utilities
---

# useCloned

Reactive clone of a ref. By default, it use `JSON.parse(JSON.stringify())` to do the clone.

## Usage

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

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

const { cloned } = useCloned(original)

original.key = 'some new value'

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

## Manual cloning

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

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

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

original.key = 'manual'

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

sync()

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

## Custom Clone Function

Using [`klona`](https://www.npmjs.com/package/klona) for example:

```ts
import { useCloned } from '@vueuse/core'
import { klona } from 'klona'

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

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

84 changes: 84 additions & 0 deletions packages/core/useCloned/index.test.ts
@@ -0,0 +1,84 @@
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<Record<string, any>>({ test: 'test' })

const { cloned } = useCloned(data, {
clone: source => ({ ...source, proxyTest: true }),
})

data.value.test = 'partial'

await nextTick()

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, { 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)
})
})
65 changes: 65 additions & 0 deletions packages/core/useCloned/index.ts
@@ -0,0 +1,65 @@
import type { MaybeComputedRef } from '@vueuse/shared'
import type { ComputedRef, WatchOptions } from 'vue-demi'
import { isRef, ref, unref, watch } from 'vue-demi'

export interface UseClonedOptions<T = any> extends WatchOptions {
/**
* Custom clone function.
*
* By default, it use `JSON.parse(JSON.stringify(value))` to clone.
*/
clone?: (source: T) => T

/**
* Manually sync the ref
*
* @default false
*/
manual?: boolean
}

export interface UseClonedReturn<T> {
/**
* Cloned ref
*/
cloned: ComputedRef<T>
/**
* Sync cloned data with source manually
*/
sync: () => void
}

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

export function useCloned<T>(
source: MaybeComputedRef<T>,
options: UseClonedOptions = {},
) {
const cloned = ref<T>({} as T)
const {
manual,
clone = cloneFnJSON,
// watch options
deep = true,
immediate = true,
} = options

function sync() {
cloned.value = clone(unref(source))
}

if (!manual && isRef(source)) {
watch(source, sync, {
...options,
deep,
immediate,
})
}
else {
sync()
}

return { cloned, sync }
}

0 comments on commit 0a0a1da

Please sign in to comment.