Skip to content

Commit fc972d5

Browse files
feat(devtools): add withGlitchTracking feature
It tracks all state changes of the State, including intermediary updates that are typically suppressed by Angular's glitch-free mechanism. This feature is especially useful for debugging. Example: ```typescript const Store = signalStore( { providedIn: 'root' }, withState({ count: 0 }), withDevtools('counter', withGlitchTracking()), withMethods((store) => ({ increase: () => patchState(store, (value) => ({ count: value.count + 1 })) })) ); // would show up in the DevTools with value 0 const store = inject(Store); store.increase(); // would show up in the DevTools with value 1 store.increase(); // would show up in the DevTools with value 2 store.increase(); // would show up in the DevTools with value 3 ```
1 parent b778ca2 commit fc972d5

File tree

14 files changed

+616
-117
lines changed

14 files changed

+616
-117
lines changed

apps/demo/e2e/devtools.spec.ts

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,50 @@ test('has title', async ({ page }) => {
4949
const devtoolsActions = await page.evaluate(() => window['devtoolsSpy']);
5050

5151
expect(devtoolsActions).toEqual([
52-
{ type: 'add todo' },
53-
{ type: 'select todo 1' },
54-
{ type: 'Store Update' },
55-
{ type: 'select todo 4' },
56-
{ type: 'Store Update' },
57-
{ type: 'select todo 1' },
58-
{ type: 'Store Update' },
59-
{ type: 'select todo 4' },
60-
{ type: 'Store Update' },
52+
{
53+
type: 'add todo',
54+
},
55+
{
56+
type: 'select todo 1',
57+
},
58+
{
59+
type: 'Store Update',
60+
},
61+
{
62+
type: 'Store Update',
63+
},
64+
{
65+
type: 'Store Update',
66+
},
67+
{
68+
type: 'select todo 4',
69+
},
70+
{
71+
type: 'Store Update',
72+
},
73+
{
74+
type: 'Store Update',
75+
},
76+
{
77+
type: 'Store Update',
78+
},
79+
{
80+
type: 'select todo 1',
81+
},
82+
{
83+
type: 'Store Update',
84+
},
85+
{
86+
type: 'Store Update',
87+
},
88+
{
89+
type: 'select todo 4',
90+
},
91+
{
92+
type: 'Store Update',
93+
},
94+
{
95+
type: 'Store Update',
96+
},
6197
]);
6298
});

apps/demo/src/app/devtools/todo-detail.component.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { Component, effect, inject, input } from '@angular/core';
22
import { MatCardModule } from '@angular/material/card';
33
import { Todo } from './todo-store';
4-
import { signalStore, withState } from '@ngrx/signals';
4+
import { patchState, signalStore, withHooks, withState } from '@ngrx/signals';
55
import {
66
renameDevtoolsName,
77
withDevtools,
8+
withGlitchTracking,
89
withMapper,
910
} from '@angular-architects/ngrx-toolkit';
1011

@@ -29,12 +30,15 @@ const TodoDetailStore = signalStore(
2930

3031
return acc;
3132
}, {} as Record<string, unknown>);
32-
})
33+
}),
34+
withGlitchTracking()
3335
),
3436
withState({
3537
id: 1,
3638
secret: 'do not show in DevTools',
37-
})
39+
active: false,
40+
}),
41+
withHooks((store) => ({ onInit: () => patchState(store, { active: true }) }))
3842
);
3943

