Skip to content

Commit

Permalink
feat(watchArray): new function (#1705)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
w1ndy and antfu committed Jul 6, 2022
1 parent ece3dff commit 2ba83d1
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 0 deletions.
28 changes: 28 additions & 0 deletions packages/shared/watchArray/index.md
@@ -0,0 +1,28 @@
---
category: Watch
---

# watchArray

Watch for an array with additions and removals.

## Usage

Similar to `watch`, but provides the added and removed elements to the callback function. Pass `{ deep: true }` if the list is updated in place with `push`, `splice`, etc.

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

const list = ref([1, 2, 3])

watchArray(list, (newList, oldList, added, removed) => {
console.log(newList) // [1, 2, 3, 4]
console.log(oldList) // [1, 2, 3]
console.log(added) // [4]
console.log(removed) // []
})

onMounted(() => {
list.value = [...list.value, 4]
})
```
96 changes: 96 additions & 0 deletions packages/shared/watchArray/index.test.ts
@@ -0,0 +1,96 @@
import { isVue2, nextTick, reactive, ref } from 'vue-demi'
import { watchArray } from '.'

describe('watchArray', () => {
it('should work when two lists are different', async () => {
const spy = vitest.fn((newList, oldList, added, removed) => {
expect(newList).toEqual([1, 1, 4])
expect(oldList).toEqual([1, 2, 3])
expect(added).toEqual([1, 4])
expect(removed).toEqual([2, 3])
})

const num = ref([1, 2, 3])
watchArray(num, spy)
num.value = [1, 1, 4]
await nextTick()
expect(spy).toBeCalledTimes(1)
})

it('should work when two lists are identical', async () => {
const spy = vitest.fn((newList, oldList, added, removed) => {
expect(newList).toEqual([1, 2, 3])
expect(oldList).toEqual([1, 2, 3])
expect(added).toEqual([])
expect(removed).toEqual([])
})

const num = ref([1, 2, 3])
watchArray(num, spy)
num.value = [1, 2, 3]
await nextTick()
expect(spy).toBeCalledTimes(1)
})

it('should work with list push', async () => {
const spy = vitest.fn((newList, oldList, added, removed) => {
expect(newList).toEqual([1, 2, 3, 4])
expect(oldList).toEqual([1, 2, 3])
expect(added).toEqual([4])
expect(removed).toEqual([])
})

const num = ref([1, 2, 3])
watchArray(num, spy, { deep: true })
num.value.push(4)
await nextTick()
// TODO: Vue 2 somehow get trigger twice
expect(spy).toBeCalledTimes(isVue2 ? 2 : 1)
})

it('should work with list splice', async () => {
const spy = vitest.fn((newList, oldList, added, removed) => {
expect(newList).toEqual([1, 5, 6, 7, 3])
expect(oldList).toEqual([1, 2, 3])
expect(added).toEqual([5, 6, 7])
expect(removed).toEqual([2])
})

const num = ref([1, 2, 3])
watchArray(num, spy, { deep: true })
num.value.splice(1, 1, 5, 6, 7)
await nextTick()
// TODO: Vue 2 somehow get trigger twice
expect(spy).toBeCalledTimes(isVue2 ? 2 : 1)
})

it('should work with reactive source', async () => {
const spy = vitest.fn((newList, oldList, added, removed) => {
expect(newList).toEqual([1, 2, 3, 4])
expect(oldList).toEqual([1, 2, 3])
expect(added).toEqual([4])
expect(removed).toEqual([])
})

const num = reactive([1, 2, 3])
watchArray(num, spy)
num.push(4)
await nextTick()
expect(spy).toBeCalledTimes(1)
})

it('should work with functional source', async () => {
const spy = vitest.fn((newList, oldList, added, removed) => {
expect(newList).toEqual([1, 2, 3, 4])
expect(oldList).toEqual([1, 2, 3])
expect(added).toEqual([4])
expect(removed).toEqual([])
})

const num = ref([1, 2, 3])
watchArray(() => num.value, spy)
num.value = [...num.value, 4]
await nextTick()
expect(spy).toBeCalledTimes(1)
})
})
44 changes: 44 additions & 0 deletions packages/shared/watchArray/index.ts
@@ -0,0 +1,44 @@
import type { WatchOptions, WatchSource } from 'vue-demi'
import { unref, watch } from 'vue-demi'

export declare type WatchArrayCallback<V = any, OV = any> = (value: V, oldValue: OV, added: V, removed: OV, onCleanup: (cleanupFn: () => void) => void) => any

/**
* Watch for an array with additions and removals.
*
* @see https://vueuse.org/watchArray
*/
export function watchArray<T, Immediate extends Readonly<boolean> = false>(
source: WatchSource<T[]> | T[],
cb: WatchArrayCallback<T[], Immediate extends true ? T[] | undefined : T[]>,
options?: WatchOptions<Immediate>,
) {
let oldList: T[] = options?.immediate
? []
: [...(source instanceof Function
? source()
: Array.isArray(source)
? source
: unref(source)),
]

return watch(source as WatchSource<T[]>, (newList, _, onCleanup) => {
const oldListRemains = new Array<boolean>(oldList.length)
const added: T[] = []
for (const obj of newList) {
let found = false
for (let i = 0; i < oldList.length; i++) {
if (!oldListRemains[i] && obj === oldList[i]) {
oldListRemains[i] = true
found = true
break
}
}
if (!found)
added.push(obj)
}
const removed = oldList.filter((_, i) => !oldListRemains[i])
cb(newList, oldList, added, removed, onCleanup)
oldList = [...newList]
}, options)
}

0 comments on commit 2ba83d1

Please sign in to comment.