diff --git a/docs/src/content/docs/utilities/Signals/signal-slice.md b/docs/src/content/docs/utilities/Signals/signal-slice.md index be87f706..d3d30d7a 100644 --- a/docs/src/content/docs/utilities/Signals/signal-slice.md +++ b/docs/src/content/docs/utilities/Signals/signal-slice.md @@ -120,6 +120,27 @@ The associated action can then be triggered with: this.state.toggleActive(); ``` +If it also possible to supply an external subject as a reducer like this: + +```ts +someAction$ = new Subject(); + +state = signalSlice({ + initialState: this.initialState, + reducers: { + someAction: someAction$, + }, +}); +``` + +This is useful for circumstances where you need any of your `sources` to react +to `someAction$` being triggered. A source can not react to internally created +reducers/actions, but it can react to the externally created subject. Supplying +this subject as a reducer allows you to still trigger it through +`state.someAction()`. This makes using actions more consistent, as everything +can be accessed on the state object, even if you need to create an external +subject. + ## Async Reducers A standard reducer accepts a function that updates the state synchronously. It diff --git a/libs/ngxtension/signal-slice/src/signal-slice.spec.ts b/libs/ngxtension/signal-slice/src/signal-slice.spec.ts index 59f28bac..33dd8376 100644 --- a/libs/ngxtension/signal-slice/src/signal-slice.spec.ts +++ b/libs/ngxtension/signal-slice/src/signal-slice.spec.ts @@ -149,6 +149,25 @@ describe(signalSlice.name, () => { TestBed.flushEffects(); }); }); + + it('should accept an external subject as a reducer', () => { + TestBed.runInInjectionContext(() => { + const testAge = 50; + + const trigger$ = new Subject(); + const state = signalSlice({ + initialState, + sources: [trigger$.pipe(map(() => ({ age: testAge })))], + reducers: { + trigger: trigger$, + }, + }); + + state.trigger(); + + expect(state().age).toEqual(testAge); + }); + }); }); describe('asyncReducers', () => { @@ -210,7 +229,7 @@ describe(signalSlice.name, () => { }); }); - it.only('should resolve to the updated state when async reducer is invoked with a stream and that stream is completed', fakeAsync(() => { + it('should resolve to the updated state when async reducer is invoked with a stream and that stream is completed', fakeAsync(() => { TestBed.runInInjectionContext(() => { const age$ = new Subject(); diff --git a/libs/ngxtension/signal-slice/src/signal-slice.ts b/libs/ngxtension/signal-slice/src/signal-slice.ts index e9005fc8..f2d69bd9 100644 --- a/libs/ngxtension/signal-slice/src/signal-slice.ts +++ b/libs/ngxtension/signal-slice/src/signal-slice.ts @@ -12,10 +12,9 @@ import { connect, type PartialOrValue, type Reducer } from 'ngxtension/connect'; import { Subject, isObservable, take, type Observable } from 'rxjs'; type NamedReducers = { - [actionName: string]: ( - state: TSignalValue, - value: any - ) => PartialOrValue; + [actionName: string]: + | Subject + | ((state: TSignalValue, value: any) => PartialOrValue); }; type NamedAsyncReducers = { @@ -54,7 +53,9 @@ type Action = TValue extends void type ActionMethod< TSignalValue, TReducer extends NamedReducers[string] -> = TReducer extends (state: TSignalValue, value: infer TValue) => any +> = TReducer extends + | ((state: TSignalValue, value: infer TValue) => any) + | Subject ? TValue extends Observable ? Action : Action @@ -177,10 +178,27 @@ export function signalSlice< } for (const [key, reducer] of Object.entries(reducers as TReducers)) { - const subject = new Subject(); - - connect(state, subject, reducer); - addReducerProperties(readonlyState, state$, key, destroyRef, subject, subs); + if (isObservable(reducer)) { + addReducerProperties( + readonlyState, + state$, + key, + destroyRef, + reducer, + subs + ); + } else { + const subject = new Subject(); + connect(state, subject, reducer); + addReducerProperties( + readonlyState, + state$, + key, + destroyRef, + subject, + subs + ); + } } for (const [key, asyncReducer] of Object.entries(