Skip to content

Commit

Permalink
feat: added onSuccess and onError to withCalls and withEntitiesLoadin…
Browse files Browse the repository at this point in the history
…gCall
  • Loading branch information
Gabriel Guerrero authored and gabrielguerrero committed Apr 29, 2024
1 parent aa19aef commit 2c701bb
Show file tree
Hide file tree
Showing 14 changed files with 153 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,13 @@ import { ProductsLocalStore } from './product.store';
></mat-paginator>
</div>
<product-detail
[product]="store.productDetail()"
[productLoading]="store.isLoadProductDetailLoading()"
/>
@if (store.isLoadProductDetailLoading()) {
<mat-spinner />
} @else if (store.isLoadProductDetailLoaded()) {
<product-detail [product]="store.productDetail()!" />
} @else {
<div class="content-center"><h2>Please Select a product</h2></div>
}
</div>
}
</mat-card-content>
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.productDetail()!" />
} @else {
<div class="content-center"><h2>Please Select a product</h2></div>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export type CallConfig<
call: Call<Params, Result>;
resultProp?: PropName;
mapPipe?: 'switchMap' | 'concatMap' | 'exhaustMap';
onSuccess?: (result: Result) => void;
onError?: (error: any) => void;
};
export type ExtractCallResultType<T extends Call | CallConfig> =
T extends Call<any, infer R>
Expand Down
35 changes: 35 additions & 0 deletions libs/ngrx-traits/signals/src/lib/with-calls/with-calls.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,22 @@ import { withCalls } from '../index';

describe('withCalls', () => {
const apiResponse = new Subject<string>();
const onSuccess = jest.fn();
const onError = jest.fn();
const Store = signalStore(
withState({ foo: 'bar' }),
withCalls(() => ({
testCall: ({ ok }: { ok: boolean }) => {
return ok ? apiResponse : throwError(() => new Error('fail'));
},
testCall2: {
call: ({ ok }: { ok: boolean }) => {
return ok ? apiResponse : throwError(() => new Error('fail'));
},
resultProp: 'result',
onSuccess,
onError,
},
})),
);
it('Successful call should set status to loading and loaded ', async () => {
Expand All @@ -34,4 +44,29 @@ describe('withCalls', () => {
expect(store.testCallResult()).toBe(undefined);
});
});

describe('when using a CallConfig', () => {
it('Successful call should set status to loading and loaded ', async () => {
TestBed.runInInjectionContext(() => {
const store = new Store();
expect(store.isTestCall2Loading()).toBeFalsy();
store.testCall2({ ok: true });
expect(store.isTestCall2Loading()).toBeTruthy();
apiResponse.next('test');
expect(store.isTestCall2Loaded()).toBeTruthy();
expect(store.result()).toBe('test');
expect(onSuccess).toHaveBeenCalledWith('test');
});
});
it('Fail on a call should set status return error ', async () => {
TestBed.runInInjectionContext(() => {
const store = new Store();
expect(store.isTestCall2Loading()).toBeFalsy();
store.testCall2({ ok: false });
expect(store.testCall2Error()).toEqual(new Error('fail'));
expect(store.result()).toBe(undefined);
expect(onError).toHaveBeenCalledWith(new Error('fail'));
});
});
});
});
17 changes: 15 additions & 2 deletions libs/ngrx-traits/signals/src/lib/with-calls/with-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ import { getWithCallKeys } from './with-calls.util';
* call: ({ id }: { id: string }) =>
* inject(ProductService).getProductDetail(id),
* resultProp: 'productDetail',
* mapPipe: 'switchMap', // default is 'exhaustMap'
* onSuccess: (result) => {
* // do something with the result
* },
* onError: (error) => {
* // do something with the error
* },
* },
* checkout: () =>
* inject(OrderService).checkout({
Expand Down Expand Up @@ -102,7 +109,7 @@ export function withCalls<
? Calls[K]['resultProp'] extends string
? Calls[K]['resultProp']
: `${K & string}Result`
: `${K & string}Result`]: ExtractCallResultType<Calls[K]>;
: `${K & string}Result`]: ExtractCallResultType<Calls[K]> | undefined;
};
signals: NamedCallStatusComputed<keyof Calls & string>;
methods: {
Expand Down Expand Up @@ -216,12 +223,18 @@ export function withCalls<
[resultPropKey]: result,
});
setLoaded();
isCallConfig(call) &&
call.onSuccess &&
call.onSuccess(result);
}),
first(),
catchError((error: unknown) => {
setError(error);
isCallConfig(call) &&
call.onError &&
call.onError(error);
return of();
}),
first(),
);
});
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,13 @@ export function withEntitiesLocalFilter<
entity?: Entity;
collection?: Collection;
}): SignalStoreFeature<
// TODO: we have a problem with the state property, when set to string
// TODO: we have a problem with the state property, when set to any
// it works but is it has a Collection, some methods are not generated, it seems
// to only be accessible using store['filterEntities']
// the workaround doesn't cause any issues, because the signals prop does work and still
// gives the right error requiring withEntities to be used
{
state: NamedEntityState<Entity, string>;
state: NamedEntityState<Entity, any>;
signals: NamedEntitySignals<Entity, Collection>;
methods: {};
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export function withEntitiesRemoteFilter<
collection?: Collection;
}): SignalStoreFeature<
{
state: NamedEntityState<Entity, string>;
state: NamedEntityState<Entity, any>;
signals: NamedEntitySignals<Entity, Collection>;
methods: NamedCallStatusMethods<Collection>;
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { signalStore, type } from '@ngrx/signals';
import { withEntities } from '@ngrx/signals/entities';
import { of } from 'rxjs';
import { of, throwError } from 'rxjs';

import {
withCallStatus,
Expand Down Expand Up @@ -98,6 +98,65 @@ describe('withEntitiesLoadingCall', () => {
expect(store.entities()).toEqual(mockProducts.slice(0, 30));
});
}));

