Skip to content

Commit

Permalink
feat: withEntitiesInfinitePagination
Browse files Browse the repository at this point in the history
Added withEntitiesInfinitePagination, to easily implement infinite scroll
Fixed logic for clearing the cache in pagination and selection when filter or sort is called
  • Loading branch information
Gabriel Guerrero authored and gabrielguerrero committed Apr 17, 2024
1 parent e4e5d70 commit fa39f6b
Show file tree
Hide file tree
Showing 16 changed files with 1,012 additions and 277 deletions.
3 changes: 3 additions & 0 deletions libs/ngrx-traits/signals/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
"@ngrx/signals": "^17.1.1",
"rxjs": "^7.8.1"
},
"optionalDependencies": {
"@angular/cdk": "^17.1.0"
},
"sideEffects": false,
"repository": {
"type": "git",
Expand Down
2 changes: 2 additions & 0 deletions libs/ngrx-traits/signals/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export * from './with-entities-filter/with-entities-local-filter';
export * from './with-entities-filter/with-entities-remote-filter';
export * from './with-entities-pagination/with-entities-local-pagination';
export * from './with-entities-pagination/with-entities-remote-pagination';
export * from './with-entities-pagination/with-entities-infinite-pagination';
export * from './with-entities-pagination/signal-infinite-datasource';
export {
Sort,
SortDirection,
Expand Down
30 changes: 30 additions & 0 deletions libs/ngrx-traits/signals/src/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,35 @@ 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 @@ -17,7 +17,7 @@ import { rxMethod } from '@ngrx/signals/rxjs-interop';
import type { StateSignal } from '@ngrx/signals/src/state-signal';
import { pipe, tap } from 'rxjs';

import { getWithEntitiesKeys } from '../util';
import { combineFunctions, getWithEntitiesKeys } from '../util';
import {
debounceFilterPipe,
getWithEntitiesFilterKeys,
Expand Down Expand Up @@ -104,8 +104,11 @@ export type NamedEntitiesFilterMethods<Collection extends string, Filter> = {
*
* // generates the following signals
* store.productsFilter // { search: string }
* // generates the following methods signals
* store.filterProductsEntities // (options: { filter: { search: string }, debounce?: number, patch?: boolean, forceLoad?: boolean }) => void
* // generates the following computed signals
* store.isProductsFilterChanged // boolean
* // generates the following methods
* store.filterProductsEntities // (options: { filter: { search: string }, debounce?: number, patch?: boolean, forceLoad?: boolean }) => void
* store.resetProductsFilter // () => void
*/
export function withEntitiesLocalFilter<
Entity extends { id: string | number },
Expand Down Expand Up @@ -155,10 +158,13 @@ export function withEntitiesLocalFilter<
* }),
* );
*
* // generates the following signals
* // generates the following signals
* store.productsFilter // { search: string }
* // generates the following methods signals
* store.filterProductsEntities // (options: { filter: { search: string }, debounce?: number, patch?: boolean, forceLoad?: boolean }) => void
* // generates the following computed signals
* store.isProductsFilterChanged // boolean
* // generates the following methods
* store.filterProductsEntities // (options: { filter: { search: string }, debounce?: number, patch?: boolean, forceLoad?: boolean }) => void
* store.resetProductsFilter // () => void
*/
export function withEntitiesLocalFilter<
Entity extends { id: string | number },
Expand Down Expand Up @@ -197,7 +203,8 @@ export function withEntitiesLocalFilter<
entity?: Entity;
collection?: Collection;
}): SignalStoreFeature<any, any> {
const { entityMapKey, idsKey } = getWithEntitiesKeys(config);
const { entityMapKey, idsKey, clearEntitiesCacheKey } =
getWithEntitiesKeys(config);
const {
filterEntitiesKey,
filterKey,
Expand All @@ -222,6 +229,7 @@ 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 @@ -243,6 +251,7 @@ export function withEntitiesLocalFilter<
[idsKey]: newEntities.map((entity) => entity.id),
},
);
clearEntitiesCache();
}),
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ 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 type {
CallStateMethods,
NamedCallStateMethods,
Expand Down Expand Up @@ -95,10 +96,13 @@ import type {
* // });
* // },
* })),
* // generates the following signals
* store.productsFilter // { name: string } stored filter
* // generates the following signals
* store.productsFilter // { search: string }
* // generates the following computed signals
* store.isProductsFilterChanged // boolean
* // generates the following methods
* store.filterProductsEntities // (options: { filter: { name: string }, debounce?: number, patch?: boolean, forceLoad?: boolean }) => void
* store.filterProductsEntities // (options: { filter: { search: string }, debounce?: number, patch?: boolean, forceLoad?: boolean }) => void
* store.resetProductsFilter // () => void
*/
export function withEntitiesRemoteFilter<
Entity extends { id: string | number },
Expand Down Expand Up @@ -181,10 +185,13 @@ export function withEntitiesRemoteFilter<
* // });
* // },
* })),
* // generates the following signals
* store.productsFilter // { name: string } stored filter
* // generates the following signals
* store.productsFilter // { search: string }
* // generates the following computed signals
* store.isProductsFilterChanged // boolean
* // generates the following methods
* store.filterProductsEntities // (options: { filter: { name: string }, debounce?: number, patch?: boolean, forceLoad?: boolean }) => void
* store.filterProductsEntities // (options: { filter: { search: string }, debounce?: number, patch?: boolean, forceLoad?: boolean }) => void
* store.resetProductsFilter // () => void
*/

export function withEntitiesRemoteFilter<
Expand Down Expand Up @@ -226,6 +233,8 @@ export function withEntitiesRemoteFilter<
resetEntitiesFilterKey,
isEntitiesFilterChangedKey,
} = getWithEntitiesFilterKeys(config);
const { clearEntitiesCacheKey } = getWithEntitiesKeys(config);

