Skip to content

Commit

Permalink
feat(signals): add support for _ method names in withCalls
Browse files Browse the repository at this point in the history
Add support for private methods (methods with _ in the start of the name) to withCalls

fix #142
  • Loading branch information
Gabriel Guerrero authored and gabrielguerrero committed Sep 27, 2024
1 parent b79659c commit 0fa2efe
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
import { capitalize } from '../util';

export function getWithCallStatusKeys(config?: { prop?: string }) {
const prop = config?.prop;
export function getWithCallStatusKeys(config?: {
prop?: string;
supportPrivate?: boolean;
}) {
let prop = config?.prop;
let prefix = '';

if (config?.supportPrivate && prop?.startsWith('_')) {
prop = prop.slice(1);
prefix = '_';
}

const capitalizedProp = prop && capitalize(prop);
return {
callStatusKey: prop ? `${config.prop}CallStatus` : 'callStatus',
loadingKey: prop ? `is${capitalizedProp}Loading` : 'isLoading',
loadedKey: prop ? `is${capitalizedProp}Loaded` : 'isLoaded',
errorKey: prop ? `${config.prop}Error` : 'error',
setLoadingKey: prop ? `set${capitalizedProp}Loading` : 'setLoading',
setLoadedKey: prop ? `set${capitalizedProp}Loaded` : 'setLoaded',
setErrorKey: prop ? `set${capitalizedProp}Error` : 'setError',
callStatusKey: prop ? `${prefix}${prop}CallStatus` : `${prefix}callStatus`,
loadingKey: prop
? `${prefix}is${capitalizedProp}Loading`
: `${prefix}isLoading`,
loadedKey: prop
? `${prefix}is${capitalizedProp}Loaded`
: `${prefix}isLoaded`,
errorKey: prop ? `${prefix}${prop}Error` : `${prefix}error`,
setLoadingKey: prop
? `${prefix}set${capitalizedProp}Loading`
: `${prefix}setLoading`,
setLoadedKey: prop
? `${prefix}set${capitalizedProp}Loaded`
: `${prefix}setLoaded`,
setErrorKey: prop
? `${prefix}set${capitalizedProp}Error`
: `${prefix}setError`,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,13 @@ export type ExtractCallResultType<T extends Call | CallConfig> =
: never;

export type NamedCallsStatusComputed<Prop extends string> = {
[K in Prop as `is${Capitalize<string & K>}Loading`]: Signal<boolean>;
[K in Prop as K extends `_${infer J}`
? `_is${Capitalize<string & J>}Loading`
: `is${Capitalize<string & K>}Loading`]: Signal<boolean>;
} & {
[K in Prop as `is${Capitalize<string & K>}Loaded`]: Signal<boolean>;
[K in Prop as K extends `_${infer J}`
? `_is${Capitalize<string & J>}Loaded`
: `is${Capitalize<string & K>}Loaded`]: Signal<boolean>;
};
export type NamedCallsStatusErrorComputed<
Calls extends Record<string, Call | CallConfig>,
Expand Down
87 changes: 85 additions & 2 deletions libs/ngrx-traits/signals/src/lib/with-calls/with-calls.spec.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import { signal } from '@angular/core';
import { computed, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { patchState, signalStore, withState } from '@ngrx/signals';
import {
patchState,
signalStore,
withComputed,
withMethods,
withState,
} from '@ngrx/signals';
import { BehaviorSubject, of, Subject, tap, throwError } from 'rxjs';

import { typedCallConfig, withCalls } from '../index';

describe('withCalls', () => {
let apiResponse = new Subject<string>();
let privateApiResponse = new Subject<string>();
const onSuccess = jest.fn();
const onError = jest.fn();
const Store = signalStore(
{ protectedState: false },
withState({ foo: 'bar' }),
withCalls(() => ({
testCall: ({ ok }: { ok: boolean }) => {
return ok ? apiResponse : throwError(() => new Error('fail'));
},
_testCall: ({ ok }: { ok: boolean }) => {
return ok ? privateApiResponse : throwError(() => new Error('fail'));
},
testCall2: {
call: ({ ok }: { ok: boolean }) => {
return ok ? apiResponse : throwError(() => new Error('fail'));
Expand All @@ -23,6 +34,28 @@ describe('withCalls', () => {
onSuccess,
onError,
},
_testCall2: typedCallConfig({
call: ({ ok }: { ok: boolean }) => {
return ok ? privateApiResponse : throwError(() => new Error('fail'));
},
resultProp: '_result',
onSuccess,
onError,
}),
})),
withComputed((store) => ({
privateIsTestCallLoading: computed(() => store._isTestCallLoading()),
privateIsTestCallLoaded: computed(() => store._isTestCallLoaded()),
privateTestCallResult: computed(() => store._testCallResult()),
privateTestCallError: computed(() => store._testCallError()),
privateIsTestCall2Loading: computed(() => store._isTestCall2Loading()),
privateIsTestCall2Loaded: computed(() => store._isTestCall2Loaded()),
privateResult: computed(() => store._result()),
privateTestCall2Error: computed(() => store._testCall2Error()),
})),
withMethods((store) => ({
privateTestCall: ({ ok }: { ok: boolean }) => store._testCall({ ok }),
privateTestCall2: ({ ok }: { ok: boolean }) => store._testCall2({ ok }),
})),
);

Expand Down Expand Up @@ -667,4 +700,54 @@ describe('withCalls', () => {
});
});
});

describe('when using private name', () => {
it('Successful call should set status to loading and loaded ', async () => {
TestBed.runInInjectionContext(() => {
const store = new Store();
expect(store.privateIsTestCallLoading()).toBeFalsy();
store.privateTestCall({ ok: true });
expect(store.privateIsTestCallLoading()).toBeTruthy();
privateApiResponse.next('test');
expect(store.privateIsTestCallLoaded()).toBeTruthy();
expect(store.privateTestCallResult()).toBe('test');
});
});
it('Fail on a call should set status return error ', async () => {
TestBed.runInInjectionContext(() => {
const store = new Store();
expect(store.privateIsTestCallLoading()).toBeFalsy();
store.privateTestCall({ ok: false });
expect(store.privateTestCallError()).toEqual(new Error('fail'));
expect(store.privateTestCallResult()).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.privateIsTestCall2Loading()).toBeFalsy();
store.privateTestCall2({ ok: true });
expect(store.privateIsTestCall2Loading()).toBeTruthy();
privateApiResponse.next('test');
expect(store.privateIsTestCall2Loaded()).toBeTruthy();
expect(store.privateResult()).toBe('test');
expect(onSuccess).toHaveBeenCalledWith('test', { ok: true });
});
});
it('Fail on a call should set status return error ', async () => {
TestBed.runInInjectionContext(() => {
const store = new Store();
expect(store.privateIsTestCall2Loading()).toBeFalsy();
store.privateTestCall2({ ok: false });
expect(store.privateTestCall2Error()).toEqual(new Error('fail'));
expect(store.privateResult()).toBe(undefined);
expect(onError).toHaveBeenCalledWith(new Error('fail'), {
ok: false,
});
});
});
});
});
});
4 changes: 3 additions & 1 deletion libs/ngrx-traits/signals/src/lib/with-calls/with-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ import { getWithCallKeys } from './with-calls.util';
* or a Signal or Observable of the same type as the original parameters.
* The original call can only have zero or one parameter, use an object with multiple
* props as first param if you need more.
* If the name start with an underscore, the call will be private and all generated methods
* will also start with an underscore, making it only accessible inside the store.
* @param {callsFactory} callsFactory - a factory function that receives the store and returns an object of type {Record<string, Call | CallConfig>} with the calls to be made
*
* @example
Expand Down Expand Up @@ -182,7 +184,7 @@ export function withCalls<
const callsComputed = Object.keys(calls).reduce(
(acc, callName) => {
const { loadingKey, loadedKey, errorKey, callStatusKey } =
getWithCallStatusKeys({ prop: callName });
getWithCallStatusKeys({ prop: callName, supportPrivate: true });
const callState = state[callStatusKey] as Signal<CallStatus>;
acc[loadingKey] = computed(() => callState() === 'loading');
acc[loadedKey] = computed(() => callState() === 'loaded');
Expand Down

0 comments on commit 0fa2efe

Please sign in to comment.