Skip to content

Commit

Permalink
feat(filter-trait): new isRemoteFiter prop
Browse files Browse the repository at this point in the history
New isRemoteFilter function allows to execute a local or remote search depending on the props
changed on the filter
  • Loading branch information
Gabriel Guerrero authored and gabrielguerrero committed Apr 22, 2022
1 parent 83511cb commit 6d9f526
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type FilterEntitiesConfig<T, F> = {
defaultFilter?: F;
filterFn?: (filter: F, entity: T) => boolean;
defaultDebounceTime?: number;
isRemoteFilter?: (previous: F | undefined, current: F | undefined) => boolean;
};

export type FilterEntitiesKeyedConfig<T, F> = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Injectable } from '@angular/core';
import { TraitEffect } from 'ngrx-traits';
import { asyncScheduler, EMPTY, of, timer } from 'rxjs';
import { TraitEffect, Type } from 'ngrx-traits';
import { asyncScheduler, EMPTY, of, pipe, timer } from 'rxjs';
import {
concatMap,
debounce,
distinctUntilChanged,
first,
map,
pairwise,
startWith,
} from 'rxjs/operators';
import { createEffect, ofType } from '@ngrx/effects';
import {
Expand All @@ -17,7 +19,6 @@ import {
LoadEntitiesActions,
LoadEntitiesSelectors,
} from '../load-entities/load-entities.model';
import { Type } from 'ngrx-traits';
import { ƟFilterEntitiesActions } from './filter-entities.model.internal';
import { EntitiesPaginationActions } from '../entities-pagination';

Expand Down Expand Up @@ -60,17 +61,44 @@ export function createFilterTraitEffects<Entity, F>(
JSON.stringify(previous?.filters) ===
JSON.stringify(current?.filters)
),
map((action) =>
allActions.storeEntitiesFilter({
filters: action?.filters,
patch: action?.patch,
})
)
traitConfig?.isRemoteFilter
? pipe(
startWith({
filters: traitConfig.defaultFilter as F,
patch: false,
}),
pairwise(),
concatMap(([previous, current]) =>
traitConfig?.isRemoteFilter!(
previous?.filters,
current?.filters
)
? [
allActions.storeEntitiesFilter({
filters: current?.filters,
patch: current?.patch,
}),
allActions.loadEntities(),
]
: [
allActions.storeEntitiesFilter({
filters: current?.filters,
patch: current?.patch,
}),
]
)
)
: map((action) =>
allActions.storeEntitiesFilter({
filters: action?.filters,
patch: action?.patch,
})
)
)
);

