Skip to content

Commit

Permalink
feat(signals): withLogger improvements
Browse files Browse the repository at this point in the history
Renamed withStateLogger to withLogger,can long computed props now,  filter function now receives the store, and can log diff of
changes

Fix #165
  • Loading branch information
Gabriel Guerrero authored and gabrielguerrero committed Feb 10, 2025
1 parent 4fc3f89 commit 0fc68f1
Show file tree
Hide file tree
Showing 6 changed files with 367 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
withEntitiesRemotePagination,
withEntitiesRemoteSort,
withEntitiesSingleSelection,
withStateLogger,
withSyncToWebStorage,
} from '@ngrx-traits/signals';
import {
Expand All @@ -30,6 +29,7 @@ import {
} from '@ngrx/signals/entities';
import { lastValueFrom } from 'rxjs';

import { withLogger } from '../../../../../../../libs/ngrx-traits/signals/src/lib/with-logger/with-logger';
import { Product, ProductOrder } from '../../models';
import { OrderService } from '../../services/order.service';
import { ProductService } from '../../services/product.service';
Expand Down Expand Up @@ -100,9 +100,14 @@ const orderItemsStoreFeature = signalStoreFeature(
entity: orderEntity,
collection: orderItemsCollection,
}),
withStateLogger({
withLogger({
name: 'orderItemsStore',
filterState: ({ orderItemsEntityMap }) => ({ orderItemsEntityMap }),
showDiff: true,
// filter: ({ orderItemsEntityMap, orderItemsIds }) => ({
// orderItemsEntityMap,
// orderItemsIds,
// }),
filter: ['orderItemsIdsSelected'],
}),
withSyncToWebStorage({
key: 'orderItems',
Expand Down
1 change: 1 addition & 0 deletions libs/ngrx-traits/signals/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from './with-entities-selection/with-entities-multi-selection';
export * from './with-entities-selection/with-entities-multi-selection.model';
export * from './with-entities-loading-call/with-entities-loading-call';
export * from './with-logger/with-state-logger';
export * from './with-logger/with-logger';
export * from './with-calls/with-calls';
export * from './with-calls/with-calls.model';
export * from './with-calls/call-config';
Expand Down
163 changes: 163 additions & 0 deletions libs/ngrx-traits/signals/src/lib/with-logger/with-logger.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { computed } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import {
patchState,
signalStore,
withComputed,
withState,
} from '@ngrx/signals';

import { withLogger } from './with-logger';

describe('withLogger', () => {
it('should log in the console state and computed signals', () => {
const Store = signalStore(
{ providedIn: 'root', protectedState: false },
withState(() => ({ prop1: 1, prop2: 2 })),
withComputed(({ prop1, prop2 }) => ({
prop3: computed(() => prop1() + prop2()),
})),
withLogger({ name: 'Store' }),
);

const consoleLog = jest.spyOn(console, 'log').mockImplementation(() => {
/* Empty */
});
const store = TestBed.inject(Store);
TestBed.flushEffects();
expect(consoleLog).toBeCalledWith('Store store changed: ', {
prop1: 1,
prop2: 2,
prop3: 3,
});
patchState(store, { prop1: 2 });
TestBed.flushEffects();
expect(consoleLog).toBeCalledWith('Store store changed: ', {
prop1: 2,
prop2: 2,
prop3: 4,
});
});

it('should filter props in array', () => {
const Store = signalStore(
{ providedIn: 'root', protectedState: false },
withState(() => ({ prop1: 1, prop2: 2 })),
withComputed(({ prop1, prop2 }) => ({
prop3: computed(() => prop1() + prop2()),
})),
withLogger({
name: 'Store',
filter: ['prop1', 'prop2'],
}),
);

const consoleLog = jest.spyOn(console, 'log').mockImplementation(() => {
/* Empty */
});
const store = TestBed.inject(Store);
TestBed.flushEffects();
expect(consoleLog).toBeCalledWith('Store store changed: ', {
prop1: 1,
prop2: 2,
});
patchState(store, { prop1: 2 });
TestBed.flushEffects();
expect(consoleLog).toBeCalledWith('Store store changed: ', {
prop1: 2,
prop2: 2,
});
});

it('should filter props in function returning object ', () => {
const Store = signalStore(
{ providedIn: 'root', protectedState: false },
withState(() => ({ prop1: 1, prop2: 2 })),
withComputed(({ prop1, prop2 }) => ({
prop3: computed(() => prop1() + prop2()),
})),
withLogger({
name: 'Store',
filter: ({ prop1, prop2 }) => ({ prop1: prop1(), prop2: prop2() }),
}),
);

const consoleLog = jest.spyOn(console, 'log').mockImplementation(() => {
/* Empty */
});
const store = TestBed.inject(Store);
TestBed.flushEffects();
expect(consoleLog).toBeCalledWith('Store store changed: ', {
prop1: 1,
prop2: 2,
});
patchState(store, { prop1: 2 });
TestBed.flushEffects();
expect(consoleLog).toBeCalledWith('Store store changed: ', {
prop1: 2,
prop2: 2,
});
});

it('should filter props in function returning object with signal props ', () => {
const Store = signalStore(
{ providedIn: 'root', protectedState: false },
withState(() => ({ prop1: 1, prop2: 2 })),
withComputed(({ prop1, prop2 }) => ({
prop3: computed(() => prop1() + prop2()),
})),
withLogger({
name: 'Store',
filter: ({ prop1, prop2 }) => ({ prop1: prop1, prop2: prop2 }),
}),
);

const consoleLog = jest.spyOn(console, 'log').mockImplementation(() => {
/* Empty */
});
const store = TestBed.inject(Store);
TestBed.flushEffects();
expect(consoleLog).toBeCalledWith('Store store changed: ', {
prop1: 1,
prop2: 2,
});
patchState(store, { prop1: 2 });
TestBed.flushEffects();
expect(consoleLog).toBeCalledWith('Store store changed: ', {
prop1: 2,
prop2: 2,
});
});

it('should show diff if showDiff s true', () => {
const Store = signalStore(
{ providedIn: 'root', protectedState: false },
withState(() => ({ prop1: 1, prop2: 2 })),
withComputed(({ prop1, prop2 }) => ({
prop3: computed(() => prop1() + prop2()),
})),
withLogger({
name: 'Store',
filter: ['prop3'],
showDiff: true,
}),
);
jest.resetAllMocks();
const consoleLog = jest.spyOn(console, 'log').mockImplementation(() => {
/* Empty */
});
const store = TestBed.inject(Store);
TestBed.flushEffects();
expect(consoleLog).toBeCalledWith('Store store changed: ', { prop3: 3 });
patchState(store, { prop1: 2 });
jest.resetAllMocks();
TestBed.flushEffects();
expect(consoleLog).toBeCalledWith('Store store changed: ', { prop3: 4 });
expect(consoleLog).toBeCalledWith('Store store changes diff :');
expect(consoleLog).toBeCalledWith(
'%c- prop3: 3\n' + '%c+ prop3: 4',
'color: red',
'color: green',
);
});
});
98 changes: 98 additions & 0 deletions libs/ngrx-traits/signals/src/lib/with-logger/with-logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { computed, effect, isSignal } from '@angular/core';
import {
EmptyFeatureResult,
SignalStoreFeature,
signalStoreFeature,
SignalStoreFeatureResult,
StateSignals,
type,
withHooks,
} from '@ngrx/signals';

import { deepDiff } from './with-logger.util';

/**
* Log the state of the store on every change, optionally filter the signals to log
* the filter prop can receive an array with the names of the props to filter, or you can provide a function
* which receives the store as an argument and should return the object to log, if any of the props in the object is a signal
* it will log the value of the signal. If showDiff is true it will log the diff of the state on every change.
*
* @param name - The name of the store to log
* @param filter - optional filter function to filter the store signals or an array of keys to filter
* @param showDiff - optional flag to log the diff of the state on every change
*
* @example
*
* const Store = signalStore(
* withState(() => ({ prop1: 1, prop2: 2 })),
* withComputed(({ prop1, prop2 }) => ({
* prop3: computed(() => prop1() + prop2()),
* })),
* withLogger({
* name: 'Store',
* // by default it will log all state and computed signals
* // or you can filter with an array of keys
* // filter: ['prop1', 'prop2'],
* // or you can filter with a function
* // filter: ({ prop1, prop2 }) => ({ prop1, prop2 }),
* // showDiff: true,
* }),
* );
*/
export function withLogger<Input extends SignalStoreFeatureResult>({
name,
filter,
showDiff,
}: {
name: string;
filter?:
| ((store: StateSignals<Input['state']> & Input['props']) => any)
| readonly (keyof (StateSignals<Input['state']> & Input['props']))[];
showDiff?: boolean;
}): SignalStoreFeature<Input, EmptyFeatureResult> {
return signalStoreFeature(
type<Input>(),
withHooks({
onInit(store) {
function evaluateSignals(source: any, keys?: string[]) {
return typeof source === 'object'
? Object.keys(source).reduce(
(acc, key) => {
if (!keys || keys.includes(key)) {
if (isSignal(store[key])) {
acc[key] = store[key]();
} else {
acc[key] = store[key];
}
}
return acc;
},
{} as Record<string, any>,
)
: source;
}

const signalsComputed = computed(() => {
return !filter
? evaluateSignals(store)
: typeof filter === 'function'
? evaluateSignals(
filter(
store as StateSignals<Input['state']> & Input['props'],
),
)
: evaluateSignals(store, filter as unknown as string[]);
});
let lastState: any = undefined;
effect(() => {
const state = signalsComputed();
console.log(`${name} store changed: `, state);

if (showDiff && lastState)
deepDiff(`${name} store changes diff :`, lastState, state);
lastState = state;
});
},
}),
);
}
Loading

0 comments on commit 0fc68f1

Please sign in to comment.