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

WIP: Enhance inject route utilities #450

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
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
60 changes: 59 additions & 1 deletion libs/ngxtension/inject-params/src/inject-params.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Component } from '@angular/core';
import {
Component,
inject,
Injector,
numberAttribute,
Signal,
} from '@angular/core';
import { TestBed } from '@angular/core/testing';

import { provideRouter } from '@angular/router';
Expand All @@ -22,22 +28,74 @@ describe(injectParams.name, () => {

expect(instance.params()).toEqual({ id: 'angular' });
expect(instance.userId()).toEqual('angular');
expect(instance.userIdCustomInjector!()).toEqual('angular');
expect(instance.paramKeysList()).toEqual(['id']);

await harness.navigateByUrl('/user/test', UserProfileComponent);

expect(instance.params()).toEqual({ id: 'test' });
expect(instance.userId()).toEqual('test');
expect(instance.userIdCustomInjector!()).toEqual('test');
expect(instance.paramKeysList()).toEqual(['id']);
});

it('returns a signal everytime the route params change based on the param id and transform option', async () => {
TestBed.configureTestingModule({
providers: [
provideRouter([
{ path: 'post/:id', component: PostComponent },
{ path: 'post', component: PostComponent },
]),
],
});

const harness = await RouterTestingHarness.create();

const instanceNull = await harness.navigateByUrl('/post', PostComponent);

expect(instanceNull.postId()).toEqual(null);
expect(instanceNull.postIdDefault()).toEqual(69);

const instance = await harness.navigateByUrl('/post/420', PostComponent);

expect(instance.postId()).toEqual(420);
expect(instance.postIdDefault()).toEqual(420);

await harness.navigateByUrl('/post/test', PostComponent);

expect(instance.postId()).toEqual(NaN);
expect(instance.postIdDefault()).toEqual(NaN);
});
});

@Component({
standalone: true,
template: ``,
})
export class UserProfileComponent {
private _injector = inject(Injector);

params = injectParams();
userId = injectParams('id');
paramKeysList = injectParams((params) => Object.keys(params));

userIdCustomInjector?: Signal<string | null>;

constructor() {
this.userIdCustomInjector = injectParams('id', {
injector: this._injector,
});
}
}

@Component({
standalone: true,
template: ``,
})
export class PostComponent {
postId = injectParams('id', { transform: numberAttribute });
postIdDefault = injectParams('id', {
transform: numberAttribute,
defaultValue: 69,
});
}
107 changes: 86 additions & 21 deletions libs/ngxtension/inject-params/src/inject-params.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,120 @@
import { assertInInjectionContext, inject, type Signal } from '@angular/core';
import { inject, type Signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, type Params } from '@angular/router';
import { assertInjector } from 'ngxtension/assert-injector';
import {
DefaultValueOptions,
InjectorOptions,
TransformOptions,
} from 'ngxtension/shared';
import { map } from 'rxjs';

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

/**
* Injects the params from the current route.
* The `ParamsOptions` type defines options for configuring the behavior of the `injectParams` function.
*
* @template ReadT - The expected type of the read value.
* @template WriteT - The type of the value to be written.
* @template DefaultValueT - The type of the default value.
*/
export type ParamsOptions<ReadT, WriteT, DefaultValueT> = TransformOptions<
ReadT,
WriteT
> &
DefaultValueOptions<DefaultValueT> &
InjectorOptions;

/**
* The `injectParams` function allows you to access and manipulate parameters from the current route.
*
* @returns A `Signal` that emits the entire parameters object.
*/
export function injectParams(): Signal<Params>;

/**
* Injects the params from the current route and returns the value of the provided key.
* @param key
* The `injectParams` function allows you to access and manipulate parameters from the current route.
*
* @param {string} key - The name of the parameter to retrieve.
* @returns {Signal} A `Signal` that emits the value of the specified parameter, or `null` if it's not present.
*/
export function injectParams(key: string): Signal<string | null>;

/**
* Injects the params from the current route and returns the result of the provided transform function.
* @param transform
* The `injectParams` function allows you to access and manipulate parameters from the current route.
*
* @param {string} key - The name of the parameter to retrieve.
* @param {ParamsOptions} options - Optional configuration options for the parameter.
* @returns {Signal} A `Signal` that emits the transformed value of the specified parameter, or `null` if it's not present.
*/
export function injectParams<T>(transform: (params: Params) => T): Signal<T>;
export function injectParams<ReadT>(
key?: string,
options?: ParamsOptions<ReadT, string, ReadT>,
): Signal<ReadT | null>;

/**
* The `injectParams` function allows you to access and manipulate parameters from the current route.
* It retrieves the value of a parameter based on a custom transform function applied to the parameters object.
*
* @template ReadT - The expected type of the read value.
* @param {ParamsTransformFn<ReadT>} fn - A transform function that takes the parameters object (`params: Params`) and returns the desired value.
* @returns {Signal} A `Signal` that emits the transformed value based on the provided custom transform function.
*
* @example
* const searchValue = injectParams((params) => params['search'] as string);
*/
export function injectParams<ReadT>(
fn: ParamsTransformFn<ReadT>,
): Signal<ReadT>;

/**
* Injects the params from the current route.
* If a key is provided, it will return the value of that key.
* If a transform function is provided, it will return the result of that function.
* Otherwise, it will return the entire params object.
*
* @template T - The expected type of the read value.
* @param keyOrParamsTransform OPTIONAL The key of the param to return, or a transform function to apply to the params object
* @param {ParamsOptions} options - Optional configuration options for the parameter.
* @returns {Signal} A `Signal` that emits the transformed value of the specified parameter, or the entire parameters object if no key is provided.
*
* @example
* const userId = injectParams('id'); // returns the value of the 'id' param
* const userId = injectParams(p => p['id'] as string); // same as above but can be used with a custom transform function
* const params = injectParams(); // returns the entire params object
*
* @param keyOrTransform OPTIONAL The key of the param to return, or a transform function to apply to the params object
*/
export function injectParams<T>(
keyOrTransform?: string | ((params: Params) => T),
keyOrParamsTransform?: string | ((params: Params) => T),
options: ParamsOptions<T, string, T> = {},
): Signal<T | Params | string | null> {
assertInInjectionContext(injectParams);
const route = inject(ActivatedRoute);
const params = route.snapshot.params;
return assertInjector(injectParams, options?.injector, () => {
const route = inject(ActivatedRoute);
const params = route.snapshot.params;
const { transform, defaultValue } = options;

if (typeof keyOrTransform === 'function') {
return toSignal(route.params.pipe(map(keyOrTransform)), {
initialValue: keyOrTransform(params),
});
}
if (!keyOrParamsTransform) {
return toSignal(route.params, { initialValue: params });
}

if (typeof keyOrParamsTransform === 'function') {
return toSignal(route.params.pipe(map(keyOrParamsTransform)), {
initialValue: keyOrParamsTransform(params),
});
}

const getParam = (params: Params) => {
const param = params?.[keyOrParamsTransform] as string | undefined;

const getParam = (params: Params) =>
keyOrTransform ? params?.[keyOrTransform] ?? null : params;
if (!param) {
return defaultValue ?? null;
}

return toSignal(route.params.pipe(map(getParam)), {
initialValue: getParam(params),
return transform ? transform(param) : param;
};

return toSignal(route.params.pipe(map(getParam)), {
initialValue: getParam(params),
});
});
}
Loading
Loading