diff --git a/examples/react/functional-component-ts/src/App.tsx b/examples/react/functional-component-ts/src/App.tsx index 5e49dc95..c69e67cc 100644 --- a/examples/react/functional-component-ts/src/App.tsx +++ b/examples/react/functional-component-ts/src/App.tsx @@ -3,6 +3,7 @@ import './App.css'; import { useAgile, useWatcher } from '@agile-ts/react'; import { useEvent } from '@agile-ts/event'; import { + COUNTUP, MY_COLLECTION, MY_COMPUTED, MY_EVENT, @@ -13,6 +14,7 @@ import { import { globalBind } from '@agile-ts/core'; let rerenderCount = 0; +let rerenderCountInCountupView = 0; const App = (props: any) => { // Note: Rerenders twice because of React Strickt Mode (also useState does trigger a rerender twice) @@ -56,6 +58,19 @@ const App = (props: any) => { globalBind('__core__', { ...require('./core') }); }, []); + const CountupView = () => { + const countup = useAgile(COUNTUP); + rerenderCountInCountupView++; + return ( +
+

Countup: {countup}

+

+ Rerender Count of count up View: {rerenderCountInCountupView} +

+
+ ); + }; + return (
@@ -124,7 +139,8 @@ const App = (props: any) => { }> Update mySelector value -

{rerenderCount}

+

Rerender Count: {rerenderCount}

+
); diff --git a/examples/react/functional-component-ts/src/core/index.ts b/examples/react/functional-component-ts/src/core/index.ts index 705736db..28d6caca 100644 --- a/examples/react/functional-component-ts/src/core/index.ts +++ b/examples/react/functional-component-ts/src/core/index.ts @@ -30,6 +30,7 @@ App.registerStorage( }) ); +export const COUNTUP = App.createState(1).interval((value) => value + 1, 1000); export const MY_STATE = App.createState('MyState', { key: 'my-state' }); //.persist(); export const MY_STATE_2 = App.createState('MyState2', { key: 'my-state2', diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 6b9cb056..ec8a10e4 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -42,6 +42,8 @@ export class State { public watchers: { [key: string]: StateWatcherCallback } = {}; + public currentInterval?: NodeJS.Timer | number; + /** * @public * State - Class that holds one Value and causes rerender on subscribed Components @@ -402,10 +404,12 @@ export class State { defaultStorageKey: null, }); - if (this.persistent) + if (this.persistent) { Agile.logger.warn( - `By persisting the State '${this._key}' twice you overwrite the old Persistent Instance!` + `By persisting the State '${this._key}' twice you overwrite the old Persistent Instance!`, + this.persistent ); + } // Create persistent -> Persist Value this.persistent = new StatePersistent(this, { @@ -428,19 +432,63 @@ export class State { * @param callback - Callback Function */ public onLoad(callback: (success: boolean) => void): this { - if (this.persistent) { - this.persistent.onLoad = callback; - - // If State is already 'isPersisted' the loading was successful -> callback can be called - if (this.isPersisted) callback(true); - } else { + if (!this.persistent) { Agile.logger.error( `Please make sure you persist the State '${this._key}' before using the 'onLoad' function!` ); + return this; } + + this.persistent.onLoad = callback; + + // If State is already 'isPersisted' the loading was successful -> callback can be called + if (this.isPersisted) callback(true); + return this; } + //========================================================================================================= + // Interval + //========================================================================================================= + /** + * @public + * Calls callback at certain intervals in milliseconds and assigns the callback return value to the State + * @param callback- Callback that is called on each interval and should return the new State value + * @param ms - The intervals in milliseconds + */ + public interval( + callback: (value: ValueType) => ValueType, + ms?: number + ): this { + if (this.currentInterval) { + Agile.logger.warn( + `You can only have one interval active!`, + this.currentInterval + ); + return this; + } + + this.currentInterval = setInterval(() => { + this.set(callback(this._value)); + }, ms ?? 1000); + + return this; + } + + //========================================================================================================= + // Clear Interval + //========================================================================================================= + /** + * @public + * Clears the current Interval + */ + public clearInterval(): void { + if (this.currentInterval) { + clearInterval(this.currentInterval as number); + delete this.currentInterval; + } + } + //========================================================================================================= // Copy //========================================================================================================= diff --git a/packages/core/tests/unit/collection/selector.test.ts b/packages/core/tests/unit/collection/selector.test.ts index 97736f9c..13f18143 100644 --- a/packages/core/tests/unit/collection/selector.test.ts +++ b/packages/core/tests/unit/collection/selector.test.ts @@ -18,7 +18,6 @@ describe('Selector Tests', () => { dummyCollection = new Collection(dummyAgile); jest.spyOn(Selector.prototype, 'select'); - console.warn = jest.fn(); }); it('should create Selector and call initial select (default config)', () => { diff --git a/packages/core/tests/unit/state/state.test.ts b/packages/core/tests/unit/state/state.test.ts index 86d89205..7094f34e 100644 --- a/packages/core/tests/unit/state/state.test.ts +++ b/packages/core/tests/unit/state/state.test.ts @@ -643,7 +643,8 @@ describe('State Tests', () => { }); it('should overwrite existing persistent with a warning', () => { - numberState.persistent = new StatePersistent(numberState); + const oldPersistent = new StatePersistent(numberState); + numberState.persistent = oldPersistent; numberState.persist('newPersistentKey'); @@ -656,7 +657,8 @@ describe('State Tests', () => { defaultStorageKey: null, }); expect(console.warn).toHaveBeenCalledWith( - `Agile Warn: By persisting the State '${numberState._key}' twice you overwrite the old Persistent Instance!` + `Agile Warn: By persisting the State '${numberState._key}' twice you overwrite the old Persistent Instance!`, + oldPersistent ); }); }); @@ -694,6 +696,112 @@ describe('State Tests', () => { }); }); + describe('interval function tests', () => { + const dummyCallbackFunction = jest.fn(); + const dummyCallbackFunction2 = jest.fn(); + + beforeEach(() => { + jest.useFakeTimers(); + numberState.set = jest.fn(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it('should create an interval (without custom milliseconds)', () => { + dummyCallbackFunction.mockReturnValueOnce(10); + + numberState.interval(dummyCallbackFunction); + + jest.runTimersToTime(1000); // travel 1000s in time -> execute interval + + expect(setInterval).toHaveBeenCalledTimes(1); + expect(setInterval).toHaveBeenLastCalledWith( + expect.any(Function), + 1000 + ); + expect(dummyCallbackFunction).toHaveBeenCalledWith(numberState._value); + expect(numberState.set).toHaveBeenCalledWith(10); + expect(numberState.currentInterval).toEqual({ + id: expect.anything(), + ref: expect.anything(), + unref: expect.anything(), + }); + expect(console.warn).not.toHaveBeenCalled(); + }); + + it('should create an interval (with custom milliseconds)', () => { + dummyCallbackFunction.mockReturnValueOnce(10); + + numberState.interval(dummyCallbackFunction, 2000); + + jest.runTimersToTime(2000); // travel 2000 in time -> execute interval + + expect(setInterval).toHaveBeenCalledTimes(1); + expect(setInterval).toHaveBeenLastCalledWith( + expect.any(Function), + 2000 + ); + expect(dummyCallbackFunction).toHaveBeenCalledWith(numberState._value); + expect(numberState.set).toHaveBeenCalledWith(10); + expect(numberState.currentInterval).toEqual({ + id: expect.anything(), + ref: expect.anything(), + unref: expect.anything(), + }); + expect(console.warn).not.toHaveBeenCalled(); + }); + + it("shouldn't be able to create second interval and print warning", () => { + numberState.interval(dummyCallbackFunction, 3000); + const currentInterval = numberState.currentInterval; + numberState.interval(dummyCallbackFunction2); + + expect(setInterval).toHaveBeenCalledTimes(1); + expect(setInterval).toHaveBeenLastCalledWith( + expect.any(Function), + 3000 + ); + expect(numberState.currentInterval).toStrictEqual(currentInterval); + expect(console.warn).toHaveBeenCalledWith( + 'Agile Warn: You can only have one interval active!', + numberState.currentInterval + ); + }); + }); + + describe('clearInterval function tests', () => { + const dummyCallbackFunction = jest.fn(); + + beforeEach(() => { + jest.useFakeTimers(); + numberState.set = jest.fn(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it('should clear existing interval', () => { + numberState.interval(dummyCallbackFunction); + const currentInterval = numberState.currentInterval; + + numberState.clearInterval(); + + expect(clearInterval).toHaveBeenCalledTimes(1); + expect(clearInterval).toHaveBeenLastCalledWith(currentInterval); + expect(numberState.currentInterval).toBeUndefined(); + }); + + it("shouldn't clear not existing interval", () => { + numberState.clearInterval(); + + expect(clearInterval).not.toHaveBeenCalled(); + expect(numberState.currentInterval).toBeUndefined(); + }); + }); + describe('copy function tests', () => { it('should return a reference free copy of the current State Value', () => { jest.spyOn(Utils, 'copy');