Skip to content

Commit

Permalink
feat(signals): allow idkey to withEntities* to have allow custom key …
Browse files Browse the repository at this point in the history
…prop in entities

Fix ##85
  • Loading branch information
Gabriel Guerrero authored and gabrielguerrero committed Jul 2, 2024
1 parent 711d1b7 commit 05e0424
Show file tree
Hide file tree
Showing 19 changed files with 1,067 additions and 446 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,34 +43,4 @@ export const ProductsBranchStore = signalStore(
},
})),
),
signalStoreFeature(
withEntities({
entity: entity2,
collection,
}),
withCallStatus({ initialValue: 'loading', collection }),
withEntitiesRemoteFilter({
entity: entity2,
collection,
defaultFilter: { search: '' },
}),
withEntitiesRemoteScrollPagination({
pageSize: 10,
entity: entity2,
collection,
}),
withEntitiesLoadingCall({
collection,
fetchEntities: async ({ productsPagedRequest, productsFilter }) => {
const res = await lastValueFrom(
inject(ProductService).getProducts({
search: productsFilter().search,
skip: productsPagedRequest().startIndex,
take: productsPagedRequest().size,
}),
);
return { entities: res.resultList };
},
}),
),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { inject } from '@angular/core';
import {
withCalls,
withCallStatus,
withEntitiesLoadingCall,
withEntitiesLocalFilter,
withEntitiesLocalPagination,
withEntitiesLocalSort,
withEntitiesSingleSelection,
} from '@ngrx-traits/signals';
import { signalStore, type } from '@ngrx/signals';
import { withEntities } from '@ngrx/signals/entities';
import { map } from 'rxjs/operators';

import { Product } from '../../models';
import { OrderService } from '../../services/order.service';
import { ProductService } from '../../services/product.service';

// Example of wihEntities with a custom id key
type ProductCustom = Omit<Product, 'id'> & { productId: string };
const entityConfig = {
entity: type<ProductCustom>(),
collection: 'products',
idKey: 'productId' as any,
} as const;

export const ProductsLocalStore = signalStore(
{ providedIn: 'root' },
withEntities(entityConfig),
withCallStatus({ ...entityConfig, initialValue: 'loading' }),
withEntitiesLocalPagination({
...entityConfig,
pageSize: 5,
}),
withEntitiesLocalFilter({
...entityConfig,
defaultFilter: { search: '' },
filterFn: (entity, filter) =>
!filter?.search ||
entity?.name.toLowerCase().includes(filter?.search.toLowerCase()),
}),
withEntitiesLocalSort({
...entityConfig,
defaultSort: { field: 'name', direction: 'asc' },
}),
withEntitiesSingleSelection({
...entityConfig,
}),
withEntitiesLoadingCall({
...entityConfig,
fetchEntities: ({ productsFilter }) => {
return inject(ProductService)
.getProducts({
search: productsFilter().search,
})
.pipe(
map((d) =>
d.resultList.map(({ id, ...product }) => ({
...product,
productId: id,
})),
),
);
},
}),
withCalls(() => ({
loadProductDetail: {
call: ({ id }: { id: string }) =>
inject(ProductService).getProductDetail(id),
resultProp: 'productDetail',
mapPipe: 'switchMap',
},
checkout: () => inject(OrderService).checkout(),
})),
);
63 changes: 63 additions & 0 deletions docs/signals.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,17 @@ export const ProductsLocalStore = signalStore(
// and method loadProductDetail({id})
);
```

### withCallStatus

In the example, we use the `withCallStatus` store feature, which adds computed signals like isLoading() and isLoaded() and corresponding setters setLoading and setLoading. You can see them being used in the withHooks to load the products.

### withEntitiesLocalPagination

You can also see in the example `withEntitiesLocalPagination`, which will add signal entitiesCurrentPage() and loadEntitiesPage({pageIndex: number}) that we can use to render a paginated list like the one below.

### withCalls

Finally `withCalls` adds the signals like isLoadProductDetailLoading(), isLoadProductDetailError() and loadProductDetailResult() and the method loadProductDetail({id}) that when called will change the status while the call is being made and store the result when it's done.

Now let's see how we can use them in a component.
Expand Down Expand Up @@ -199,6 +206,62 @@ Most store features support a collection param that allows you have custom names
})),
);
```
### withEntitiesLoadingCall
Now we can also replace that withHook with withEntitiesLoadingCall, which is similar to withCalls but is specialized on entities list,
it will call the fetchEntities, when the entities status it set to loading, and will handle the storing the result, status changes and errors if any for you.

