Simplifica los parámetros de rutas en componentes de Angular

En mi artículo anterior sobre cómo construir una tienda de ejemplo, exploré las animaciones en Angular 17. Al mismo tiempo, también desarrollé la funcionalidad completa para la página de detalle de un producto.
La página de detalle del producto necesita extraer el ID del producto desde la URL usando el router, y luego usar este ID para consultar la información del producto desde una API, tal como se muestra a continuación:

Para lograr esto, necesito configurar ActivatedRoute en conjunto con RouterLink. Vamos a verlo paso a paso.
Configurar la ruta y RouterLink
Crea el componente product-detail usando el CLI de Angular:
ng g c pages/product-detailEn mi archivo app.routes.ts, agregué la nueva ruta usando :id para representar el parámetro y loadComponent.
{
path:'products/:id',
loadComponent: () =>
import('./pages/product-detail/product-detail.component').then(c => c.ProductDetailComponent)
}En mi código, agregué la directiva routerLink para dirigir a los usuarios a la página de detalles del producto, pasando tanto la URL como el parámetro.
[routerLink]="['/products/', product.id]En mi plantilla, se ve de la siguiente manera:
@for (product of products();track product.id) {
<kendo-card width="360px" [routerLink]="['/products/', product.id]"
style="cursor: pointer">
<img [src]="product.image" kendoCardMedia alt="cover_img"/>
<kendo-card-body>
<h4>{{ product.title }}</h4>
</kendo-card-body>
<kendo-card-footer class="k-hstack">
<span>Price {{ product.price | currency }}</span>
</kendo-card-footer>
</kendo-card>
} @empty {
<h2> No products! 😌</h2>
}Leer el parámetro del router
En la página de detalle, necesitamos inyectar tanto ActivatedRoute como HttpClient. En mi caso, realizo la petición dentro del mismo componente.
Usando route.paramMap en combinación con switchMap, obtengo el parámetro y hago la petición para retornar el producto. Para evitar la suscripción manual o usar el pipe async, utilizo toSignal y convierto el observable de HttpClient en una signal, que luego puedo usar en la plantilla.
Para mantener este artículo breve y directo, realizo la petición dentro del mismo componente, aunque es recomendable usar un servicio externo.
route = inject(ActivatedRoute)
http = inject(HttpClient);
$product = toSignal<Product>(this.route.paramMap.pipe(
switchMap(params => {
const id = params.get('id');
return this.http.get<Product>(`https://fakestoreapi.com/products/${id}`)
})
));En la plantilla, leemos la signal usando () para acceder al producto retornado.
@if ($product()) {
<kendo-card width="360px" >
<img [src]="$product()?.image" kendoCardMedia alt="cover_img"/>
<kendo-card-body>
<h4>{{ $product()?.title }}</h4>
<hr kendoCardSeparator/>
<p>
{{ $product()?.description }}
</p>
</kendo-card-body>
<kendo-card-footer class="k-hstack">
<span>Price {{ $product()?.price | currency }}</span>
</kendo-card-footer>
</kendo-card>
} @else {
<h2> Sorry product not found 😌</h2>
}Guardamos los cambios y todo debería funcionar. Pero, ¿existe una forma mejor o más concisa de hacerlo? A partir de Angular 16.1, podemos usar la propiedad de vinculación de Input que coincide con el parámetro. ¡Vamos a probarlo!
Usar Input con parámetros del router
Primero debemos registrar la función withComponentInputBinding() en la configuración del router. Esto permite vincular el input que coincida con el parámetro de la ruta, que en nuestro caso es id.
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()),
provideHttpClient()
]
};En product-details, agregamos la propiedad id con el decorador @Input(), y luego usamos ese id para hacer la petición. Para asegurarnos de que el id tenga valor, lo mostramos en el constructor.
export class ProductDetailComponent {
#http = inject(HttpClient);
@Input() id!: string;
$product = toSignal<Product>(this.#http.get<Product>(`https://fakestoreapi.com/products/${this.id}`));
constructor() {
console.log(this.id);
}El id es undefined, lo que provoca que el producto se solicite con un valor indefinido, porque el id todavía no se ha establecido en el componente. Para acceder a él, intentemos leerlo en el ciclo de vida OnInit.

Efectivamente, ahora sí recibe el id, pero la función toSignal() necesita ejecutarse dentro del injectionContext(). Si leíste mi artículo anterior sobre entender InjectionContext, reconocerás que estamos ante una situación similar.

Para resolver este problema, necesitamos inyectar Injector e importar runInInjectionContext.
#http = inject(HttpClient);
#injector = inject(Injector);Para que el código quede más limpio, movemos la petición a una nueva función llamada getProductDetail.
private getProductDetail() {
runInInjectionContext(this.#injector, () => {
this.$product = toSignal(
this.#http.get<Product>(`https://fakestoreapi.com/products/${this.id}`),
);
});
}El código final se verá así:
export class ProductDetailComponent implements OnInit {
@Input() id!: string;
$product!: Signal<Product | undefined>;
#http = inject(HttpClient);
#injector = inject(Injector);
ngOnInit(): void {
this.getProductDetail();
}
private getProductDetail() {
runInInjectionContext(this.#injector, () => {
this.$product = toSignal(
this.#http.get<Product>(`https://fakestoreapi.com/products/${this.id}`),
);
});
}
}Guardamos los cambios y todo debería funcionar. ¡Genial!
Resumen
La funcionalidad withComponentInputBinding nos ayuda a evitar suscripciones manuales, inyectar la ruta y obtener sus valores fácilmente. Por supuesto, si el procesamiento de datos es un poco más complejo, te recomiendo mover esa lógica a un servicio.
Te recomiendo revisar la documentación oficial:
-
https://www.danywalls.com/understanding-injectioncontext-and-signal-effects
-
Código fuente: https://github.com/danywalls/moduless-with-kendo/tree/feature/router_binding
Happy Coding 😁
Modern frontend development — Angular, React, TypeScript, accessibility, and performance.
Artículos Relacionados
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.