4044
@Component({

docs/docs/with-devtools.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,37 @@ export class TodoDetailComponent {
6363
}
6464
```
6565

66+
## `withGlitchTracking()`
67+
68+
It tracks all state changes of the State, including intermediary updates
69+
that are typically suppressed by Angular's glitch-free mechanism.
70+
71+
This feature is especially useful for debugging.
72+
73+
Example:
74+
75+
```typescript
76+
const Store = signalStore(
77+
{ providedIn: 'root' },
78+
withState({ count: 0 }),
79+
withDevtools('counter', withGlitchTracking()),
80+
withMethods((store) => ({
81+
increase: () => patchState(store, (value) => ({ count: value.count + 1 })),
82+
}))
83+
);
84+
85+
// would show up in the DevTools with value 0
86+
const store = inject(Store);
87+
88+
store.increase(); // would show up in the DevTools with value 1
89+
store.increase(); // would show up in the DevTools with value 2
90+
store.increase(); // would show up in the DevTools with value 3
91+
```
92+
93+
Without `withGlitchTracking`, the DevTools would only show the final value of 3.
94+
95+
It is also possible to mix. So one store could have `withGlitchTracking` and another one not.
96+
6697
## `withDisabledNameIndices()`
6798

6899
`withDevtools` foresees the possibility to add features which extend or modify it. At the moment, `withDisabledNameIndices` is the only feature available. It disables the automatic indexing of the store names in the Devtools.

libs/ngrx-toolkit/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { withDevToolsStub } from './lib/devtools/with-dev-tools-stub';
22
export { withDevtools } from './lib/devtools/with-devtools';
33
export { withDisabledNameIndices } from './lib/devtools/features/with-disabled-name-indicies';
44
export { withMapper } from './lib/devtools/features/with-mapper';
5+
export { withGlitchTracking } from './lib/devtools/features/with-glitch-tracking';
56
export { patchState, updateState } from './lib/devtools/update-state';
67
export { renameDevtoolsName } from './lib/devtools/rename-devtools-name';
78

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,35 @@
1-
import { Injectable } from '@angular/core';
2-
3-
@Injectable({ providedIn: 'root' })
4-
export class GlitchTrackingService {}
1+
import { createDevtoolsFeature } from '../internal/devtools-feature';
2+
import { GlitchTrackerService } from '../internal/glitch-tracker.service';
53

64
/**
7-
* Track all state changes of the State, including intermediary updates
5+
* It tracks all state changes of the State, including intermediary updates
86
* that are typically suppressed by Angular's glitch-free mechanism.
7+
*
8+
* This feature is especially useful for debugging.
9+
*
10+
* Example:
11+
*
12+
* <pre>
13+
* const Store = signalStore(
14+
* { providedIn: 'root' },
15+
* withState({ count: 0 }),
16+
* withDevtools('counter', withGlitchTracking()),
17+
* withMethods((store) => ({
18+
* increase: () =>
19+
* patchState(store, (value) => ({ count: value.count + 1 })),
20+
* }))
21+
* );
22+
*
23+
* // would show up in the DevTools with value 0
24+
* const store = inject(Store);
25+
*
26+
* store.increase(); // would show up in the DevTools with value 1
27+
* store.increase(); // would show up in the DevTools with value 2
28+
* store.increase(); // would show up in the DevTools with value 3
29+
* </pre>
30+
*
31+
* Without `withGlitchTracking`, the DevTools would only show the final value of 3.
932
*/
1033
export function withGlitchTracking() {
11-
throw new Error('Not implemented');
34+
return createDevtoolsFeature({ tracker: GlitchTrackerService });
1235
}

libs/ngrx-toolkit/src/lib/devtools/features/with-mapper.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,26 @@ import { createDevtoolsFeature, Mapper } from '../internal/devtools-feature';
44
* Allows you to define a function to map the state.
55
*
66
* It is needed for huge states, that slows down the Devtools and where
7-
* you don't need to see the whole state.
7+
* you don't need to see the whole state or other reasons.
8+
*
9+
* Example:
10+
*
11+
* <pre>
12+
* const initialState = {
13+
* id: 1,
14+
* email: 'john.list@host.com',
15+
* name: 'John List',
16+
* enteredPassword: ''
17+
* }
18+
*
19+
* const Store = signalStore(
20+
* withState(initialState),
21+
* withDevtools(
22+
* 'user',
23+
* withMapper(state => ({state, { enteredPassword: '***' }}))
24+
* )
25+
* )
26+
* </pre>
827
*
928
* @param map function which maps the state
1029
*/
Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
import { effect, Injectable, signal } from '@angular/core';
2-
import { StateSource } from '@ngrx/signals';
2+
import { getState, StateSource } from '@ngrx/signals';
33
import { Tracker, TrackerStores } from './models';
44

55
@Injectable({ providedIn: 'root' })
66
export class DefaultTracker implements Tracker {
77
readonly #stores = signal<TrackerStores>({});
8-
#trackCallback: undefined | (() => void);
8+
9+
get stores(): TrackerStores {
10+
return this.#stores();
11+
}
12+
13+
#trackCallback: undefined | ((changedState: Record<string, object>) => void);
914

1015
#trackingEffect = effect(() => {
1116
if (this.#trackCallback === undefined) {
1217
throw new Error('no callback function defined');
1318
}
14-
this.#stores(); // track stores
15-
this.#trackCallback();
19+
const stores = this.#stores();
20+
21+
const fullState = Object.entries(stores).reduce((acc, [id, store]) => {
22+
return { ...acc, [id]: getState(store) };
23+
}, {} as Record<string, object>);
24+
25+
this.#trackCallback(fullState);
1626
});
1727

1828
track(id: string, store: StateSource<object>): void {
@@ -22,7 +32,7 @@ export class DefaultTracker implements Tracker {
2232
}));
2333
}
2434

25-
onChange(callback: () => void): void {
35+
onChange(callback: (changedState: Record<string, object>) => void): void {
2636
this.#trackCallback = callback;
2737
}
2838

@@ -37,10 +47,11 @@ export class DefaultTracker implements Tracker {
3747
);
3848
}
3949

40-
getStores(): Record<string, StateSource<object>> {
41-
return Object.entries(this.#stores()).reduce((states, [key, store]) => {
42-
states[key] = store;
43-
return states;
44-
}, {} as Record<string, StateSource<object>>);
50+
notifyRenamedStore(id: string): void {
51+
if (this.#stores()[id]) {
52+
this.#stores.update((stores) => {
53+
return { ...stores };
54+
});
55+
}
4556
}
4657
}

0 commit comments

Comments
 (0)