Skip to content

Commit

Permalink
feat(signals): add skipLoadingCall to remote filter, sort and pagination
Browse files Browse the repository at this point in the history
New param skipLoadingCall for filterEntities, sortEntities and loadEntitiesPage to disable auto
calling set loading automatically, necesary to better control when the loading happens

fix #114
  • Loading branch information
Gabriel Guerrero authored and gabrielguerrero committed Jul 19, 2024
1 parent 3641a04 commit 081f87d
Show file tree
Hide file tree
Showing 10 changed files with 328 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ export function debounceFilterPipe<Filter>(
debounce?: number;
patch?: boolean;
forceLoad?: boolean;
skipLoadingCall?: boolean;
}) =>
value?.forceLoad ? of({}) : timer(value.debounce || defaultDebounce),
value?.forceLoad ? of({}) : timer(value.debounce ?? defaultDebounce),
),
concatMap((payload) =>
payload.patch
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export type EntitiesRemoteFilterMethods<Filter> = {
filterEntities: (
options:
| {
filter: Filter;
debounce?: number;
patch?: false | undefined;
forceLoad?: boolean;
skipLoadingCall?: boolean;
}
| {
filter: Partial<Filter>;
debounce?: number;
patch: true;
forceLoad?: boolean;
skipLoadingCall?: boolean;
},
) => void;
resetEntitiesFilter: (options?: {
debounce?: number;
forceLoad?: boolean;
skipLoadingCall?: boolean;
}) => void;
};
export type NamedEntitiesRemoteFilterMethods<
Collection extends string,
Filter,
> = {
[K in Collection as `filter${Capitalize<string & K>}Entities`]: (
options:
| {
filter: Filter;
debounce?: number;
patch?: false | undefined;
forceLoad?: boolean;
skipLoadingCall?: boolean;
}
| {
filter: Partial<Filter>;
debounce?: number;
patch: true;
forceLoad?: boolean;
skipLoadingCall?: boolean;
},
) => void;
} & {
[K in Collection as `reset${Capitalize<string & K>}Filter`]: (options?: {
debounce?: number;
forceLoad?: boolean;
skipLoadingCall?: boolean;
}) => void;
};
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,39 @@ describe('withEntitiesRemoteFilter', () => {
});
}));

it('should not filter entities is skipLoadingCall is true but should store filter', fakeAsync(() => {
TestBed.runInInjectionContext(() => {
const store = new Store();
TestBed.flushEffects();
store.filterEntities({
filter: { search: 'zero', foo: 'bar2' },
skipLoadingCall: true,
});
expect(store.entities().length).toEqual(mockProducts.length);
tick(400);
expect(store.entitiesFilter()).toEqual({ search: 'zero', foo: 'bar2' });
expect(store.entities().length).toEqual(mockProducts.length);
// now we manually trigger the loading call and should filter
store.setLoading();
tick(400);
expect(store.entities().length).toEqual(2);
expect(store.entities()).toEqual([
{
description: 'Super Nintendo Game',
id: '1',
name: 'F-Zero',
price: 12,
},
{
description: 'GameCube Game',
id: '80',
name: 'F-Zero GX',
price: 55,
},
]);
});
}));