it('should call setLoaded and onSuccess if fetchEntities call is successful', fakeAsync(() => {
const onSuccess = jest.fn();
const onError = jest.fn();
TestBed.runInInjectionContext(() => {
const Store = signalStore(
withEntities({
entity,
}),
withCallStatus(),
withEntitiesLoadingCall({
fetchEntities: () => {
let result = [...mockProducts];
return of(result);
},
onSuccess,
onError,
}),
);
const store = new Store();
TestBed.flushEffects();
expect(store.entities()).toEqual([]);
store.setLoading();
tick();
expect(store.entities()).toEqual(mockProducts);
expect(store.isLoaded()).toBeTruthy();
expect(onSuccess).toHaveBeenCalledWith(mockProducts);
expect(onError).not.toHaveBeenCalled();
});
}));

it('should call setError and onError if fetchEntities call fails ', fakeAsync(() => {
const onSuccess = jest.fn();
const onError = jest.fn();
TestBed.runInInjectionContext(() => {
const Store = signalStore(
withEntities({
entity,
}),
withCallStatus(),
withEntitiesLoadingCall({
fetchEntities: () => {
return throwError(() => new Error('fail'));
},
onSuccess,
onError,
}),
);
const store = new Store();
TestBed.flushEffects();
expect(store.entities()).toEqual([]);
store.setLoading();
tick();
expect(store.entities()).toEqual([]);
expect(store.error()).toEqual(new Error('fail'));
expect(onError).toHaveBeenCalledWith(new Error('fail'));
expect(onSuccess).not.toHaveBeenCalled();
});
}));
});