```typescript
const entity = type<Product>();
const collection = "products";
export const ProductsLocalStore = signalStore(
withEntities({ entity, collection }),
withCallStatus({ collection, initialValue: "loading" }),
withEntitiesLocalPagination({ entity, collection, pageSize: 5 }),
// 👇 replaces withHook, will store entities result, change the status and handle errors
withEntitiesLoadingCall({
entity,
collection,
fetchEntities: () =>
inject(ProductService)
.getProducts()
.pipe((res) => res.resultList),
}),
withCalls(() => ({
loadProductDetail: ({ id }: { id: string }) =>
inject(ProductService).getProductDetail(id),
})),
);
```

### Custom ids
By default, the withEntities expect the Entity to have an id prop, but you can change that by passing a custom id like:
```typescript
const entityConfig = {
entity: type<Product>(),
collection: "products",
idKey: "productId",
} as const; // 👈 important to use as const otherwise collection and idKey type will be a string instead of a string literal
export const ProductsLocalStore = signalStore(
withEntities(entityConfig),
withCallStatus({ ...entityConfig, initialValue: "loading" }),
withEntitiesLocalPagination({ ...entityConfig, pageSize: 5 }),
withEntitiesLoadingCall({
...entityConfig,
fetchEntities: () =>
inject(ProductService)
.getProducts()
.pipe((res) => res.resultList),
}),
withCalls(() => ({
loadProductDetail: ({ id }: { id: string }) =>
inject(ProductService).getProductDetail(id),
})),
);
```
You create a entityConfig like shown above using as const, and the you need to spread it to all withEntities* that you are using

```typescript
To see a full list of the store features in the library with details and examples, check the [API](../libs/ngrx-traits/signals/api-docs.md) documentation.
Also, I recommend checking the example section, where you can see multiple use cases for the library. [Examples](../apps/example-app/src/app/examples/signals)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,26 @@ import { getWithCallStatusKeys } from './with-call-status.util';
* store.setUsersLoaded // () => void
* store.setUsersError // (error?: unknown) => void
*/
export function withCallStatus<Error = unknown>(config?: {
initialValue?: CallStatus;
errorType?: Error;
}): SignalStoreFeature<
export function withCallStatus<Prop extends string, Error = unknown>(
config:
| {
prop: Prop;
initialValue?: CallStatus;
errorType?: Error;
}
| {
collection: Prop;
initialValue?: CallStatus;
errorType?: Error;
},
): SignalStoreFeature<
{ state: {}; signals: {}; methods: {} },
{
state: CallStatusState;
signals: CallStatusComputed<Error>;
methods: CallStatusMethods<Error>;
state: NamedCallStatusState<Prop>;
signals: NamedCallStatusComputed<Prop, Error>;
methods: NamedCallStatusMethods<Prop, Error>;
}
>;

/**
* Generates necessary state, computed and methods for call progress status to the store
* @param config - Configuration object
Expand Down Expand Up @@ -89,26 +97,18 @@ export function withCallStatus<Error = unknown>(config?: {
* store.setUsersLoaded // () => void
* store.setUsersError // (error?: unknown) => void
*/
export function withCallStatus<Prop extends string, Error = unknown>(
config?:
| {
prop: Prop;
initialValue?: CallStatus;
errorType?: Error;
}
| {
collection: Prop;
initialValue?: CallStatus;
errorType?: Error;
},
): SignalStoreFeature<
export function withCallStatus<Error = unknown>(config?: {
initialValue?: CallStatus;
errorType?: Error;
}): SignalStoreFeature<
{ state: {}; signals: {}; methods: {} },
{
state: NamedCallStatusState<Prop>;
signals: NamedCallStatusComputed<Prop, Error>;
methods: NamedCallStatusMethods<Prop, Error>;
state: CallStatusState;
signals: CallStatusComputed<Error>;
methods: CallStatusMethods<Error>;
}
>;

export function withCallStatus<Prop extends string>({
prop,
initialValue = 'init',
Expand Down
Loading

0 comments on commit 05e0424

Please sign in to comment.