Skip to content

Commit

Permalink
feat: new filterState for withLogger and withSyncToWebStorage
Browse files Browse the repository at this point in the history
new filterState for withLogger and withSyncToWebStorage, and fixed some examples
  • Loading branch information
Gabriel Guerrero authored and gabrielguerrero committed Apr 29, 2024
1 parent d5d05d7 commit 29d8f98
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ import { rebuildFormArray } from '../../utils/form-utils';
</td>
</ng-container>
<ng-container matColumnDef="total">
<th mat-header-cell mat-sort-header *matHeaderCellDef>Total</th>
<th mat-header-cell *matHeaderCellDef>Total</th>
<td mat-cell *matCellDef="let row">
{{ row.price * (row.quantity || 1) | currency }}
</td>
Expand Down Expand Up @@ -145,7 +145,6 @@ import { rebuildFormArray } from '../../utils/form-utils';
})
export class ProductBasketComponent implements OnDestroy {
controls = new UntypedFormArray([]);
_list: ProductOrder[] = [];
destroy$ = new Subject<void>();
displayedColumns: (keyof ProductOrder | 'select' | 'total')[] = [
'select',
Expand All @@ -169,9 +168,10 @@ export class ProductBasketComponent implements OnDestroy {
});
rowControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(({ quantity }) =>
this.updateProduct.emit({ ...this._list[index], quantity }),
);
.subscribe(({ quantity }) => {
console.log('updateProduct', { ...this.list()[index], quantity });
this.updateProduct.emit({ ...this.list()[index], quantity });
});
return rowControl;
},
values,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import { ProductsShopStore } from '../../products-shop.store';
@if (store.isLoadProductDetailLoading()) {
<mat-spinner />
} @else if (store.isLoadProductDetailLoaded()) {
<product-detail [product]="store.productDetail()!" />
<product-detail [product]="store.loadProductDetailResult()!" />
} @else {
<div class="content-center"><h2>Please Select a product</h2></div>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { inject } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import {
withCalls,
withCallStatus,
withEntitiesLoadingCall,
withEntitiesLocalFilter,
withEntitiesLocalPagination,
withEntitiesLocalSort,
withEntitiesMultiSelection,
withEntitiesRemoteFilter,
withEntitiesRemotePagination,
withEntitiesRemoteScrollPagination,
withEntitiesRemoteSort,
withEntitiesSingleSelection,
withLogger,
} from '@ngrx-traits/signals';
import {
patchState,
Expand Down Expand Up @@ -80,7 +80,6 @@ const orderItemsStoreFeature = signalStoreFeature(
entity: orderEntity,
collection: orderItemsCollection,
}),
withCallStatus({ initialValue: 'loading', collection: orderItemsCollection }),
withEntitiesLocalSort({
entity: orderEntity,
collection: orderItemsCollection,
Expand All @@ -90,15 +89,15 @@ const orderItemsStoreFeature = signalStoreFeature(
entity: orderEntity,
collection: orderItemsCollection,
}),
withEntitiesSingleSelection({
entity: orderEntity,
collection: orderItemsCollection,
}),
withEntitiesLocalPagination({
pageSize: 10,
entity: orderEntity,
collection: orderItemsCollection,
}),
withLogger({
name: 'orderItemsStore',
filterState: ({ orderItemsEntityMap }) => ({ orderItemsEntityMap }),
}),
);

export const ProductsShopStore = signalStore(
Expand All @@ -107,32 +106,41 @@ export const ProductsShopStore = signalStore(
orderItemsStoreFeature,
withEntitiesLoadingCall({
collection: productsCollection,
fetchEntities: async ({ productsPagedRequest, productsFilter }) => {
fetchEntities: async ({
productsPagedRequest,
productsFilter,
productsSort,
}) => {
const res = await lastValueFrom(
inject(ProductService).getProducts({
search: productsFilter().search,
skip: productsPagedRequest().startIndex,
take: productsPagedRequest().size,
sortAscending: productsSort().direction === 'asc',
sortColumn: productsSort().field,
}),
);
return { entities: res.resultList, total: res.total };
},
}),
withCalls(({ orderItemsEntities }) => ({
loadProductDetail: {
call: ({ id }: { id: string }) =>
inject(ProductService).getProductDetail(id),
resultProp: 'productDetail',
mapPipe: 'switchMap',
withCalls(({ orderItemsEntities }, snackBar = inject(MatSnackBar)) => ({
loadProductDetail: ({ id }: { id: string }) =>
inject(ProductService).getProductDetail(id),
checkout: {
call: () =>
inject(OrderService).checkout(
...orderItemsEntities().map((p) => ({
productId: p.id,
quantity: p.quantity!,
})),
),
resultProp: 'orderNumber',
onSuccess: (orderId) => {
snackBar.open(`Order number: ${orderId}`, 'Close', {
duration: 5000,
});
},
},

checkout: () =>
inject(OrderService).checkout(
...orderItemsEntities().map((p) => ({
productId: p.id,
quantity: p.quantity!,
})),
),
})),
withMethods(
({ productsEntitySelected, orderItemsIdsSelected, ...state }) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,58 +30,27 @@ import { RouterLink } from '@angular/router';
</div>
</mat-list-item>
<mat-divider></mat-divider>
<!-- <mat-list-item [routerLink]="'product-list'">-->
<!-- <div matListItemTitle><b>Simple List</b></div>-->
<!-- <div matListItemLine>-->
<!-- Example using trait to load a product list with filtering and-->
<!-- sorting in memory-->
<!-- </div>-->
<!-- </mat-list-item>-->
<!-- <mat-divider></mat-divider>-->
<mat-list-item [routerLink]="'product-list-paginated'">
<div matListItemTitle><b>Paginated List</b></div>
<div matListItemLine>
Example using store features to load a product list with remote
filtering and detail view
</div>
</mat-list-item>
<!-- <mat-divider></mat-divider>-->
<!-- <mat-list-item [routerLink]="'product-picker'">-->
<!-- <div matListItemTitle>-->
<!-- <b>Local store example with a product picker</b>-->
<!-- </div>-->
<!-- <div matListItemLine>-->
<!-- Example using local traits to load a product list with filtering-->
<!-- and sorting in memory-->
<!-- </div>-->
<!-- </mat-list-item>-->
<!-- <mat-divider></mat-divider>-->
<mat-list-item [routerLink]="'products-shop'" style="height: 90px;">
<div matListItemTitle>
<b>Using addCrudEntities and creating loadProduct custom trait</b>
<b
>Complex example using multi collections and most of the store
features</b
>
</div>
<div matListItemLine style="white-space: normal">
This is a more complex example were we add a product basket, so
you can buy more than one product at a time. Here you will see how
to use the addCrudEntities and we create a custom trait called
loadProduct to help with the preview of the product
Example using two collection in the store, one for products and
one for a product basket, you can find here examples of withCalls,
remote pagination,sorting and filtering , local sorting,
pagination, single and multi selection and more
</div>
</mat-list-item>
<!-- <mat-divider></mat-divider>-->
<!-- <mat-list-item-->
<!-- [routerLink]="'cache-and-dropdowns'"-->
<!-- style="height: 110px;"-->
<!-- >-->
<!-- <div matListItemTitle>-->
<!-- <b>Example using local traits in dropdowns with cache</b>-->
<!-- </div>-->
<!-- <div matListItemLine style="white-space: normal">-->
<!-- Here you can see how to use local store in two dropdowns where-->
<!-- selecting one trigger a load in the second, plus how you can use-->
<!-- cache to reduce the number of calls they do-->
<!-- </div>-->
<!-- </mat-list-item>-->
</mat-list>
</mat-card-content>
</mat-card>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,7 @@ import { getWithEntitiesRemotePaginationKeys } from '../with-entities-pagination
export function withEntitiesLoadingCall<
Input extends SignalStoreFeatureResult,
Entity extends { id: string | number },
>({
fetchEntities,
}: {
>(config: {
fetchEntities: (
store: Prettify<
SignalStoreSlices<Input['state']> &
Expand Down Expand Up @@ -228,9 +226,7 @@ export function withEntitiesLoadingCall<
Input extends SignalStoreFeatureResult,
Entity extends { id: string | number },
Collection extends string,
>({
fetchEntities,
}: {
>(config: {
// entity?: Entity; // is this needed? entity can come from the method fetchEntities return type
collection: Collection;
fetchEntities: (
Expand Down
24 changes: 21 additions & 3 deletions libs/ngrx-traits/signals/src/lib/with-logger/with-logger.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
import { effect } from '@angular/core';
import { getState, signalStoreFeature, withHooks } from '@ngrx/signals';
import {
getState,
SignalStoreFeature,
signalStoreFeature,
withHooks,
} from '@ngrx/signals';
import type {
EmptyFeatureResult,
SignalStoreFeatureResult,
} from '@ngrx/signals/src/signal-store-models';

/**
* Log the state of the store on every change
* @param name - The name of the store to log
* @param filterState - filter the state before logging
*/
export function withLogger(name: string) {
export function withLogger<Input extends SignalStoreFeatureResult>({
name,
filterState,
}: {
name: string;
filterState?: (state: Input['state']) => Partial<Input['state']>;
}): SignalStoreFeature<Input, EmptyFeatureResult> {
return signalStoreFeature(
withHooks({
onInit(store) {
effect(() => {
const state = getState(store);
const state = filterState
? filterState(getState(store))
: getState(store);
console.log(`${name} state changed`, state);
});
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,41 @@ describe('withSyncToWebStorage', () => {
});
});

it('should save and load to local session using filtered state and onRestore be called', () => {
const onRestore = jest.fn();
TestBed.runInInjectionContext(() => {
const Store = signalStore(
withEntities({ entity }),
withCallStatus(),
withSyncToWebStorage({
key: 'test',
type: 'session',
restoreOnInit: false,
saveStateChangesAfterMs: 0,
filterState: (state) => ({
ids: state.ids,
entityMap: state.entityMap,
}),
onRestore,
}),
);
const store = new Store();
store.clearFromStore();
TestBed.flushEffects();
store.setLoaded();
patchState(store, setAllEntities(mockProducts));
store.saveToStorage();

store.setLoading();
patchState(store, setAllEntities(mockProducts.slice(0, 30)));

store.loadFromStorage();
expect(store.entities()).toEqual(mockProducts);
expect(store.isLoading()).toBe(true); // it keeps the current value because it was filtered
expect(onRestore).toBeCalled();
});
});

it('should save after milliseconds set in saveStateChangesAfterMs if is greater than 0 ', fakeAsync(() => {
TestBed.runInInjectionContext(() => {
const Store = signalStore(
Expand Down
Loading

0 comments on commit 29d8f98

Please sign in to comment.