describe('with collection set[Collection]Loading should call fetch entities', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ import { getWithEntitiesRemotePaginationKeys } from '../with-entities-pagination
* @param config - Configuration object
* @param config.fetchEntities - A function that fetches the entities from a remote source the return type
* @param config.collection - The collection name
* @param config.onSuccess - A function that is called when the fetchEntities is successful
* @param config.onError - A function that is called when the fetchEntities fails
* can be an array of entities or an object with entities and total
*
* @example
Expand Down Expand Up @@ -142,6 +144,12 @@ export function withEntitiesLoadingCall<
: Entity[] | { entities: Entity[] }
>;
mapPipe?: 'switchMap' | 'concatMap' | 'exhaustMap';
onSuccess?: (
result: Input['methods'] extends SetEntitiesResult<infer ResultParam>
? ResultParam
: Entity[] | { entities: Entity[] },
) => void;
onError?: (error: any) => void;
}): SignalStoreFeature<
Input & {
state: EntityState<Entity> & CallStatusState;
Expand All @@ -164,6 +172,8 @@ export function withEntitiesLoadingCall<
* @param config - Configuration object
* @param config.fetchEntities - A function that fetches the entities from a remote source the return type
* @param config.collection - The collection name
* @param config.onSuccess - A function that is called when the fetchEntities is successful
* @param config.onError - A function that is called when the fetchEntities fails
* can be an array of entities or an object with entities and total
*
* @example
Expand Down Expand Up @@ -248,6 +258,15 @@ export function withEntitiesLoadingCall<
: Entity[] | { entities: Entity[] }
>;
mapPipe?: 'switchMap' | 'concatMap' | 'exhaustMap';
onSuccess?: (
result: Input['methods'] extends NamedSetEntitiesResult<
Collection,
infer ResultParam
>
? ResultParam
: Entity[] | { entities: Entity[] },
) => void;
onError?: (error: any) => void;
}): SignalStoreFeature<
Input & {
state: NamedEntityState<Entity, Collection> &
Expand Down Expand Up @@ -276,6 +295,8 @@ export function withEntitiesLoadingCall<
Input['methods'],
) => Observable<any> | Promise<any>;
mapPipe?: 'switchMap' | 'concatMap' | 'exhaustMap';
onSuccess?: (result: any) => void;
onError?: (error: any) => void;
}): SignalStoreFeature<Input, EmptyFeatureResult> {
const { loadingKey, setErrorKey, setLoadedKey } = getWithCallStatusKeys({
prop: collection,
Expand Down Expand Up @@ -328,13 +349,14 @@ export function withEntitiesLoadingCall<
);
}
setLoaded();
if (config.onSuccess) config.onSuccess(result);
}),
first(),
catchError((error: unknown) => {
setError(error);
setLoaded();
if (config.onError) config.onError(error);
return of();
}),
first(),
),
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export function withEntitiesLocalPagination<
collection?: Collection;
}): SignalStoreFeature<
{
state: NamedEntityState<Entity, string>; // if put Collection the some props get lost and can only be access ['prop'] weird bug
state: NamedEntityState<Entity, any>; // if put Collection the some props get lost and can only be access ['prop'] weird bug
signals: NamedEntitySignals<Entity, Collection>;
methods: {};
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ export function withEntitiesRemotePagination<
collection?: Collection;
}): SignalStoreFeature<
{
state: NamedEntityState<Entity, string>; // if put Collection the some props get lost and can only be access ['prop'] weird bug
state: NamedEntityState<Entity, any>; // if put Collection the some props get lost and can only be access ['prop'] weird bug
signals: NamedEntitySignals<Entity, Collection> &
NamedCallStatusComputed<Collection>;
methods: NamedCallStatusMethods<Collection>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ export function withEntitiesRemoteScrollPagination<
pagesToCache?: number;
}): SignalStoreFeature<
{
state: NamedEntityState<Entity, string>; // if put Collection the some props get lost and can only be access ['prop'] weird bug
state: NamedEntityState<Entity, any>; // if put Collection the some props get lost and can only be access ['prop'] weird bug
signals: NamedEntitySignals<Entity, Collection> &
NamedCallStatusComputed<Collection>;
methods: NamedCallStatusMethods<Collection>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,13 @@ export function withEntitiesLocalSort<
entity?: Entity;
collection?: Collection;
}): SignalStoreFeature<
// TODO: we have a problem with the state property, when set to string
// TODO: we have a problem with the state property, when set to any
// it works but is it has a Collection, some methods are not generated, it seems
// to only be accessible using store['filterEntities']
// the workaround doesn't cause any issues, because the signals prop does work and still
// gives the right error requiring withEntities to be used
{
state: NamedEntityState<Entity, string>;
state: NamedEntityState<Entity, any>;
signals: NamedEntitySignals<Entity, Collection>;
methods: {};
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export function withEntitiesRemoteSort<
collection?: Collection;
}): SignalStoreFeature<
{
state: NamedEntityState<Entity, string>;
state: NamedEntityState<Entity, any>;
signals: NamedEntitySignals<Entity, Collection>;
methods: NamedCallStatusMethods<Collection>;
},
Expand Down

0 comments on commit 2c701bb

Please sign in to comment.