Skip to content

Commit 97c0031

Browse files
feat(devtools): add withMapper feature
`withMapper` allows mapping the state before it is sent to the DevTools.
1 parent 9212013 commit 97c0031

File tree

11 files changed

+141
-20
lines changed

11 files changed

+141
-20
lines changed

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { signalStore, withState } from '@ngrx/signals';
55
import {
66
renameDevtoolsName,
77
withDevtools,
8+
withMapper,
89
} from '@angular-architects/ngrx-toolkit';
910

1011
/**
@@ -17,8 +18,23 @@ import {
1718
* run renameDevtoolsStore() in the effect.
1819
*/
1920
const TodoDetailStore = signalStore(
20-
withDevtools('todo-detail'),
21-
withState({ id: 1 })
21+
withDevtools(
22+
'todo-detail',
23+
withMapper((state: Record<string, unknown>) => {
24+
return Object.keys(state).reduce((acc, key) => {
25+
if (key === 'secret') {
26+
return acc;
27+
}
28+
acc[key] = state[key];
29+
30+
return acc;
31+
}, {} as Record<string, unknown>);
32+
})
33+
),
34+
withState({
35+
id: 1,
36+
secret: 'do not show in DevTools',
37+
})
2238
);
2339

2440
@Component({

apps/demo/src/app/devtools/todo-store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export type AddTodo = Omit<Todo, 'id'>;
2626

2727
export const TodoStore = signalStore(
2828
{ providedIn: 'root' },
29-
withDevtools('todo'),
29+
withDevtools('todo-store'),
3030
withEntities<Todo>(),
3131
withState({
3232
selectedIds: [] as number[],

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { MatTableDataSource, MatTableModule } from '@angular/material/table';
55
import { SelectionModel } from '@angular/cdk/collections';
66
import { Todo, TodoStore } from './todo-store';
77
import { TodoDetailComponent } from './todo-detail.component';
8+
import { FormsModule } from '@angular/forms';
89

910
@Component({
1011
selector: 'demo-todo',
@@ -68,6 +69,7 @@ import { TodoDetailComponent } from './todo-detail.component';
6869
MatIconModule,
6970
MatTableModule,
7071
TodoDetailComponent,
72+
FormsModule,
7173
],
7274
})
7375
export class TodoComponent {

docs/docs/with-devtools.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ You activate per store:
7575
const Store = signalStore({ providedIn: 'root' }, withDevtools('flights', withDisabledNameIndices()), withState({ airline: 'Lufthansa' }));
7676
```
7777

78+
## `withMapper()`
79+
80+
`withMapper` allows you to define a function that maps the state before it is sent to the Devtools.
81+
82+
Sometimes, it is necessary to map the state before it is sent to the Devtools. For example, you might want to exclude some properties, like passwords or other sensitive data.
83+
84+
````typescript
85+
7886
## Disabling Devtools in production
7987

8088
`withDevtools()` is by default enabled in production mode, if you want to tree-shake it from the application bundle you need to abstract it in your environment file.
@@ -89,7 +97,7 @@ import { withDevtools } from '@angular-architects/ngrx-toolkit';
8997
export const environment = {
9098
storeWithDevTools: withDevtools,
9199
};
92-
```
100+
````
93101
94102
environments/environment.prod.ts
95103

libs/ngrx-toolkit/src/index.ts

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
export const DEVTOOLS_FEATURE = Symbol('DEVTOOLS_FEATURE');
22

3+
export type Mapper = (state: object) => object;
4+
5+
export type DevtoolsOptions = {
6+
indexNames: boolean; // defines if names should be indexed.
7+
map: Mapper; // defines a mapper for the state.
8+
};
9+
310
/**
411
* A DevtoolsFeature adds or modifies the behavior of the
512
* devtools extension.
613
*
714
* We use them (function calls) instead of a config object,
815
* because of tree-shaking.
916
*/
10-
export interface DevtoolsFeature {
17+
export type DevtoolsFeature = {
1118
[DEVTOOLS_FEATURE]: true;
12-
indexNames: boolean | undefined; // defines if names should be indexed.
13-
}
19+
} & Partial<DevtoolsOptions>;
1420

15-
export function createDevtoolsFeature(indexNames = true): DevtoolsFeature {
21+
export function createDevtoolsFeature(
22+
options: Partial<DevtoolsOptions>
23+
): DevtoolsFeature {
1624
return {
1725
[DEVTOOLS_FEATURE]: true,
18-
indexNames,
26+
...options,
1927
};
2028
}

libs/ngrx-toolkit/src/lib/devtools/internal/devtools-syncer.service.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import {
88
} from '@angular/core';
99
import { currentActionNames } from './currrent-action-names';
1010
import { isPlatformBrowser } from '@angular/common';
11-
import { Connection, DevtoolsOptions } from '../with-devtools';
11+
import { Connection } from '../with-devtools';
1212
import { getState, StateSource } from '@ngrx/signals';
13+
import { DevtoolsOptions } from '../devtools-feature';
1314

1415
const dummyConnection: Connection = {
1516
send: () => void true,
@@ -60,8 +61,8 @@ export class DevtoolsSyncer implements OnDestroy {
6061
const stores = this.#stores();
6162
const rootState: Record<string, unknown> = {};
6263
for (const name in stores) {
63-
const { store } = stores[name];
64-
rootState[name] = getState(store);
64+
const { store, options } = stores[name];
65+
rootState[name] = options.map(getState(store));
6566
}
6667

6768
const names = Array.from(currentActionNames);
@@ -137,5 +138,9 @@ Enable automatic indexing via withDevTools('${storeName}', { indexNames: true })
137138

138139
type StoreRegistry = Record<
139140
string,
140-
{ store: StateSource<object>; options: DevtoolsOptions; id: number }
141+
{
142+
store: StateSource<object>;
143+
options: DevtoolsOptions;
144+
id: number;
145+
}
141146
>;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { setupExtensions } from './helpers.spec';
2+
import { TestBed } from '@angular/core/testing';
3+
import { signalStore, withState } from '@ngrx/signals';
4+
import { withMapper } from '../with-mapper';
5+
import { withDevtools } from '../with-devtools';
6+
7+
function domRemover(state: Record<string, unknown>) {
8+
return Object.keys(state).reduce((acc, key) => {
9+
const value = state[key];
10+
11+
if (value instanceof HTMLElement) {
12+
return acc;
13+
} else {
14+
return { ...acc, [key]: value };
15+
}
16+
}, {});
17+
}
18+
19+
describe('with-mapper', () => {
20+
it('should remove DOM Nodes', () => {
21+
const { sendSpy } = setupExtensions();
22+
23+
const Store = signalStore(
24+
{ providedIn: 'root' },
25+
withState({
26+
name: 'Car',
27+
carElement: document.createElement('div'),
28+
}),
29+
withDevtools('shop', withMapper(domRemover))
30+
);
31+
32+
TestBed.inject(Store);
33+
TestBed.flushEffects();
34+
expect(sendSpy).toHaveBeenCalledWith(
35+
{ type: 'Store Update' },
36+
{ shop: { name: 'Car' } }
37+
);
38+
});
39+
40+
it('should every property ending with *Key', () => {
41+
const { sendSpy } = setupExtensions();
42+
const Store = signalStore(
43+
{ providedIn: 'root' },
44+
withState({
45+
name: 'Car',
46+
unlockKey: '1234',
47+
}),
48+
withDevtools(
49+
'shop',
50+
withMapper((state: Record<string, unknown>) =>
51+
Object.keys(state).reduce((acc, key) => {
52+
if (key.endsWith('Key')) {
53+
return acc;
54+
} else {
55+
return { ...acc, [key]: state[key] };
56+
}
57+
}, {})
58+
)
59+
)
60+
);
61+
62+
TestBed.inject(Store);
63+
TestBed.flushEffects();
64+
expect(sendSpy).toHaveBeenCalledWith(
65+
{ type: 'Store Update' },
66+
{ shop: { name: 'Car' } }
67+
);
68+
});
69+
});

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { signalStoreFeature, withHooks, withMethods } from '@ngrx/signals';
22
import { inject } from '@angular/core';
33
import { DevtoolsSyncer } from './internal/devtools-syncer.service';
4-
import { DevtoolsFeature } from './devtools-feature';
4+
import { DevtoolsFeature, DevtoolsOptions } from './devtools-feature';
55

66
export type Action = { type: string };
77
export type Connection = {
@@ -17,10 +17,6 @@ declare global {
1717
}
1818
}
1919

20-
export type DevtoolsOptions = {
21-
indexNames: boolean;
22-
};
23-
2420
export const existingNames = new Map<string, unknown>();
2521

2622
export const renameDevtoolsMethodName = '___renameDevtoolsName';
@@ -46,8 +42,9 @@ export function withDevtools(name: string, ...features: DevtoolsFeature[]) {
4642
);
4743
}
4844
existingNames.set(name, true);
49-
const finalOptions = {
45+
const finalOptions: DevtoolsOptions = {
5046
indexNames: !features.some((f) => f.indexNames === false),
47+
map: features.find((f) => f.map)?.map ?? ((state) => state),
5148
};
5249

5350
return signalStoreFeature(

libs/ngrx-toolkit/src/lib/devtools/with-disabled-name-indicies.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,5 @@ import { createDevtoolsFeature } from './devtools-feature';
2626
*
2727
*/
2828
export function withDisabledNameIndices() {
29-
return createDevtoolsFeature(false);
29+
return createDevtoolsFeature({ indexNames: false });
3030
}

0 commit comments

Comments
 (0)