Skip to content

Commit

Permalink
feat(signal-slice): allow supplying external subjects as reducers (#152)
Browse files Browse the repository at this point in the history
* feat(signal-slice): test supplying external subject as reducer

* feat(signal-slice): accept external subject as reducer

* docs(signal-slice): add docs for reducers from external subjects

---------

Co-authored-by: Chau Tran <nartc7789@gmail.com>
  • Loading branch information
joshuamorony and nartc committed Nov 16, 2023
1 parent 04ec0d3 commit 7df93d3
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 10 deletions.
21 changes: 21 additions & 0 deletions docs/src/content/docs/utilities/Signals/signal-slice.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>();

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
Expand Down
21 changes: 20 additions & 1 deletion libs/ngxtension/signal-slice/src/signal-slice.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>();
const state = signalSlice({
initialState,
sources: [trigger$.pipe(map(() => ({ age: testAge })))],
reducers: {
trigger: trigger$,
},
});

state.trigger();

expect(state().age).toEqual(testAge);
});
});
});

describe('asyncReducers', () => {
Expand Down Expand Up @@ -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<number>();

Expand Down
36 changes: 27 additions & 9 deletions libs/ngxtension/signal-slice/src/signal-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ import { connect, type PartialOrValue, type Reducer } from 'ngxtension/connect';
import { Subject, isObservable, take, type Observable } from 'rxjs';

type NamedReducers<TSignalValue> = {
[actionName: string]: (
state: TSignalValue,
value: any
) => PartialOrValue<TSignalValue>;
[actionName: string]:
| Subject<any>
| ((state: TSignalValue, value: any) => PartialOrValue<TSignalValue>);
};

type NamedAsyncReducers<TSignalValue> = {
Expand Down Expand Up @@ -54,7 +53,9 @@ type Action<TSignalValue, TValue> = TValue extends void
type ActionMethod<
TSignalValue,
TReducer extends NamedReducers<TSignalValue>[string]
> = TReducer extends (state: TSignalValue, value: infer TValue) => any
> = TReducer extends
| ((state: TSignalValue, value: infer TValue) => any)
| Subject<infer TValue>
? TValue extends Observable<infer TObservableValue>
? Action<TSignalValue, TObservableValue>
: Action<TSignalValue, TValue>
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 7df93d3

Please sign in to comment.