Skip to content

Commit

Permalink
feat(devtools): add withMapper feature
Browse files Browse the repository at this point in the history
`withMapper` allows mapping the state 
before it is sent to the DevTools.
  • Loading branch information
rainerhahnekamp authored Jan 7, 2025
1 parent 9212013 commit 97c0031
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 20 deletions.
20 changes: 18 additions & 2 deletions apps/demo/src/app/devtools/todo-detail.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { signalStore, withState } from '@ngrx/signals';
import {
renameDevtoolsName,
withDevtools,
withMapper,
} from '@angular-architects/ngrx-toolkit';

/**
Expand All @@ -17,8 +18,23 @@ import {
* run renameDevtoolsStore() in the effect.
*/
const TodoDetailStore = signalStore(
withDevtools('todo-detail'),
withState({ id: 1 })
withDevtools(
'todo-detail',
withMapper((state: Record<string, unknown>) => {
return Object.keys(state).reduce((acc, key) => {
if (key === 'secret') {
return acc;
}
acc[key] = state[key];

return acc;
}, {} as Record<string, unknown>);
})
),
withState({
id: 1,
secret: 'do not show in DevTools',
})
);

@Component({
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/src/app/devtools/todo-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export type AddTodo = Omit<Todo, 'id'>;

export const TodoStore = signalStore(
{ providedIn: 'root' },
withDevtools('todo'),
withDevtools('todo-store'),
withEntities<Todo>(),
withState({
selectedIds: [] as number[],
Expand Down
2 changes: 2 additions & 0 deletions apps/demo/src/app/devtools/todo.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { SelectionModel } from '@angular/cdk/collections';
import { Todo, TodoStore } from './todo-store';
import { TodoDetailComponent } from './todo-detail.component';
import { FormsModule } from '@angular/forms';

@Component({
selector: 'demo-todo',
Expand Down Expand Up @@ -68,6 +69,7 @@ import { TodoDetailComponent } from './todo-detail.component';
MatIconModule,
MatTableModule,
TodoDetailComponent,
FormsModule,
],
})
export class TodoComponent {
Expand Down
10 changes: 9 additions & 1 deletion docs/docs/with-devtools.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ You activate per store:
const Store = signalStore({ providedIn: 'root' }, withDevtools('flights', withDisabledNameIndices()), withState({ airline: 'Lufthansa' }));
```

## `withMapper()`

`withMapper` allows you to define a function that maps the state before it is sent to the Devtools.

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.

````typescript

## Disabling Devtools in production

`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.
Expand All @@ -89,7 +97,7 @@ import { withDevtools } from '@angular-architects/ngrx-toolkit';
export const environment = {
storeWithDevTools: withDevtools,
};
```
````
environments/environment.prod.ts
Expand Down
1 change: 1 addition & 0 deletions libs/ngrx-toolkit/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { withDevToolsStub } from './lib/devtools/with-dev-tools-stub';
export { withDevtools } from './lib/devtools/with-devtools';
export { withDisabledNameIndices } from './lib/devtools/with-disabled-name-indicies';
export { withMapper } from './lib/devtools/with-mapper';
export { patchState, updateState } from './lib/devtools/update-state';
export { renameDevtoolsName } from './lib/devtools/rename-devtools-name';

Expand Down
18 changes: 13 additions & 5 deletions libs/ngrx-toolkit/src/lib/devtools/devtools-feature.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
export const DEVTOOLS_FEATURE = Symbol('DEVTOOLS_FEATURE');

export type Mapper = (state: object) => object;

export type DevtoolsOptions = {
indexNames: boolean; // defines if names should be indexed.
map: Mapper; // defines a mapper for the state.
};

/**
* A DevtoolsFeature adds or modifies the behavior of the
* devtools extension.
*
* We use them (function calls) instead of a config object,
* because of tree-shaking.
*/
export interface DevtoolsFeature {
export type DevtoolsFeature = {
[DEVTOOLS_FEATURE]: true;
indexNames: boolean | undefined; // defines if names should be indexed.
}
} & Partial<DevtoolsOptions>;

export function createDevtoolsFeature(indexNames = true): DevtoolsFeature {
export function createDevtoolsFeature(
options: Partial<DevtoolsOptions>
): DevtoolsFeature {
return {
[DEVTOOLS_FEATURE]: true,
indexNames,
...options,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import {
} from '@angular/core';
import { currentActionNames } from './currrent-action-names';
import { isPlatformBrowser } from '@angular/common';
import { Connection, DevtoolsOptions } from '../with-devtools';
import { Connection } from '../with-devtools';
import { getState, StateSource } from '@ngrx/signals';
import { DevtoolsOptions } from '../devtools-feature';

const dummyConnection: Connection = {
send: () => void true,
Expand Down Expand Up @@ -60,8 +61,8 @@ export class DevtoolsSyncer implements OnDestroy {
const stores = this.#stores();
const rootState: Record<string, unknown> = {};
for (const name in stores) {
const { store } = stores[name];
rootState[name] = getState(store);
const { store, options } = stores[name];
rootState[name] = options.map(getState(store));
}

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

type StoreRegistry = Record<
string,
{ store: StateSource<object>; options: DevtoolsOptions; id: number }
{
store: StateSource<object>;
options: DevtoolsOptions;
id: number;
}
>;
69 changes: 69 additions & 0 deletions libs/ngrx-toolkit/src/lib/devtools/tests/with-mapper.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { setupExtensions } from './helpers.spec';
import { TestBed } from '@angular/core/testing';
import { signalStore, withState } from '@ngrx/signals';
import { withMapper } from '../with-mapper';
import { withDevtools } from '../with-devtools';

function domRemover(state: Record<string, unknown>) {
return Object.keys(state).reduce((acc, key) => {
const value = state[key];

if (value instanceof HTMLElement) {
return acc;
} else {
return { ...acc, [key]: value };
}
}, {});
}

describe('with-mapper', () => {
it('should remove DOM Nodes', () => {
const { sendSpy } = setupExtensions();

const Store = signalStore(
{ providedIn: 'root' },
withState({
name: 'Car',
carElement: document.createElement('div'),
}),
withDevtools('shop', withMapper(domRemover))
);

TestBed.inject(Store);
TestBed.flushEffects();
expect(sendSpy).toHaveBeenCalledWith(
{ type: 'Store Update' },
{ shop: { name: 'Car' } }
);
});

it('should every property ending with *Key', () => {
const { sendSpy } = setupExtensions();
const Store = signalStore(
{ providedIn: 'root' },
withState({
name: 'Car',
unlockKey: '1234',
}),
withDevtools(
'shop',
withMapper((state: Record<string, unknown>) =>
Object.keys(state).reduce((acc, key) => {
if (key.endsWith('Key')) {
return acc;
} else {
return { ...acc, [key]: state[key] };
}
}, {})
)
)
);

TestBed.inject(Store);
TestBed.flushEffects();
expect(sendSpy).toHaveBeenCalledWith(
{ type: 'Store Update' },
{ shop: { name: 'Car' } }
);
});
});
9 changes: 3 additions & 6 deletions libs/ngrx-toolkit/src/lib/devtools/with-devtools.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { signalStoreFeature, withHooks, withMethods } from '@ngrx/signals';
import { inject } from '@angular/core';
import { DevtoolsSyncer } from './internal/devtools-syncer.service';
import { DevtoolsFeature } from './devtools-feature';
import { DevtoolsFeature, DevtoolsOptions } from './devtools-feature';

export type Action = { type: string };
export type Connection = {
Expand All @@ -17,10 +17,6 @@ declare global {
}
}

export type DevtoolsOptions = {
indexNames: boolean;
};

export const existingNames = new Map<string, unknown>();

export const renameDevtoolsMethodName = '___renameDevtoolsName';
Expand All @@ -46,8 +42,9 @@ export function withDevtools(name: string, ...features: DevtoolsFeature[]) {
);
}
existingNames.set(name, true);
const finalOptions = {
const finalOptions: DevtoolsOptions = {
indexNames: !features.some((f) => f.indexNames === false),
map: features.find((f) => f.map)?.map ?? ((state) => state),
};

return signalStoreFeature(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ import { createDevtoolsFeature } from './devtools-feature';
*
*/
export function withDisabledNameIndices() {
return createDevtoolsFeature(false);
return createDevtoolsFeature({ indexNames: false });
}
15 changes: 15 additions & 0 deletions libs/ngrx-toolkit/src/lib/devtools/with-mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createDevtoolsFeature, Mapper } from './devtools-feature';

/**
* Allows you to define a function to map the state.
*
* It is needed for huge states, that slows down the Devtools and where
* you don't need to see the whole state.
*
* @param map function which maps the state
*/
export function withMapper<State extends object>(
map: (state: State) => Record<string, unknown>
) {
return createDevtoolsFeature({ map: map as Mapper });
}

0 comments on commit 97c0031

Please sign in to comment.