Skip to content

Commit bf40871

Browse files
committed
feat: add debounceSignal
1 parent e86b7eb commit bf40871

File tree

6 files changed

+225
-0
lines changed

6 files changed

+225
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# ngxtension/debounce-signal
2+
3+
Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/debounce-signal`.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"lib": {
3+
"entryFile": "src/index.ts"
4+
}
5+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "ngxtension/debounce-signal",
3+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
4+
"projectType": "library",
5+
"sourceRoot": "libs/ngxtension/debounce-signal/src",
6+
"targets": {
7+
"test": {
8+
"executor": "@nx/jest:jest",
9+
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
10+
"options": {
11+
"jestConfig": "libs/ngxtension/jest.config.ts",
12+
"testPathPattern": ["debounce-signal"]
13+
}
14+
},
15+
"lint": {
16+
"executor": "@nx/eslint:lint",
17+
"outputs": ["{options.outputFile}"]
18+
}
19+
}
20+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { debounceSignal } from './debounce-signal';
3+
4+
describe(debounceSignal.name, () => {
5+
describe('data types', () => {
6+
beforeEach(() => {
7+
jest.useFakeTimers(); // Enable fake timers BEFORE any code using timers runs
8+
});
9+
10+
afterEach(() => {
11+
jest.useRealTimers(); // Restore real timers AFTER each test
12+
jest.clearAllMocks();
13+
});
14+
15+
it('number', () => {
16+
TestBed.runInInjectionContext(() => {
17+
const s = debounceSignal(1, 300);
18+
19+
expect(s()).toEqual(1);
20+
s.set(2);
21+
expect(s()).toEqual(1); // Still the initial value
22+
23+
// Advance the clock by the debounce time + 1ms
24+
jest.advanceTimersByTime(301);
25+
expect(s()).toEqual(2); // Now the debounced value
26+
});
27+
});
28+
29+
it('multiple changes number', () => {
30+
TestBed.runInInjectionContext(() => {
31+
const s = debounceSignal(1, 300);
32+
33+
expect(s()).toEqual(1);
34+
s.set(2);
35+
s.set(3);
36+
s.set(4);
37+
expect(s()).toEqual(1); // Still the initial value
38+
39+
// Advance the clock by the debounce time + 1ms
40+
jest.advanceTimersByTime(301);
41+
expect(s()).toEqual(4); // Now the debounced value
42+
});
43+
});
44+
45+
it('string', () => {
46+
TestBed.runInInjectionContext(() => {
47+
const s = debounceSignal('1', 300);
48+
49+
expect(s()).toEqual('1');
50+
s.set('2');
51+
expect(s()).toEqual('1');
52+
53+
jest.advanceTimersByTime(301);
54+
expect(s()).toEqual('2');
55+
});
56+
});
57+
58+
it('multiple changes string', () => {
59+
TestBed.runInInjectionContext(() => {
60+
const s = debounceSignal('1', 300);
61+
62+
expect(s()).toEqual('1');
63+
s.set('2');
64+
s.set('3');
65+
s.set('4');
66+
expect(s()).toEqual('1');
67+
68+
jest.advanceTimersByTime(301);
69+
expect(s()).toEqual('4');
70+
});
71+
});
72+
73+
it('should handle immediate updates followed by debounced updates', () => {
74+
TestBed.runInInjectionContext(() => {
75+
const s = debounceSignal(1, 300);
76+
expect(s()).toBe(1);
77+
78+
s.set(2);
79+
expect(s()).toBe(1);
80+
81+
jest.advanceTimersByTime(150);
82+
s.set(3);
83+
expect(s()).toBe(1);
84+
85+
jest.advanceTimersByTime(151);
86+
expect(s()).toBe(1);
87+
88+
jest.advanceTimersByTime(150);
89+
expect(s()).toBe(3);
90+
});
91+
});
92+
93+
it('object', () => {
94+
TestBed.runInInjectionContext(() => {
95+
const initialObject = { a: 1, b: { c: 2 } };
96+
const s = debounceSignal(initialObject, 300);
97+
98+
expect(s()).toEqual(initialObject);
99+
100+
const updatedObject = { a: 2, b: { c: 3 } };
101+
s.set(updatedObject);
102+
expect(s()).toEqual(initialObject);
103+
104+
jest.advanceTimersByTime(301);
105+
expect(s()).toEqual(updatedObject);
106+
});
107+
});
108+
109+
it('array', () => {
110+
TestBed.runInInjectionContext(() => {
111+
const initialArray = [1, 2, 3];
112+
const s = debounceSignal(initialArray, 300);
113+
114+
expect(s()).toEqual(initialArray);
115+
116+
const updatedArray = [4, 5, 6];
117+
s.set(updatedArray);
118+
expect(s()).toEqual(initialArray);
119+
120+
jest.advanceTimersByTime(301);
121+
expect(s()).toEqual(updatedArray);
122+
});
123+
});
124+
125+
it('nested object property', () => {
126+
TestBed.runInInjectionContext(() => {
127+
const initialObject = { a: 1, b: { c: 2 } };
128+
const s = debounceSignal(initialObject, 300);
129+
130+
expect(s().b.c).toEqual(2);
131+
132+
s.update((val) => ({ ...val, b: { ...val.b, c: 3 } }));
133+
134+
expect(s().b.c).toEqual(2);
135+
136+
jest.advanceTimersByTime(301);
137+
expect(s().b.c).toEqual(3);
138+
});
139+
});
140+
141+
it('multiple changes to nested object property', () => {
142+
TestBed.runInInjectionContext(() => {
143+
const initialObject = { a: 1, b: { c: 2 } };
144+
const s = debounceSignal(initialObject, 300);
145+
146+
expect(s().b.c).toEqual(2);
147+
148+
s.update((val) => ({ ...val, b: { ...val.b, c: 3 } }));
149+
s.update((val) => ({ ...val, b: { ...val.b, c: 4 } }));
150+
s.update((val) => ({ ...val, b: { ...val.b, c: 5 } }));
151+
152+
expect(s().b.c).toEqual(2);
153+
154+
jest.advanceTimersByTime(301);
155+
expect(s().b.c).toEqual(5);
156+
});
157+
});
158+
});
159+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { CreateSignalOptions, WritableSignal } from '@angular/core';
2+
import {
3+
SIGNAL,
4+
SignalGetter,
5+
signalSetFn,
6+
signalUpdateFn,
7+
} from '@angular/core/primitives/signals';
8+
import { createSignal } from 'ngxtension/create-signal';
9+
10+
export function debounceSignal<T>(
11+
initialValue: T,
12+
time: number,
13+
options?: CreateSignalOptions<T>,
14+
): WritableSignal<T> {
15+
const signalFn = createSignal(initialValue) as SignalGetter<T> &
16+
WritableSignal<T>;
17+
const node = signalFn[SIGNAL];
18+
if (options?.equal) {
19+
node.equal = options.equal;
20+
}
21+
22+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
23+
24+
signalFn.set = (newValue: T) => {
25+
clearTimeout(timeoutId);
26+
27+
timeoutId = setTimeout(() => signalSetFn(node, newValue), time);
28+
};
29+
30+
signalFn.update = (updateFn: (value: T) => T) => {
31+
clearTimeout(timeoutId);
32+
33+
timeoutId = setTimeout(() => signalUpdateFn(node, updateFn), time);
34+
};
35+
36+
return signalFn;
37+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './debounce-signal';

0 commit comments

Comments
 (0)