Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(signals): add support for _ method names in withCalls #143

Merged
merged 1 commit into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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