Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function withMyEntity<Entity>(loadMethod: (id: number) => Promise<Entity>) {
const UserStore = signalStore(
{ providedIn: 'root' },
withMethods(() => ({
findById(id: number) {
findById(_id: number) {
return of({ id: 1, name: 'Konrad' });
},
})),
Expand Down
5 changes: 5 additions & 0 deletions docs/docs/with-devtools.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export const bookEvents = eventGroup({
source: 'Book Store',
events: {
loadBooks: type<void>(),
bookSelected: type<{ bookId: string }>(),
},
});

Expand All @@ -202,6 +203,10 @@ const Store = signalStore(
on(bookEvents.loadBooks, () => ({
books: mockBooks,
})),
// DevTools action will include payload: { bookId: string }
on(bookEvents.bookSelected, ({ payload }) => ({
selectedBookId: payload.bookId,
})),
),
withHooks({
onInit() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const currentActionNames = new Set<string>();
import { Action } from './models';

export const currentActionNames = new Set<string | Action>();
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,27 @@ import { StateSource } from '@ngrx/signals';
import { REDUX_DEVTOOLS_CONFIG } from '../provide-devtools-config';
import { currentActionNames } from './current-action-names';
import { DevtoolsInnerOptions } from './devtools-feature';
import { Connection, StoreRegistry, Tracker } from './models';
import { Action, Connection, StoreRegistry, Tracker } from './models';

const dummyConnection: Connection = {
send: () => void true,
};

function toDevtoolsAction(actions: (string | Action)[]): Action {
if (!actions.length) {
return { type: 'Store Update' } as Action;
}

const objects = actions.filter((a): a is Action => typeof a === 'object');
const type = [
...new Set(actions.map((a) => (typeof a === 'string' ? a : a.type))),
].join(', ');

return objects.length
? ({ ...objects[0], type } as Action)
: ({ type } as Action);
}

/**
* A service provided by the root injector is
* required because the synchronization runs
Expand Down Expand Up @@ -90,11 +105,11 @@ export class DevtoolsSyncer implements OnDestroy {
...mappedChangedStatePerName,
};

const names = Array.from(currentActionNames);
const type = names.length ? names.join(', ') : 'Store Update';
const actions = Array.from(currentActionNames);
const action = toDevtoolsAction(actions);
currentActionNames.clear();

this.#connection.send({ type }, this.#currentState);
this.#connection.send(action, this.#currentState);
}

getNextId() {
Expand Down
5 changes: 4 additions & 1 deletion libs/ngrx-toolkit/src/lib/devtools/internal/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { StateSource } from '@ngrx/signals';
import { ReduxDevtoolsConfig } from '../provide-devtools-config';
import { DevtoolsInnerOptions } from './devtools-feature';

export type Action = { type: string };
declare const __actionBrand: unique symbol;
export type Action = { type: string; [key: string]: unknown } & {
readonly [__actionBrand]: true;
};
export type Connection = {
send: (action: Action, state: Record<string, unknown>) => void;
};
Expand Down
27 changes: 26 additions & 1 deletion libs/ngrx-toolkit/src/lib/devtools/tests/action-name.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TestBed } from '@angular/core/testing';
import { signalStore, withMethods, withState } from '@ngrx/signals';
import { updateState } from '../update-state';
import { asAction, updateState } from '../update-state';
import { withDevtools } from '../with-devtools';
import { setupExtensions } from './helpers.spec';

Expand Down Expand Up @@ -45,4 +45,29 @@ describe('updateState', () => {
{ shop: { name: 'i4' } },
);
});

it('should set and send an action object', () => {
const { sendSpy } = setupExtensions();

const Store = signalStore(
{ providedIn: 'root' },
withDevtools('shop'),
withState({ name: 'Car' }),
withMethods((store) => ({
setName(name: string) {
updateState(store, asAction({ type: 'Set Name', name }), { name });
},
})),
);
const store = TestBed.inject(Store);
TestBed.flushEffects();

store.setName('i4');
TestBed.flushEffects();

expect(sendSpy).toHaveBeenLastCalledWith(
{ type: 'Set Name', name: 'i4' },
{ shop: { name: 'i4' } },
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const testEvents = eventGroup({
source: 'Spec Store',
events: {
bump: type<void>(),
bookSelected: type<{ bookId: string }>(),
},
});

Expand Down Expand Up @@ -79,6 +80,29 @@ describe('withTrackedReducer', () => {
);
});

it('should send event payload to devtools when using tracked reducer', () => {
const { sendSpy, withBasicStore } = setup();

const Store = signalStore(
{ providedIn: 'root' },
withBasicStore('store'),
withTrackedReducer(
on(testEvents.bookSelected, ({ payload }) => ({
count: Number(payload.bookId),
})),
),
);

TestBed.inject(Store);

dispatchBookSelectedEvent('42');

expect(sendSpy).toHaveBeenLastCalledWith(
{ type: '[Spec Store] bookSelected', payload: { bookId: '42' } },
{ store: { count: 42 } },
);
});

it('should distinguish between two synchronous state changes in reducer and normal patchState', () => {
const { sendSpy, withBasicStore } = setup();

Expand Down Expand Up @@ -260,6 +284,10 @@ function dispatchBumpEvent() {
TestBed.inject(Dispatcher).dispatch(testEvents.bump());
}

function dispatchBookSelectedEvent(bookId: string) {
TestBed.inject(Dispatcher).dispatch(testEvents.bookSelected({ bookId }));
}

function setup() {
const { sendSpy } = setupExtensions();

Expand Down
11 changes: 10 additions & 1 deletion libs/ngrx-toolkit/src/lib/devtools/update-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
WritableStateSource,
} from '@ngrx/signals';
import { currentActionNames } from './internal/current-action-names';
import { Action } from './internal/models';

type PatchFn = typeof originalPatchState extends (
arg1: infer First,
Expand All @@ -19,6 +20,14 @@ export const patchState: PatchFn = (state, action, ...rest) => {
updateState(state, action, ...rest);
};

/**
* Casts an object with a `type` property to an {@link Action}.
* Use this when you need to pass a structured action object to {@link updateState}.
*/
export function asAction<T extends { type: string }>(value: T): Action {
return value as unknown as Action;
}

/**
* Wrapper of `patchState` for DevTools integration. Next to updating the state,
* it also sends the action to the DevTools.
Expand All @@ -28,7 +37,7 @@ export const patchState: PatchFn = (state, action, ...rest) => {
*/
export function updateState<State extends object>(
stateSource: WritableStateSource<State>,
action: string,
action: string | Action,
...updaters: Array<
Partial<NoInfer<State>> | PartialStateUpdater<NoInfer<State>>
>
Expand Down
4 changes: 2 additions & 2 deletions libs/ngrx-toolkit/src/lib/devtools/with-tracked-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from '@ngrx/signals/events';
import { tap } from 'rxjs/operators';
import { GLITCH_TRACKING_FEATURE } from './features/with-glitch-tracking';
import { updateState } from './update-state';
import { asAction, updateState } from './update-state';
import { DEVTOOL_FEATURE_NAMES } from './with-devtools';

export function withTrackedReducer<State extends object>(
Expand All @@ -39,7 +39,7 @@ export function withTrackedReducer<State extends object>(
const result = caseReducer.reducer(event, state);
const updaters = Array.isArray(result) ? result : [result];

updateState(store, event.type, ...updaters);
updateState(store, asAction(event), ...updaters);
}),
),
),
Expand Down
Loading