diff --git a/packages/vue/src/composables/useLastChanged/index.test.ts b/packages/vue/src/composables/useLastChanged/index.test.ts new file mode 100644 index 0000000..bb6a1fe --- /dev/null +++ b/packages/vue/src/composables/useLastChanged/index.test.ts @@ -0,0 +1,50 @@ +import { ref, nextTick } from 'vue'; +import { describe, it, expect, vi } from 'vitest'; +import { useLastChanged } from '.'; +import { timestamp } from '@robonen/stdlib'; + +describe('useLastChanged', () => { + it('should initialize with null if no initialValue is provided', () => { + const source = ref(0); + const lastChanged = useLastChanged(source); + + expect(lastChanged.value).toBeNull(); + }); + + it('should initialize with the provided initialValue', () => { + const source = ref(0); + const initialValue = 123456789; + const lastChanged = useLastChanged(source, { initialValue }); + + expect(lastChanged.value).toBe(initialValue); + }); + + it('should update the timestamp when the source changes', async () => { + const source = ref(0); + const lastChanged = useLastChanged(source); + + const initialTimestamp = lastChanged.value; + source.value = 1; + await nextTick(); + + expect(lastChanged.value).not.toBe(initialTimestamp); + expect(lastChanged.value).toBeLessThanOrEqual(timestamp()); + }); + + it('should update the timestamp immediately if immediate option is true', async () => { + const source = ref(0); + const lastChanged = useLastChanged(source, { immediate: true }); + + expect(lastChanged.value).toBeLessThanOrEqual(timestamp()); + }); + + it('should not update the timestamp if the source does not change', async () => { + const source = ref(0); + const lastChanged = useLastChanged(source); + + const initialTimestamp = lastChanged.value; + await nextTick(); + + expect(lastChanged.value).toBe(initialTimestamp); + }); +}); \ No newline at end of file diff --git a/packages/vue/src/composables/useLastChanged/index.ts b/packages/vue/src/composables/useLastChanged/index.ts new file mode 100644 index 0000000..c16cded --- /dev/null +++ b/packages/vue/src/composables/useLastChanged/index.ts @@ -0,0 +1,36 @@ +import { timestamp } from '@robonen/stdlib'; +import { ref, watch, type WatchSource, type WatchOptions, type Ref } from 'vue'; + +export interface UseLastChangedOptions< + Immediate extends boolean, + InitialValue extends number | null | undefined = undefined, +> extends WatchOptions { + initialValue?: InitialValue; +} + +/** + * @name useLastChanged + * @category State + * @description Records the last time a value changed + * + * @param {WatchSource} source The value to track + * @param {UseLastChangedOptions} [options={}] The options for the last changed tracker + * @returns {Ref} The timestamp of the last change + * + * @example + * const value = ref(0); + * const lastChanged = useLastChanged(value); + * + * @example + * const value = ref(0); + * const lastChanged = useLastChanged(value, { immediate: true }); + */ +export function useLastChanged(source: WatchSource, options?: UseLastChangedOptions): Ref; +export function useLastChanged(source: WatchSource, options: UseLastChangedOptions | UseLastChangedOptions): Ref +export function useLastChanged(source: WatchSource, options: UseLastChangedOptions = {}): Ref | Ref { + const lastChanged = ref(options.initialValue ?? null); + + watch(source, () => lastChanged.value = timestamp(), options); + + return lastChanged; +}