Skip to content

Commit

Permalink
feat(signals): withEntitiesLocalSort allow custom sort function
Browse files Browse the repository at this point in the history
Allow override the sortFunction used in the entities sort

fix #131
  • Loading branch information
Gabriel Guerrero authored and gabrielguerrero committed Sep 16, 2024
1 parent 880acef commit 0451e08
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 9 deletions.
1 change: 1 addition & 0 deletions libs/ngrx-traits/signals/api-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,7 @@ export const store = signalStore(
| configFactory.entity | <p>The type entity to be used</p> |
| configFactory.collection | <p>The name of the collection for which will be sorted</p> |
| configFactory.selectId | <p>The function to use to select the id of the entity</p> |
| configFactory.sortFunction | <p>Optional custom function use to sort the entities</p> |
**Example**
```js
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,28 @@ import {
withEntitiesLocalFilter,
withEntitiesLocalSort,
} from '../index';
import { mockProducts } from '../test.mocks';
import { mockProducts as mockProductsOld } from '../test.mocks';
import { Product } from '../test.model';
import { sortData } from './with-entities-local-sort.util';

type ProductWithReleaseDate = Product & { releaseDate: Date };

function addYears(date: Date, years: number) {
const newDate = new Date(date);
newDate.setFullYear(newDate.getFullYear() + years);
return newDate;
}
const today = new Date('2018/06/28');
const mockProducts = mockProductsOld.map(
(p, index) =>
({
...p,
releaseDate: addYears(today, index - 2),
}) as ProductWithReleaseDate,
);

describe('withEntitiesLocalSort', () => {
const entity = type<Product>();
const entity = type<ProductWithReleaseDate>();
it('should sort entities and store sort', () => {
const Store = signalStore(
{ protectedState: false },
Expand Down Expand Up @@ -63,7 +80,9 @@ describe('withEntitiesLocalSort', () => {
});

it('should sort entities with custom id', () => {
type ProductCustom = Omit<Product, 'id'> & { productId: string };
type ProductCustom = Omit<ProductWithReleaseDate, 'id'> & {
productId: string;
};
const config = entityConfig({
entity: type<ProductCustom>(),
selectId: (e) => e.productId,
Expand Down Expand Up @@ -163,7 +182,9 @@ describe('withEntitiesLocalSort', () => {
});

it('with collection should sort entities with custom id and store sort', () => {
type ProductCustom = Omit<Product, 'id'> & { productId: string };
type ProductCustom = Omit<ProductWithReleaseDate, 'id'> & {
productId: string;
};
const config = entityConfig({
entity: type<ProductCustom>(),
collection: 'products',
Expand Down Expand Up @@ -434,18 +455,21 @@ describe('withEntitiesLocalSort', () => {
id: '39',
description: 'Super Nintendo Game',
price: 88,
releaseDate: expect.any(Date),
},
{
name: "Yoshi's Cookie",
id: '20',
description: 'Super Nintendo Game',
price: 50,
releaseDate: expect.any(Date),
},
{
name: "Yoshi's Safari",
id: '15',
description: 'Super Nintendo Game',
price: 40,
releaseDate: expect.any(Date),
},
]);
});
Expand Down Expand Up @@ -484,20 +508,144 @@ describe('withEntitiesLocalSort', () => {
id: '39',
description: 'Super Nintendo Game',
price: 88,
releaseDate: expect.any(Date),
},
{
name: "Yoshi's Cookie",
id: '20',
description: 'Super Nintendo Game',
price: 50,
releaseDate: expect.any(Date),
},
{
name: "Yoshi's Safari",
id: '15',
description: 'Super Nintendo Game',
price: 40,
releaseDate: expect.any(Date),
},
]);
});
}));

it('should sort entities by release date', () => {
const Store = signalStore(
{ protectedState: false },
withEntities({
entity,
}),
withEntitiesLocalSort({
entity,
defaultSort: { field: 'releaseDate', direction: 'desc' },
}),
);
const store = new Store();
patchState(store, setAllEntities(mockProducts));
expect(store.entitiesSort()).toEqual({
field: 'releaseDate',
direction: 'desc',
});
// check default sort
store.sortEntities();
expect(
store
.entities()
.map((e) => e.releaseDate.toDateString())
.slice(0, 5),
).toEqual([
'Fri Jun 28 2137',
'Thu Jun 28 2136',
'Tue Jun 28 2135',
'Mon Jun 28 2134',
'Sun Jun 28 2133',
]);
// sort by price
store.sortEntities({
sort: { field: 'releaseDate', direction: 'asc' },
});
expect(
store
.entities()
.map((e) => e.releaseDate.toDateString())
.slice(0, 5),
).toEqual([
'Tue Jun 28 2016',
'Wed Jun 28 2017',
'Thu Jun 28 2018',
'Fri Jun 28 2019',
'Sun Jun 28 2020',
]);
expect(store.entities().length).toEqual(mockProducts.length);
expect(store.entitiesSort()).toEqual({
field: 'releaseDate',
direction: 'asc',
});
});

it('should sort entities by using custom sort function', () => {
const Store = signalStore(
{ protectedState: false },
withEntities({
entity,
}),
withEntitiesLocalSort({
entity,
defaultSort: { field: 'sortBySecondWordInName', direction: 'desc' },
sortFunction: (entities, sort) => {
if (sort.field === 'sortBySecondWordInName') {
return entities.sort((a, b) => {
const [aFirst, aSecond] = a.name.split(' ');
const [bFirst, bSecond] = b.name.split(' ');
return (
(aSecond || aFirst).localeCompare(bSecond || bFirst) *
(sort.direction === 'asc' ? 1 : -1)
);
});
}
return sortData(entities, sort);
},
}),
);
const store = new Store();
patchState(store, setAllEntities(mockProducts));
expect(store.entitiesSort()).toEqual({
field: 'sortBySecondWordInName',
direction: 'desc',
});
// check default sort
store.sortEntities();
expect(
store
.entities()
.map((e) => e.name)
.slice(0, 5),
).toEqual([
'Pokémon XD: Gale of Darkness',
'Wario World',
"Wario's Woods",
'Battalion Wars',
'Uniracers',
]);
// sort by price
store.sortEntities({
sort: { field: 'sortBySecondWordInName', direction: 'asc' },
});
expect(
store
.entities()
.map((e) => e.name)
.slice(0, 5),
).toEqual([
'Mario & Wario',
'Tetris & Dr. Mario',
'Tetris 2',
'Pikmin 2',
'Kirby Air Ride',
]);
expect(store.entities().length).toEqual(mockProducts.length);
expect(store.entitiesSort()).toEqual({
field: 'sortBySecondWordInName',
direction: 'asc',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
} from './with-entities-local-sort.util';
import { getWithEntitiesSortKeys } from './with-entities-sort.util';

export { sortData };
/**
* Generates necessary state, computed and methods for sorting locally entities in the store.
*
Expand All @@ -53,6 +54,7 @@ import { getWithEntitiesSortKeys } from './with-entities-sort.util';
* @param configFactory.entity - The type entity to be used
* @param configFactory.collection - The name of the collection for which will be sorted
* @param configFactory.selectId - The function to use to select the id of the entity
* @param configFactory.sortFunction - Optional custom function use to sort the entities
*
* @example
* const entity = type<Product>();
Expand Down Expand Up @@ -84,6 +86,7 @@ export function withEntitiesLocalSort<
entity: Entity;
collection?: Collection;
selectId?: SelectEntityId<Entity>;
sortFunction?: (entities: Entity[], sort: Sort<Entity>) => Entity[];
}
>,
): SignalStoreFeature<
Expand Down Expand Up @@ -128,11 +131,11 @@ export function withEntitiesLocalSort<
const sort = newSort ?? (state[sortKey]() as Sort<Entity>);
patchState(state as WritableStateSource<object>, {
[sortKey]: sort,
[idsKey]: sortData(state[entitiesKey]() as Entity[], sort).map(
(entity) =>
config.selectId
? config.selectId(entity)
: (entity as any).id,
[idsKey]: (config?.sortFunction
? config.sortFunction(state[entitiesKey]() as Entity[], sort)
: sortData(state[entitiesKey]() as Entity[], sort)
).map((entity) =>
config.selectId ? config.selectId(entity) : (entity as any).id,
),
});
broadcast(state, entitiesLocalSortChanged({ sort }));
Expand Down

0 comments on commit 0451e08

Please sign in to comment.