Skip to content
Open
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
41 changes: 41 additions & 0 deletions libs/ngxtension/inject-params/src/inject-params.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,42 @@ describe(injectParams.name, () => {
expect(instance.name()).toEqual('john doe');
});

it('should throw error when required param is missing', async () => {
TestBed.configureTestingModule({
providers: [
provideRouter([
{ path: 'required', component: RequiredParamComponent },
]),
],
});

const harness = await RouterTestingHarness.create();

await expect(
harness.navigateByUrl('/required', RequiredParamComponent),
).rejects.toThrow(
'[ngxtension:injectParams] Parameter id is required but was not provided.',
);
});

it('should not throw error when required param is present', async () => {
TestBed.configureTestingModule({
providers: [
provideRouter([
{ path: 'required/:id', component: RequiredParamComponent },
]),
],
});

const harness = await RouterTestingHarness.create();

const instance = await harness.navigateByUrl(
'/required/123',
RequiredParamComponent,
);
expect(instance.requiredId()).toBe('123');
});

describe('global option', () => {
it('should get params from current route when global is false', async () => {
TestBed.configureTestingModule({
Expand Down Expand Up @@ -636,6 +672,11 @@ export class SpecialCharsComponent {
name = injectParams('name');
}

@Component({ template: `` })
export class RequiredParamComponent {
requiredId = injectParams('id', { required: true });
}

@Component({
template: `
<router-outlet />
Expand Down
40 changes: 38 additions & 2 deletions libs/ngxtension/inject-params/src/inject-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@ import {
DefaultValueOptions,
InjectorOptions,
ParseOptions,
RequiredOptions,
} from 'ngxtension/shared';
import { map, switchMap, type Observable } from 'rxjs';

type ParamsTransformFn<ReadT> = (params: Params) => ReadT;

function missingRequiredParamError(key: string | undefined): Error {
return new Error(
`[ngxtension:injectParams] Parameter ${key} is required but was not provided.`,
);
}

/**
* Merges all params from the route hierarchy by walking from root to the given route.
* Child route params override parent route params if there are naming conflicts.
Expand Down Expand Up @@ -48,7 +55,8 @@ export type ParamsOptions<ReadT, WriteT, DefaultValueT> = ParseOptions<
WriteT
> &
DefaultValueOptions<DefaultValueT> &
InjectorOptions;
InjectorOptions &
RequiredOptions;

/**
* Internal helper function that implements the core logic for injectParams.
Expand All @@ -60,7 +68,7 @@ function injectParamsCore<T>(
keyOrParamsTransform?: string | ((params: Params) => T),
options: ParamsOptions<T, string, T> = {},
): Signal<T | Params | string | null> {
const { parse, defaultValue } = options;
const { parse, defaultValue, required } = options;

if (!keyOrParamsTransform) {
return toSignal(paramsObservable, { initialValue: initialParams });
Expand All @@ -78,6 +86,11 @@ function injectParamsCore<T>(
| undefined;

if (!param) {
if (required) {
throw missingRequiredParamError(
keyOrParamsTransform as string | undefined,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In this context, keyOrParamsTransform is guaranteed to be a string because the preceding if conditions handle undefined and function types. The as string | undefined assertion is therefore redundant and can be simplified to keyOrParamsTransform.

Suggested change
keyOrParamsTransform as string | undefined,
keyOrParamsTransform,

);
}
return defaultValue ?? null;
}

Expand All @@ -104,6 +117,29 @@ interface InjectParamsBase {
*/
(key: string): Signal<string | null>;

/**
* @param {string} key - The name of the parameter to retrieve.
* @param {ParamsOptions} options - Configuration options with required flag that ensures a non-null return.
* @returns {Signal} A `Signal` that emits the value of the specified parameter. Throws an error if the parameter is not present.
*/
<ReadT = string>(
key: string,
options: ParamsOptions<ReadT, string, ReadT> & { required: true },
): Signal<ReadT>;

/**
* @param {string} key - The name of the parameter to retrieve.
* @param {ParamsOptions} options - Configuration options with both parse and required that ensures a non-null return.
* @returns {Signal} A `Signal` that emits the parsed value. Throws an error if the parameter is not present.
*/
<ReadT>(
key: string,
options: ParamsOptions<ReadT, string, never> & {
parse: (v: string) => ReadT;
required: true;
},
): Signal<ReadT>;

/**
* @param {string} key - The name of the parameter to retrieve.
* @param {ParamsOptions} options - Configuration options with both parse and defaultValue that ensures a non-null return.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ export class SearchComponent {
}
}

@Component({
standalone: true,
template: ``,
})
export class RequiredQueryParamComponent {
requiredId = injectQueryParams('id', { required: true });
}

@Component({
standalone: true,
template: ``,
})
export class RequiredArrayQueryParamComponent {
requiredIdArray = injectQueryParams.array('id', { required: true });
}

describe(injectQueryParams.name, () => {
beforeEach(async () => {
TestBed.configureTestingModule({
Expand Down Expand Up @@ -170,6 +186,42 @@ describe(injectQueryParams.name, () => {
expect(instance.searchParamDefault()).toEqual('Angular');
expect(instance.paramKeysList()).toEqual(['query']);
});

it('should throw error when required query param is missing', async () => {
TestBed.configureTestingModule({
providers: [
provideRouter([
{ path: 'required', component: RequiredQueryParamComponent },
]),
],
});

const harness = await RouterTestingHarness.create();

await expect(
harness.navigateByUrl('/required', RequiredQueryParamComponent),
).rejects.toThrow(
'[ngxtension:injectQueryParams] Query parameter id is required but was not provided.',
);
});

it('should not throw error when required query param is present', async () => {
TestBed.configureTestingModule({
providers: [
provideRouter([
{ path: 'required', component: RequiredQueryParamComponent },
]),
],
});

const harness = await RouterTestingHarness.create();

const instance = await harness.navigateByUrl(
'/required?id=123',
RequiredQueryParamComponent,
);
expect(instance.requiredId()).toBe('123');
});
});

describe(injectQueryParams.array.name, () => {
Expand Down Expand Up @@ -340,4 +392,40 @@ describe(injectQueryParams.array.name, () => {
expect(instance.queryParams()).toEqual({ query: ['Angular', 'React'] });
expect(instance.searchParams()).toEqual(['Angular', 'React']);
});

it('should throw error when required array query param is missing', async () => {
TestBed.configureTestingModule({
providers: [
provideRouter([
{ path: 'required', component: RequiredArrayQueryParamComponent },
]),
],
});

const harness = await RouterTestingHarness.create();

await expect(
harness.navigateByUrl('/required', RequiredArrayQueryParamComponent),
).rejects.toThrow(
'[ngxtension:injectQueryParams] Query parameter id is required but was not provided.',
);
});

it('should not throw error when required array query param is present', async () => {
TestBed.configureTestingModule({
providers: [
provideRouter([
{ path: 'required', component: RequiredArrayQueryParamComponent },
]),
],
});

const harness = await RouterTestingHarness.create();

const instance = await harness.navigateByUrl(
'/required?id=123&id=456',
RequiredArrayQueryParamComponent,
);
expect(instance.requiredIdArray()).toEqual(['123', '456']);
});
});
55 changes: 52 additions & 3 deletions libs/ngxtension/inject-query-params/src/inject-query-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@ import {
DefaultValueOptions,
InjectorOptions,
ParseOptions,
RequiredOptions,
} from 'ngxtension/shared';
import { map } from 'rxjs';

type QueryParamsTransformFn<ReadT> = (params: Params) => ReadT;

function missingRequiredParamError(key: string | undefined): Error {
return new Error(
`[ngxtension:injectQueryParams] Query parameter ${key} is required but was not provided.`,
);
}

/**
* The `QueryParamsOptions` type defines options for configuring the behavior of the `injectQueryParams` function.
*
Expand All @@ -23,7 +30,8 @@ export type QueryParamsOptions<ReadT, DefaultValueT> = ParseOptions<
string | null
> &
DefaultValueOptions<DefaultValueT> &
InjectorOptions & {
InjectorOptions &
RequiredOptions & {
/**
* The initial value to use if the query parameter is not present or undefined.
*
Expand Down Expand Up @@ -55,6 +63,18 @@ export function injectQueryParams(): Signal<Params>;
*/
export function injectQueryParams(key: string): Signal<string | null>;

/**
* The `injectQueryParams` function allows you to access and manipulate query parameters from the current route.
*
* @param {string} key - The name of the query parameter to retrieve.
* @param {QueryParamsOptions} options - Configuration options with required flag that ensures a non-null return.
* @returns {Signal} A `Signal` that emits the value of the specified query parameter. Throws an error if the query parameter is not present.
*/
export function injectQueryParams<ReadT = string>(
key: string,
options: QueryParamsOptions<ReadT, ReadT> & { required: true },
): Signal<ReadT>;

/**
* The `injectQueryParams` function allows you to access and manipulate query parameters from the current route.
*
Expand Down Expand Up @@ -105,7 +125,7 @@ export function injectQueryParams<ReadT>(
const route = inject(ActivatedRoute);
const queryParams = route.snapshot.queryParams || {};

const { parse, transform, initialValue, defaultValue } = options;
const { parse, transform, initialValue, defaultValue, required } = options;

if (!keyOrParamsTransform) {
return toSignal(route.queryParams, { initialValue: queryParams });
Expand All @@ -124,11 +144,21 @@ export function injectQueryParams<ReadT>(
| undefined;

if (!param) {
if (required) {
throw missingRequiredParamError(
keyOrParamsTransform as string | undefined,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the inject-params.ts file, keyOrParamsTransform is guaranteed to be a string at this point in the execution flow. The as string | undefined assertion is redundant and can be simplified to keyOrParamsTransform.

Suggested change
keyOrParamsTransform as string | undefined,
keyOrParamsTransform,

);
}
return defaultValue ?? initialValue ?? null;
}

if (Array.isArray(param)) {
if (param.length < 1) {
if (required) {
throw missingRequiredParamError(
keyOrParamsTransform as string | undefined,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Here, keyOrParamsTransform is already known to be a string. The as string | undefined assertion is redundant and can be simplified to keyOrParamsTransform.

Suggested change
keyOrParamsTransform as string | undefined,
keyOrParamsTransform,

);
}
return defaultValue ?? initialValue ?? null;
}
return parse
Expand All @@ -151,6 +181,18 @@ export function injectQueryParams<ReadT>(
* The `injectQueryParams` function namespace provides additional functionality for handling array query parameters.
*/
export namespace injectQueryParams {
/**
* Retrieve an array query parameter with optional configuration options.
*
* @param {string} key - The name of the array query parameter to retrieve.
* @param {QueryParamsOptions} options - Configuration options with required flag that ensures a non-null return.
* @returns {Signal} A `Signal` that emits an array of values for the specified query parameter. Throws an error if the query parameter is not present.
*/
export function array<ReadT = string>(
key: string,
options: QueryParamsOptions<ReadT, ReadT[]> & { required: true },
): Signal<ReadT[]>;

/**
* Retrieve an array query parameter with optional configuration options.
*
Expand Down Expand Up @@ -191,16 +233,23 @@ export namespace injectQueryParams {
const route = inject(ActivatedRoute);
const queryParams = route.snapshot.queryParams || {};

const { parse, transform, initialValue, defaultValue } = options;
const { parse, transform, initialValue, defaultValue, required } =
options;

const transformParam = (
param: string | string[] | null,
): (ReadT | string)[] | null => {
if (!param) {
if (required) {
throw missingRequiredParamError(key);
}
return defaultValue ?? initialValue ?? null;
}
if (Array.isArray(param)) {
if (param.length < 1) {
if (required) {
throw missingRequiredParamError(key);
}
return defaultValue ?? initialValue ?? null;
}
// Avoid passing the parse function directly into the map function,
Expand Down
1 change: 1 addition & 0 deletions libs/ngxtension/shared/src/options/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './default-value';
export * from './injector';
export * from './parse';
export * from './required';
6 changes: 6 additions & 0 deletions libs/ngxtension/shared/src/options/required.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type RequiredOptions = {
/**
* If true, throws an error if the parameter is not present.
*/
required?: boolean;
};