it('should filter entities after provide debounce', fakeAsync(() => {
TestBed.runInInjectionContext(() => {
const store = new Store();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,23 @@ import {
getWithEntitiesFilterKeys,
} from './with-entities-filter.util';
import {
EntitiesFilterMethods,
EntitiesFilterState,
NamedEntitiesFilterMethods,
NamedEntitiesFilterState,
} from './with-entities-local-filter.model';
import {
EntitiesRemoteFilterMethods,
NamedEntitiesRemoteFilterMethods,
} from './with-entities-remote-filter.model';

/**
* Generates necessary state, computed and methods for remotely filtering entities in the store,
* the generated filter[collection]Entities method will filter the entities by calling set[collection]Loading()
* and you should either create an effect that listens toe [collection]Loading can call the api with the [collection]Filter params
* and you should either create an effect that listens to [collection]Loading can call the api with the [collection]Filter params
* or use withEntitiesLoadingCall to call the api with the [collection]Filter params
* and is debounced by default.
* and is debounced by default. You can change the debounce by using the debounce option filter[collection]Entities or changing the defaultDebounce prop in the config.
*
* In case you dont want filter[collection]Entities to call set[collection]Loading() (which triggers a fetchEntities), you can pass skipLoadingCall: true to filter[collection]Entities.
* Useful in cases where you want to further change the state before manually calling set[collection]Loading() to trigger a fetch of entities.
*
* Requires withEntities and withCallStatus to be present before this function.
* @param config
Expand Down Expand Up @@ -108,10 +113,9 @@ import {
* // 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.filterProductsEntities // (options: { filter: { search: string }, debounce?: number, patch?: boolean, forceLoad?: boolean, skipLoadingCall?:boolean }) => void
* store.resetProductsFilter // () => void
*/

export function withEntitiesRemoteFilter<
Entity,
Collection extends string,
Expand All @@ -130,16 +134,19 @@ export function withEntitiesRemoteFilter<
{
state: NamedEntitiesFilterState<Collection, Filter>;
computed: {};
methods: NamedEntitiesFilterMethods<Collection, Filter>;
methods: NamedEntitiesRemoteFilterMethods<Collection, Filter>;
}
>;

/**
* Generates necessary state, computed and methods for remotely filtering entities in the store,
* the generated filter[collection]Entities method will filter the entities by calling set[collection]Loading()
* and you should either create an effect that listens toe [collection]Loading can call the api with the [collection]Filter params
* and you should either create an effect that listens to [collection]Loading can call the api with the [collection]Filter params
* or use withEntitiesLoadingCall to call the api with the [collection]Filter params
* and is debounced by default.
* and is debounced by default. You can change the debounce by using the debounce option filter[collection]Entities or changing the defaultDebounce prop in the config.
*
* In case you dont want filter[collection]Entities to call set[collection]Loading() (which triggers a fetchEntities), you can pass skipLoadingCall: true to filter[collection]Entities.
* Useful in cases where you want to further change the state before manually calling set[collection]Loading() to trigger a fetch of entities.
*
* Requires withEntities and withCallStatus to be present before this function.
* @param config
Expand Down Expand Up @@ -205,7 +212,7 @@ export function withEntitiesRemoteFilter<
* // 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.filterProductsEntities // (options: { filter: { search: string }, debounce?: number, patch?: boolean, forceLoad?: boolean, skipLoadingCall?:boolean }) => void
* store.resetProductsFilter // () => void
*/
export function withEntitiesRemoteFilter<
Expand All @@ -224,7 +231,7 @@ export function withEntitiesRemoteFilter<
{
state: EntitiesFilterState<Filter>;
computed: {};
methods: EntitiesFilterMethods<Filter>;
methods: EntitiesRemoteFilterMethods<Filter>;
}
>;

Expand Down Expand Up @@ -270,11 +277,12 @@ export function withEntitiesRemoteFilter<
debounce?: number;
patch?: boolean;
forceLoad?: boolean;
skipLoadingCall?: boolean;
}>(
pipe(
debounceFilterPipe(filter, config.defaultDebounce),
tap((value) => {
setLoading();
if (!value?.skipLoadingCall) setLoading();
patchState(state as StateSignal<EntitiesFilterState<Filter>>, {
[filterKey]: value.filter,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,21 @@ export type NamedEntitiesPaginationRemoteComputed<
}>;
};

export type EntitiesPaginationRemoteMethods<Entity> =
EntitiesPaginationLocalMethods &
SetEntitiesResult<{ entities: Entity[]; total: number }>;
export type EntitiesPaginationRemoteMethods<Entity> = {
loadEntitiesPage: (options: {
pageIndex: number;
pageSize?: number;
skipLoadingCall?: boolean;
}) => void;
} & SetEntitiesResult<{ entities: Entity[]; total: number }>;

export type NamedEntitiesPaginationRemoteMethods<
Entity,
Collection extends string,
> = NamedEntitiesPaginationLocalMethods<Collection> &
NamedSetEntitiesResult<Collection, { entities: Entity[]; total: number }>;
> = {
[K in Collection as `load${Capitalize<string & K>}Page`]: (options: {
pageIndex: number;
pageSize?: number;
skipLoadingCall?: boolean;
}) => void;
} & NamedSetEntitiesResult<Collection, { entities: Entity[]; total: number }>;
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,72 @@ describe('withEntitiesRemotePagination', () => {
});
}));

it('test when a requested page is not cache doesnt get loaded if skipLoadingCall is true', fakeAsync(() => {
TestBed.runInInjectionContext(() => {
const fetchEntitiesSpy = jest.fn();
const Store = signalStore(
withEntities({ entity }),
withCallStatus(),
withEntitiesRemotePagination({ entity, pageSize: 10 }),
withEntitiesLoadingCall({
fetchEntities: ({ entitiesPagedRequest }) => {
fetchEntitiesSpy(entitiesPagedRequest());
let result = [...mockProducts];
const total = result.length;
const options = {
skip: entitiesPagedRequest()?.startIndex,
take: entitiesPagedRequest()?.size,
};
if (options?.skip || options?.take) {
const skip = +(options?.skip ?? 0);
const take = +(options?.take ?? 0);
result = result.slice(skip, skip + take);
}
return of({ entities: result, total });
},
}),
);

const store = new Store();
TestBed.flushEffects();
expect(store.entities()).toEqual([]);
store.setLoading();
jest.spyOn(store, 'setLoading');
tick();
// basic check for the first page
expect(store.entitiesCurrentPage().entities.length).toEqual(10);

// load a page not in cache
store.loadEntitiesPage({ pageIndex: 7, skipLoadingCall: true });
tick();
expect(fetchEntitiesSpy).not.toHaveBeenCalledWith({
startIndex: 70,
size: 30,
page: 7,
});
// now manually trigger load page
store.setLoading();
tick();
expect(fetchEntitiesSpy).toHaveBeenCalledWith({
startIndex: 70,
size: 30,
page: 7,
});
// check the page

expect(store.entitiesCurrentPage().entities.length).toEqual(10);
expect(store.entitiesCurrentPage().entities).toEqual(
mockProducts.slice(70, 80),
);
expect(store.entitiesCurrentPage().pageIndex).toEqual(7);
expect(store.entitiesCurrentPage().pageSize).toEqual(10);
expect(store.entitiesCurrentPage().pagesCount).toEqual(13);
expect(store.entitiesCurrentPage().total).toEqual(mockProducts.length);
expect(store.entitiesCurrentPage().hasPrevious).toEqual(true);
expect(store.entitiesCurrentPage().hasNext).toEqual(true);
});
}));

it('test when last page of cache gets loaded more pages are requested', fakeAsync(() => {
TestBed.runInInjectionContext(() => {
const fetchEntitiesSpy = jest.fn();
Expand Down
Loading

0 comments on commit 081f87d

Please sign in to comment.