-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(signals): withLogger improvements
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
1 parent
4fc3f89
commit 0fc68f1
Showing
6 changed files
with
367 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
163 changes: 163 additions & 0 deletions
163
libs/ngrx-traits/signals/src/lib/with-logger/with-logger.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
98
libs/ngrx-traits/signals/src/lib/with-logger/with-logger.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}); | ||
}, | ||
}), | ||
); | ||
} |
Oops, something went wrong.