diff --git a/core/lib/services/checks.spec.ts b/core/lib/services/checks.spec.ts index 4b5fac30a5..de289821a4 100644 --- a/core/lib/services/checks.spec.ts +++ b/core/lib/services/checks.spec.ts @@ -1,5 +1,5 @@ import {describe, expect, test} from 'vitest'; -import {isBoolean, isFunction, isNumber, clamp, isString} from './checks'; +import {isBoolean, isFunction, isNumber, clamp, isString, isArray} from './checks'; describe('Checks', () => { test(`'isNumber' should check if value is a number`, () => { @@ -79,6 +79,25 @@ describe('Checks', () => { expect(isString(() => {})).toBe(false); }); + test(`'isArray' should check if the value is an array`, () => { + expect(isArray(true)).toBe(false); + expect(isArray(false)).toBe(false); + expect(isArray(0)).toBe(false); + expect(isArray(1)).toBe(false); + expect(isArray(1.1)).toBe(false); + expect(isArray('1')).toBe(false); + expect(isArray('0')).toBe(false); + expect(isArray('1.1')).toBe(false); + expect(isArray(undefined)).toBe(false); + expect(isArray(null)).toBe(false); + expect(isArray({})).toBe(false); + expect(isArray([])).toBe(true); + expect(isArray(NaN)).toBe(false); + expect(isArray(Infinity)).toBe(false); + expect(isArray(-Infinity)).toBe(false); + expect(isArray(() => {})).toBe(false); + }); + test(`'getValueInRange' should return a value is within a specific range`, () => { expect(clamp(1, 5)).toBe(1); expect(clamp(-1, 5)).toBe(0); diff --git a/core/lib/services/checks.ts b/core/lib/services/checks.ts index bfdf3e6a8b..fe8f0ebb35 100644 --- a/core/lib/services/checks.ts +++ b/core/lib/services/checks.ts @@ -34,6 +34,12 @@ export function isString(value: any): value is string { return typeof value === 'string'; } +/** + * an array type guard + * @returns true if the value is an array + */ +export const isArray = Array.isArray; + // TODO should we check that max > min? /** * Clamp the value based on a maximum and optional minimum diff --git a/core/lib/services/stores.spec.ts b/core/lib/services/stores.spec.ts index 345220f6e6..4cef54551c 100644 --- a/core/lib/services/stores.spec.ts +++ b/core/lib/services/stores.spec.ts @@ -387,7 +387,9 @@ describe(`Stores service`, () => { const valueMax$ = writable(2); const value$ = bindableDerived(onValueChange$, [dirtyValue$, valueMax$], ([dirtyValue, valueMax]) => Math.min(dirtyValue, valueMax)); - const unsubscribe = value$.subscribe((value) => values.push(value)); + const unsubscribe = value$.subscribe((value) => { + values.push(value); + }); expect(values).toEqual([1]); valueMax$.set(3); // no change expect(onChangeCalls).toEqual([]); @@ -442,5 +444,37 @@ describe(`Stores service`, () => { expect(values).toEqual([1, 2]); unsubscribe(); }); + + test(`should override equals function`, () => { + const onChangeCalls: number[][] = []; + const values: number[][] = []; + const dirtyValue$ = writable([1]); + const onValueChange$ = writable((value: number[]) => { + onChangeCalls.push(value); + }); + + const value$ = bindableDerived( + onValueChange$, + [dirtyValue$], + ([dirtyValue]) => dirtyValue.map((dv) => Math.floor(dv)), + (a, b) => a.every((val, index) => val === b[index]) + ); + value$.subscribe((value) => values.push(value)); + expect(values).toEqual([[1]]); + + dirtyValue$.set([1]); // no change + expect(onChangeCalls).toEqual([]); + expect(values).toEqual([[1]]); + + dirtyValue$.set([2.5]); + expect(dirtyValue$()).toEqual([2]); + expect(onChangeCalls).toEqual([[2]]); + expect(values).toEqual([[1], [2]]); + + dirtyValue$.set([5.6, 7.8]); + expect(dirtyValue$()).toEqual([5, 7]); + expect(onChangeCalls).toEqual([[2], [5, 7]]); + expect(values).toEqual([[1], [2], [5, 7]]); + }); }); }); diff --git a/core/lib/services/stores.ts b/core/lib/services/stores.ts index 2100c07a73..52063e9f66 100644 --- a/core/lib/services/stores.ts +++ b/core/lib/services/stores.ts @@ -321,21 +321,25 @@ export const stateStores = , ...StoreInput[]]>( onChange$: ReadableSignal<(value: T) => void>, stores: U, - adjustValue: (arg: StoresInputValues) => T + adjustValue: (arg: StoresInputValues) => T, + equal = (currentValue: T, newValue: T) => newValue === currentValue ) => { let currentValue = stores[0](); - return derived(stores, (values) => { - const newValue = adjustValue(values); - const rectifiedValue = newValue !== values[0]; - if (rectifiedValue) { - stores[0].set(newValue); - } - if (rectifiedValue || newValue !== currentValue) { - currentValue = newValue; - // TODO check if we should do this async to avoid issue - // with angular and react only when rectifiedValue is true? - onChange$()(newValue); - } - return newValue; + return derived(stores, { + derive(values) { + const newValue = adjustValue(values); + const rectifiedValue = !equal(values[0], newValue); + if (rectifiedValue) { + stores[0].set(newValue); + } + if (rectifiedValue || !equal(currentValue, newValue)) { + currentValue = newValue; + // TODO check if we should do this async to avoid issue + // with angular and react only when rectifiedValue is true? + onChange$()(newValue); + } + return newValue; + }, + equal, }); }; diff --git a/core/lib/services/writables.spec.ts b/core/lib/services/writables.spec.ts new file mode 100644 index 0000000000..704b74f152 --- /dev/null +++ b/core/lib/services/writables.spec.ts @@ -0,0 +1,38 @@ +import {writable} from '@amadeus-it-group/tansu'; +import type {SpyInstance} from 'vitest'; +import {beforeEach, describe, expect, test, vi} from 'vitest'; +import {writableWithDefault} from './stores'; +import {typeArray} from './writables'; + +describe(`Writables service`, () => { + const equal = typeArray.equal!; + let consoleErrorSpy: SpyInstance, ReturnType>; + + beforeEach(() => { + consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + }); + + test(`should support typeArray`, () => { + const config$ = writable([0]); + const store$ = writableWithDefault([0], config$, typeArray); + // set the wrong value + store$.set(100 as any); + expect(consoleErrorSpy).toHaveBeenCalledWith('Not setting invalid value', 100); + // the store is not updated with the wrong value + expect(store$()).toStrictEqual([0]); + + const testArray = [15, 20]; + store$.set(testArray); + + expect(store$()).toBe(testArray); + }); + + test(`typeArray equal function should compare values`, () => { + expect(equal([15, 15], [15, 15])).toBe(true); + expect(equal([15, 15], [15, 10])).toBe(false); + const arr = [15, 15]; + expect(equal(arr, arr)).toBe(true); + expect(equal([15], [15, 15])).toBe(false); + expect(equal([15, 15], [15])).toBe(false); + }); +}); diff --git a/core/lib/services/writables.ts b/core/lib/services/writables.ts index 48d8db1912..132d2c6b5a 100644 --- a/core/lib/services/writables.ts +++ b/core/lib/services/writables.ts @@ -1,4 +1,4 @@ -import {isBoolean, isFunction, isNumber, isString} from './checks'; +import {isArray, isBoolean, isFunction, isNumber, isString} from './checks'; import type {WritableWithDefaultOptions} from './stores'; import {INVALID_VALUE} from './stores'; @@ -23,3 +23,17 @@ export const typeFunction: WritableWithDefaultOptions<(...args: any[]) => any> = normalizeValue: testToNormalizeValue(isFunction), equal: Object.is, }; + +export const typeArray: WritableWithDefaultOptions = { + normalizeValue: testToNormalizeValue(isArray), + equal: (a, b) => { + if (a === b) { + return true; + } else { + if (a?.length !== b?.length) { + return false; + } + return a.every((val, index) => val === b[index]); + } + }, +};