loadEntities$ =
!traitConfig?.filterFn &&
(!traitConfig?.filterFn || traitConfig?.isRemoteFilter) &&
createEffect(() => {
return this.actions$.pipe(
ofType(allActions['storeEntitiesFilter']),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,38 @@ describe('addFilter Trait', () => {
return { ...traits, effects: TestBed.inject(traits.effects[0]), mockStore };
}

function initWithIsLocalOrRemoteFilter(
initialState?: any,
filter?: TodoFilter
) {
const featureSelector = createFeatureSelector<TestState2>('test');
const traits = createEntityFeatureFactory(
{ entityName: 'entity', entitiesName: 'entities' },
addLoadEntitiesTrait<Todo>(),
addFilterEntitiesTrait<Todo, TodoFilter>({
defaultFilter: filter,
filterFn: (filter: TodoFilter, todo: Todo) =>
(filter?.content && todo.content?.includes(filter.content)) || false,
isRemoteFilter: (previous, current) =>
previous?.extra !== current?.extra,
})
)({
actionsGroupKey: 'test',
featureSelector: featureSelector,
});
TestBed.configureTestingModule({
providers: [
traits.effects[0],
provideMockActions(() => actions$),
provideMockStore({
initialState,
}),
],
});
const mockStore = TestBed.inject(MockStore);
return { ...traits, effects: TestBed.inject(traits.effects[0]), mockStore };
}

function initWithRemoteFilterWithPagination() {
const featureSelector = createFeatureSelector<TestState>('test');
const traits = createEntityFeatureFactory(
Expand All @@ -72,7 +104,7 @@ describe('addFilter Trait', () => {
return { ...traits, effects: TestBed.inject(traits.effects[1]) };
}

// note: local filtering test are in load-entities and pagination traits because most logic belongs there
// NOTE: local filtering test are in load-entities and pagination traits because most logic belongs there

describe('reducer', () => {
it('should storeFilter action should store filters', () => {
Expand Down Expand Up @@ -139,6 +171,7 @@ describe('addFilter Trait', () => {
actions.loadEntitiesFirstPage(),
]);
});

describe('storeFilter$', () => {
it('should fire immediately storeFilter action after filter if forceLoad is true', async () => {
const { effects, actions, mockStore, selectors } =
Expand Down Expand Up @@ -186,6 +219,66 @@ describe('addFilter Trait', () => {
).toBeObservable(expected);
});

it('should fire storeFilter and loadEntities if isRemote is defined and returns true', () => {
const { effects, actions, mockStore, selectors } =
initWithIsLocalOrRemoteFilter();
mockStore.overrideSelector(selectors.selectEntitiesFilter, {});
actions$ = hot('a', {
a: actions.filterEntities({
filters: { content: 'x', extra: 'new' },
}),
});
const expected = hot('---(ab)', {
a: (
actions as unknown as ƟFilterEntitiesActions<TodoFilter>
).storeEntitiesFilter({
filters: { content: 'x', extra: 'new' },
}),
b: actions.loadEntities(),
});
expect(
effects.storeFilter$({
debounce: 30,
scheduler: Scheduler.get(),
})
).toBeObservable(expected);
});
it('should fire storeFilter and not loadEntities if isRemote is defined and returns false', () => {
const { effects, actions, mockStore, selectors } =
initWithIsLocalOrRemoteFilter();
// mockStore.overrideSelector(selectors.selectEntitiesFilter, {
// content: 'y',
// extra: 'new',
// });
actions$ = hot('a-----b', {
a: actions.filterEntities({
filters: { content: 'x', extra: 'new' },
}),
b: actions.filterEntities({
filters: { content: 'y', extra: 'new' },
}),
});
const expected = hot('---(ab)--c', {
a: (
actions as unknown as ƟFilterEntitiesActions<TodoFilter>
).storeEntitiesFilter({
filters: { content: 'x', extra: 'new' },
}),
b: actions.loadEntities(),
c: (
actions as unknown as ƟFilterEntitiesActions<TodoFilter>
).storeEntitiesFilter({
filters: { content: 'y', extra: 'new' },
}),
});
expect(
effects.storeFilter$({
debounce: 30,
scheduler: Scheduler.get(),
})
).toBeObservable(expected);
});

it('should not fire storeFilter action after filter if payload is the same as before', () => {
const { effects, actions, mockStore, selectors } =
initWithRemoteFilter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ import {
* @param traitConfig - Config object fot the trait factory
* @param traitConfig.defaultFilter - Initial value for the filter
* @param traitConfig.filterFn - Function to filter entities in memory, if not present then its expected
* is filtered by the backend
* is filtered by the backend unless isRemoteFilter is defned
* @param traitConfig.defaultDebounceTime - Value in milliseconds. Default to 400ms
* @param traitConfig.isRemoteFilter - Function to when it returns true it fires loadEntities so a remote
* backend filtering can run, otherwise it uses filterFn to do a local filtering
*
* @example
* // The following trait config
Expand All @@ -50,9 +52,12 @@ import {
*
* const traits = createEntityFeatureFactory(
* addLoadEntitiesTrait<Todo>(),
* //addFilterEntitiesTrait<Todo,TodoFilter>() // remote filtering
* //addFilterEntitiesTrait<Todo,TodoFilter>() // no params uses remote filtering
* addFilterEntitiesTrait<Todo,TodoFilter>({filterFn: (filter, entity) => // local filtering
* filter?.content && entity.content?.includes(filter?.content) || false})// remote
* filter?.content && entity.content?.includes(filter?.content) || false})
* // or use the following function to switch between remote search and local
* // depending on which properties have changed in the filter
* // isRemoteFilter: (previous, current) => previous?.someRemoteParam !== current?.someRemoteParam,
* )({
* actionsGroupKey: '[Todos]',
* featureSelector: createFeatureSelector<TestState>>(
Expand All @@ -67,6 +72,7 @@ export function addFilterEntitiesTrait<Entity, F>({
defaultDebounceTime = 400,
defaultFilter,
filterFn,
isRemoteFilter,
}: FilterEntitiesConfig<Entity, F> = {}) {
return createTraitFactory({
key: filterEntitiesTraitKey,
Expand All @@ -75,6 +81,7 @@ export function addFilterEntitiesTrait<Entity, F>({
defaultDebounceTime,
defaultFilter,
filterFn,
isRemoteFilter,
} as FilterEntitiesConfig<Entity, F>,
actions: ({ actionsGroupKey, entitiesName }: TraitActionsFactoryConfig) =>
createFilterTraitActions<F>(actionsGroupKey, entitiesName),
Expand Down

0 comments on commit 6d9f526

Please sign in to comment.