From 2ba83d10ae55f2905a2e9ddc3a587027a43cc37a Mon Sep 17 00:00:00 2001 From: Di Weng Date: Thu, 7 Jul 2022 02:51:13 +0800 Subject: [PATCH] feat(watchArray): new function (#1705) Co-authored-by: Anthony Fu --- packages/shared/watchArray/index.md | 28 +++++++ packages/shared/watchArray/index.test.ts | 96 ++++++++++++++++++++++++ packages/shared/watchArray/index.ts | 44 +++++++++++ 3 files changed, 168 insertions(+) create mode 100644 packages/shared/watchArray/index.md create mode 100644 packages/shared/watchArray/index.test.ts create mode 100644 packages/shared/watchArray/index.ts diff --git a/packages/shared/watchArray/index.md b/packages/shared/watchArray/index.md new file mode 100644 index 00000000000..d14e00fa8e9 --- /dev/null +++ b/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] +}) +``` diff --git a/packages/shared/watchArray/index.test.ts b/packages/shared/watchArray/index.test.ts new file mode 100644 index 00000000000..d40b94bee87 --- /dev/null +++ b/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) + }) +}) diff --git a/packages/shared/watchArray/index.ts b/packages/shared/watchArray/index.ts new file mode 100644 index 00000000000..84da09f5fd6 --- /dev/null +++ b/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 = (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 = false>( + source: WatchSource | T[], + cb: WatchArrayCallback, + options?: WatchOptions, +) { + let oldList: T[] = options?.immediate + ? [] + : [...(source instanceof Function + ? source() + : Array.isArray(source) + ? source + : unref(source)), + ] + + return watch(source as WatchSource, (newList, _, onCleanup) => { + const oldListRemains = new Array(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) +}