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(watchArray): new function #1705

Merged
merged 6 commits into from Jul 6, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
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) // []
Comment on lines +21 to +22
Copy link
Member

Choose a reason for hiding this comment

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

Can added and removed also have meta data like [{ value: 4, index: 3 }]?

})

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)
}