Skip to content

Commit

Permalink
feat: new trait withEventHandler
Browse files Browse the repository at this point in the history
New trait to facilitate comunication between traits
  • Loading branch information
Gabriel Guerrero authored and gabrielguerrero committed Apr 21, 2024
1 parent 1acfc8a commit b4c57b8
Show file tree
Hide file tree
Showing 32 changed files with 1,145 additions and 307 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ export const ProductsLocalStore = signalStore(
{ providedIn: 'root' },
withEntities({ entity, collection }),
withCallStatus({ collection, initialValue: 'loading' }),
withEntitiesLocalPagination({
entity,
collection,
pageSize: 5,
}),
withEntitiesLocalFilter({
entity,
collection,
Expand All @@ -95,11 +100,6 @@ export const ProductsLocalStore = signalStore(
!filter?.search ||
entity?.name.toLowerCase().includes(filter?.search.toLowerCase()),
}),
withEntitiesLocalPagination({
entity,
collection,
pageSize: 5,
}),
withEntitiesLocalSort({
entity,
collection,
Expand All @@ -109,12 +109,6 @@ export const ProductsLocalStore = signalStore(
entity,
collection,
}),
withSyncToWebStorage({
key: 'products',
type: 'session',
restoreOnInit: true,
saveStateChangesAfterMs: 300,
}),
withEntitiesLoadingCall({
collection,
fetchEntities: ({ productsFilter }) => {
Expand All @@ -135,7 +129,6 @@ export const ProductsLocalStore = signalStore(
checkout: () => inject(OrderService).checkout(),
})),
);

export const ProductsLocalStore2 = signalStore(
{ providedIn: 'root' },
withEntities({ entity }),
Expand Down
150 changes: 150 additions & 0 deletions libs/ngrx-traits/signals/api-docs.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## Constants

<dl>
<dt><a href="#arraysAreNotAllowedMsg">arraysAreNotAllowedMsg</a></dt>
<dd><p>A function that takes an <code>Event</code> and a <code>State</code>, and returns a <code>State</code>.
See <code>createReducer</code>.</p></dd>
</dl>

## Functions

<dl>
Expand Down Expand Up @@ -62,12 +70,30 @@ and call the api with the [collection]Sort params and use wither setAllEntities
with the sorted result that come from the backend, plus changing the status and set errors is needed.
or use withEntitiesLoadingCall to call the api with the [collection]Sort params which handles setting
the result and errors automatically. Requires withEntities and withCallStatus to be present before this function.</p></dd>
<dt><a href="#withEventHandler">withEventHandler(eventHandlerFactory)</a></dt>
<dd><p>Adds an event handler to the store, allowing the store to listen to events and react to them.
This helps with the communications between different store feature functions, normally a store feature can only
call methods generated by other store featured if it is declared before. With event handlers, you can send events
to other store features, and they can react to them regardless of the order of declaration. This is useful for example between
the filter and pagination store features, filter fires an event when the filter is changed and the pagination store
feature can listen to it and reset the current page back to 0, by using withEventHandler pagination avoids creating a dependency
on the filter store feature, and the order in which they get declared won't affect the communication .</p></dd>
<dt><a href="#createEvent">createEvent(type, config)</a></dt>
<dd><p>Creates a configured <code>Creator</code> function that, when called, returns an object in the shape of the <code>Event</code> interface.</p>
<p>Event creators reduce the explicitness of class-based event creators.</p></dd>
<dt><a href="#withLogger">withLogger(name)</a></dt>
<dd><p>Log the state of the store on every change</p></dd>
<dt><a href="#withSyncToWebStorage">withSyncToWebStorage(key, type, saveStateChangesAfterMs, restoreOnInit)</a></dt>
<dd><p>Sync the state of the store to the web storage</p></dd>
</dl>

<a name="arraysAreNotAllowedMsg"></a>

## arraysAreNotAllowedMsg
<p>A function that takes an <code>Event</code> and a <code>State</code>, and returns a <code>State</code>.
See <code>createReducer</code>.</p>

**Kind**: global constant
<a name="withCallStatus"></a>

## withCallStatus(config)
Expand Down Expand Up @@ -750,6 +776,130 @@ store.productsSort // the current sort
// and the following methods
store.sortProductsEntities // (options: { sort: Sort<Entity>; }) => void;
```
<a name="withEventHandler"></a>
## withEventHandler(eventHandlerFactory)
<p>Adds an event handler to the store, allowing the store to listen to events and react to them.
This helps with the communications between different store feature functions, normally a store feature can only
call methods generated by other store featured if it is declared before. With event handlers, you can send events
to other store features, and they can react to them regardless of the order of declaration. This is useful for example between
the filter and pagination store features, filter fires an event when the filter is changed and the pagination store
feature can listen to it and reset the current page back to 0, by using withEventHandler pagination avoids creating a dependency
on the filter store feature, and the order in which they get declared won't affect the communication .</p>
**Kind**: global function
| Param |
| --- |
| eventHandlerFactory |
**Example**
```js
const increment = createEvent('[Counter] Increment');
const decrement = createEvent('[Counter] Decrement');
const add = createEvent('[Counter] Add', props<{ value: number }>());
const Store = signalStore(
withState({ count: 0 }),
withEventHandler((state) => [
onEvent(increment, () => {
patchState(state, { count: state.count() + 1 });
}),
onEvent(decrement, () => {
patchState(state, { count: state.count() - 1 });
}),
]),
withMethods((state) => {
return {
// this test we can send events to things defined after this method
add5: () => broadcast(state, add({ value: 5 })),
};
}),
withEventHandler((state) => [
onEvent(add, ({ value }) => {
patchState(state, { count: state.count() + value });
}),
]),
withMethods((state) => {
return {
increment: () => broadcast(state, increment()),
decrement: () => broadcast(state, decrement()),
};
}),
);
```
<a name="createEvent"></a>
## createEvent(type, config)
<p>Creates a configured <code>Creator</code> function that, when called, returns an object in the shape of the <code>Event</code> interface.</p>
<p>Event creators reduce the explicitness of class-based event creators.</p>
**Kind**: global function
**Usagenotes**: **Declaring an event creator**
Without additional metadata:
```ts
export const increment = createEvent('[Counter] Increment');
```
With additional metadata:
```ts
export const loginSuccess = createEvent(
'[Auth/API] Login Success',
props<{ user: User }>()
);
```
With a function:
```ts
export const loginSuccess = createEvent(
'[Auth/API] Login Success',
(response: Response) => response.user
);
```
**Dispatching an event**
Without additional metadata:
```ts
store.dispatch(increment());
```
With additional metadata:
```ts
store.dispatch(loginSuccess({ user: newUser }));
```
**Referencing an event in a reducer**
Using a switch statement:
```ts
switch (event.type) {
// ...
case AuthApiEvents.loginSuccess.type: {
return {
...state,
user: event.user
};
}
}
```
Using a reducer creator:
```ts
on(AuthApiEvents.loginSuccess, (state, { user }) => ({ ...state, user }))
```
**Referencing an event in an effect**
```ts
effectName$ = createEffect(
() => this.events$.pipe(
ofType(AuthApiEvents.loginSuccess),
// ...
)
);
```
| Param | Description |
| --- | --- |
| type | <p>Describes the event that will be dispatched</p> |
| config | <p>Additional metadata needed for the handling of the event. See [Usage Notes](createEvent#usage-notes).</p> |
<a name="withLogger"></a>
## withLogger(name)
Expand Down
4 changes: 0 additions & 4 deletions libs/ngrx-traits/signals/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ export * from './with-entities-pagination/with-entities-remote-pagination.model'
export * from './with-entities-pagination/with-entities-remote-scroll-pagination';
export * from './with-entities-pagination/with-entities-remote-scroll-pagination.model';
export * from './with-entities-pagination/signal-infinite-datasource';
export {
Sort,
SortDirection,
} from './with-entities-sort/with-entities-sort.utils';
export * from './with-entities-sort/with-entities-local-sort';
export * from './with-entities-sort/with-entities-local-sort.model';
export * from './with-entities-sort/with-entities-remote-sort';
Expand Down
30 changes: 0 additions & 30 deletions libs/ngrx-traits/signals/src/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,5 @@ export function getWithEntitiesKeys(config?: { collection?: string }) {
idsKey: collection ? `${config.collection}Ids` : 'ids',
entitiesKey: collection ? `${config.collection}Entities` : 'entities',
entityMapKey: collection ? `${config.collection}EntityMap` : 'entityMap',
clearEntitiesCacheKey: collection
? `clearEntities${config.collection}Cache`
: 'clearEntitiesCache',
};
}

export type OverridableFunction = {
(...args: unknown[]): void;
impl?: (...args: unknown[]) => void;
};

export function combineFunctions(
previous?: OverridableFunction,
next?: (...args: unknown[]) => void,
): OverridableFunction {
if (previous && !next) {
return previous;
}
const previousImplementation = previous?.impl;
const fun: OverridableFunction =
previous ??
((...args: unknown[]) => {
fun.impl?.(...args);
});
fun.impl = next
? (...args: unknown[]) => {
previousImplementation?.(...args);
next(...args);
}
: undefined;
return fun;
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ describe('withCalls', () => {
const store = new Store();
expect(store.isTestCallLoading()).toBeFalsy();
store.testCall({ ok: false });
console.log(store.testCallCallStatus());
expect(store.testCallError()).toEqual(new Error('fail'));
expect(store.testCallResult()).toBe(undefined);
});
Expand Down
19 changes: 13 additions & 6 deletions libs/ngrx-traits/signals/src/lib/with-calls/with-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
SignalStoreSlices,
} from '@ngrx/signals/src/signal-store-models';
import type { StateSignal } from '@ngrx/signals/src/state-signal';
import { Prettify } from '@ngrx/signals/src/ts-helpers';
import {
catchError,
concatMap,
Expand Down Expand Up @@ -86,9 +87,12 @@ export function withCalls<
const Calls extends Record<string, Call | CallConfig>,
>(
callsFactory: (
store: SignalStoreSlices<Input['state']> &
Input['signals'] &
Input['methods'],
store: Prettify<
SignalStoreSlices<Input['state']> &
Input['signals'] &
Input['methods'] &
StateSignal<Prettify<Input['state']>>
>,
) => Calls,
): SignalStoreFeature<
Input,
Expand All @@ -111,9 +115,12 @@ export function withCalls<
...store.slices,
...store.signals,
...store.methods,
} as SignalStoreSlices<Input['state']> &
Input['signals'] &
Input['methods']);
} as Prettify<
SignalStoreSlices<Input['state']> &
Input['signals'] &
Input['methods'] &
StateSignal<Prettify<Input['state']>>
>);
const callsState = Object.entries(calls).reduce(
(acc, [callName, call]) => {
const { callStatusKey } = getWithCallStatusKeys({ prop: callName });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import {
} from 'rxjs';

import { capitalize } from '../util';
import {
createEvent,
props,
} from '../with-event-handler/with-event-handler.util';

export function getWithEntitiesFilterKeys(config?: { collection?: string }) {
const collection = config?.collection;
Expand Down Expand Up @@ -52,3 +56,13 @@ export function debounceFilterPipe<Filter, Entity>(filter: Signal<Filter>) {
),
);
}

export function getWithEntitiesFilterEvents(config?: { collection?: string }) {
const collection = config?.collection;
return {
entitiesFilterChanged: createEvent(
`${collection}.entitiesFilterChanged`,
props<{ filter: unknown }>(),
),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ import { rxMethod } from '@ngrx/signals/rxjs-interop';
import type { StateSignal } from '@ngrx/signals/src/state-signal';
import { pipe, tap } from 'rxjs';

import { combineFunctions, getWithEntitiesKeys } from '../util';
import { getWithEntitiesKeys } from '../util';
import {
broadcast,
withEventHandler,
} from '../with-event-handler/with-event-handler';
import {
debounceFilterPipe,
getWithEntitiesFilterEvents,
getWithEntitiesFilterKeys,
} from './with-entities-filter.util';
import {
Expand Down Expand Up @@ -161,8 +166,8 @@ export function withEntitiesLocalFilter<
entity?: Entity;
collection?: Collection;
}): SignalStoreFeature<any, any> {
const { entityMapKey, idsKey, clearEntitiesCacheKey } =
getWithEntitiesKeys(config);
const { entityMapKey, idsKey } = getWithEntitiesKeys(config);
const { entitiesFilterChanged } = getWithEntitiesFilterEvents(config);
const {
filterEntitiesKey,
filterKey,
Expand All @@ -179,6 +184,7 @@ export function withEntitiesLocalFilter<
}),
};
}),
withEventHandler(),
withMethods((state: Record<string, Signal<unknown>>) => {
const filter = state[filterKey] as Signal<Filter>;
const entitiesMap = state[entityMapKey] as Signal<EntityMap<Entity>>;
Expand All @@ -187,7 +193,6 @@ export function withEntitiesLocalFilter<
// the ids array of the state with the filtered ids array, and the state.entities depends on it,
// so hour filter function needs the full list of entities always which will be always so we get them from entityMap
const entities = computed(() => Object.values(entitiesMap()));
const clearEntitiesCache = combineFunctions(state[clearEntitiesCacheKey]);
const filterEntities = rxMethod<{
filter: Filter;
debounce?: number;
Expand All @@ -209,12 +214,11 @@ export function withEntitiesLocalFilter<
[idsKey]: newEntities.map((entity) => entity.id),
},
);
clearEntitiesCache();
broadcast(state, entitiesFilterChanged(value));
}),
),
);
return {
[clearEntitiesCacheKey]: clearEntitiesCache,
[filterEntitiesKey]: filterEntities,
[resetEntitiesFilterKey]: () => {
filterEntities({ filter: defaultFilter });
Expand Down
Loading

0 comments on commit b4c57b8

Please sign in to comment.