return signalStoreFeature(
withState({ [filterKey]: defaultFilter }),
withComputed((state: Record<string, Signal<unknown>>) => {
Expand All @@ -240,6 +249,8 @@ export function withEntitiesRemoteFilter<
const setLoading = state[setLoadingKey] as () => void;
const filter = state[filterKey] as Signal<Filter>;

const clearEntitiesCache = combineFunctions(state[clearEntitiesCacheKey]);

const filterEntities = rxMethod<{
filter: Filter;
debounce?: number;
Expand All @@ -253,10 +264,12 @@ export function withEntitiesRemoteFilter<
patchState(state as StateSignal<EntitiesFilterState<Filter>>, {
[filterKey]: value.filter,
});
clearEntitiesCache();
}),
),
);
return {
[clearEntitiesCacheKey]: clearEntitiesCache,
[filterEntitiesKey]: filterEntities,
[resetEntitiesFilterKey]: () => {
filterEntities({ filter: defaultFilter });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
runInInjectionContext,
Signal,
} from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import {
patchState,
signalStoreFeature,
Expand All @@ -20,12 +21,25 @@ import {
EntitySignals,
NamedEntitySignals,
} from '@ngrx/signals/entities/src/models';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import type {
EmptyFeatureResult,
SignalStoreFeatureResult,
SignalStoreSlices,
} from '@ngrx/signals/src/signal-store-models';
import { catchError, first, from, map, Observable, of } from 'rxjs';
import {
catchError,
concatMap,
exhaustMap,
first,
from,
map,
Observable,
of,
switchMap,
tap,
} from 'rxjs';
import { filter } from 'rxjs/operators';

import {
CallState,
Expand Down Expand Up @@ -124,6 +138,7 @@ export function withEntitiesLoadingCall<
? { entities: Entity[]; total: number }
: Entity[] | { entities: Entity[] }
>;
mapPipe?: 'switchMap' | 'concatMap' | 'exhaustMap';
}): SignalStoreFeature<
Input & {
state: EntityState<Entity> & CallState;
Expand Down Expand Up @@ -230,6 +245,7 @@ export function withEntitiesLoadingCall<
// ? { entities: Entity[]; total: number }
// : Entity[] | { entities: Entity[] }
>;
mapPipe?: 'switchMap' | 'concatMap' | 'exhaustMap';
}): SignalStoreFeature<
Input & {
state: NamedEntityState<Entity, Collection> & NamedCallState<Collection>;
Expand All @@ -247,6 +263,7 @@ export function withEntitiesLoadingCall<
>({
collection,
fetchEntities,
...config
}: {
entity?: Entity; // is this needed? entity can come from the method fetchEntities return type
collection?: Collection;
Expand All @@ -255,6 +272,7 @@ export function withEntitiesLoadingCall<
Input['signals'] &
Input['methods'],
) => Observable<any> | Promise<any>;
mapPipe?: 'switchMap' | 'concatMap' | 'exhaustMap';
}): SignalStoreFeature<Input, EmptyFeatureResult> {
const { loadingKey, setErrorKey, setLoadedKey } = getWithCallStatusKeys({
prop: collection,
Expand All @@ -273,59 +291,68 @@ export function withEntitiesLoadingCall<

return signalStoreFeature(
withHooks({
onInit: (input, environmentInjector = inject(EnvironmentInjector)) => {
effect(() => {
if (loading()) {
runInInjectionContext(environmentInjector, () => {
from(
fetchEntities({
...store.slices,
...store.signals,
...store.methods,
} as SignalStoreSlices<Input['state']> &
Input['signals'] &
Input['methods']),
)
.pipe(
map((result) => {
if (Array.isArray(result)) {
onInit: (state, environmentInjector = inject(EnvironmentInjector)) => {
const loading$ = toObservable(loading);
const mapPipe = config.mapPipe ? mapPipes[config.mapPipe] : switchMap;

loading$
.pipe(
filter(Boolean),
mapPipe(() =>
runInInjectionContext(environmentInjector, () =>
from(
fetchEntities({
...store.slices,
...store.signals,
...store.methods,
} as SignalStoreSlices<Input['state']> &
Input['signals'] &
Input['methods']),
),
).pipe(
map((result) => {
if (Array.isArray(result)) {
patchState(
state,
collection
? setAllEntities(result as Entity[], {
collection,
})
: setAllEntities(result),
);
} else {
const { entities, total } = result;
if (setEntitiesLoadResult)
setEntitiesLoadResult(entities, total);
else
patchState(
input,
state,
collection
? setAllEntities(result as Entity[], {
? setAllEntities(entities as Entity[], {
collection,
})
: setAllEntities(result),
: setAllEntities(entities),
);
} else {
const { entities, total } = result;
if (setEntitiesLoadResult)
setEntitiesLoadResult(entities, total);
else
patchState(
input,
collection
? setAllEntities(entities as Entity[], {
collection,
})
: setAllEntities(entities),
);
}
setLoaded();
}),
catchError((error: unknown) => {
setError(error);
setLoaded();
return of();
}),
first(),
)
.subscribe();
});
}
});
}
setLoaded();
}),
catchError((error: unknown) => {
setError(error);
setLoaded();
return of();
}),
first(),
),
),
)
.subscribe();
},
}),
)(store); // we execute the factory so we can pass the input
};
}
const mapPipes = {
switchMap: switchMap,
concatMap: concatMap,
exhaustMap: exhaustMap,
};
Loading

0 comments on commit fa39f6b

Please sign in to comment.