Parámetros de entrada

<< Enrutamiento básico Route guards >>

Cuando queremos ir a la página de detalle de un producto cuyo componente hemos creado antes (product-detail), debemos indicar qué producto vamos a ver. La ruta definida para esta vista es 'products/:id'.

Los componentes de una ruta que comienzan con ':' son parámetros de la ruta y son valores variables. Al cargar la ruta de detalle de un producto, se pondrá la id del producto asociado a ese parámetro, por ejemplo products/4 (donde 4 es el id del producto).

Veamos cómo llamar a esta ruta a partir de un enlace asociado a la descripción de un producto:

<!-- ... -->
<div class="col-4 my-auto">
  <a [routerLink]="['/products', product().id]">{{ product().description }}</a>
</div>
<!-- ... -->

En la sección resource API ya explicamos como crear un httpResource para obtener un producto por id dentro de ProductsService. En este apartado lo usaremos para la página de detalle de un producto.

Inyectar parámetros de la ruta

Las últimas versiones de Angular permiten que el componente reciba los parámetros de la ruta como parámetros de entrada (input). Además, podemos pasarle la opción transform que recibe una función que transforma el dato antes de devolverlo. En este caso, la función numberAttribute (@angular/core), nos lo devuelve como number.

Para usar inputs para obtener parámetros de ruta, debemos añadir la llamada a la función withComponentInputBinding como parámetro a la función provideRouter en el archivo app.config.ts.

export const appConfig: ApplicationConfig = {
  providers: [
    //...
    provideRouter(routes, withComponentInputBinding()),
    //...
  ],
};

Obsoleto: Antes de existir esta opción se debía utilizar el servicio ActivatedRoute y acceder a los parámetros de la ruta desde ahí. Esta opción está disponible desde Angular 2.0.

export class ProductDetail implements OnInit {
  #productsService = inject(ProductsService);
  #activatedRoute = inject(ActivatedRoute);
  product?: Product;

  ngOnInit(): void {
    this.#activatedRoute.paramMap.subscribe((params) => {
      this.#productsService
        .getProduct(+params.get('id')!)
        .subscribe((p) => (this.product = p));
    });
  }
}

A partir de Angular 19 también está la opción de utilizar la API Resource con funciones como httpResource para crear  un recurso que se actualizará cada vez que cambie una señal vinculada (en este caso la id), y del que podríamos extraer información como el estado (status), si está cargando un nuevo producto , etc. para darle un feedback más preciso al usuario.

export class ProductDetail {
  id = input.required({ transform: numberAttribute });

  #productsService = inject(ProductsService);
  #title = inject(Title);

  productResource = this.#productsService.getProductIdResource(this.id);
  product = computed(() => this.productResource.value());

  //...
}

Los recursos creados con resource, rxResource, o httpResource son de lectura/escritura. Es decir, cuando cambie la id automáticamente se recargaría el producto a partir del servidor, pero también podríamos modificar y sustituir el producto en local → productResource.set(...).

Mostrando datos asíncronos

Vamos a crear una plantilla para mostrar los datos del producto. La cuestión es que el producto, hasta que el servidor nos devuelve los datos, está con valor undefined. Si no comprobamos eso, Angular nos dará un error cuando trate de acceder a las propiedades del producto en la plantilla la primera vez. Después, una vez recibe el producto y lo asigna, lo renderiza correctamente.

Para evitar eso podríamos utilizar product? en la plantilla, y mientras no haya cargado el producto, los campos estarán vacíos. Sin embargo, vamos a englobar todo dentro de una estructura @if, para poder mostrar un mensaje de carga alternativo mientras esperamos a los datos.

Al incorporar en la plantilla el componente star-rating, deberíamos poner el método para cambiar la puntuación en el producto también en product-detail, el mismo que en product-item. Si compartieran más lógica, podríamos considerar reutilizar el componente product-item pasándole un booleano por ejemplo, que indicara si queremos el HTML con la estructura de card o de fila.

@if (productResource.hasValue()) {
  <div class="card">
    <div class="card-header bg-primary text-white">
      {{ product()!.description }}
    </div>
    <div class="card-block p-3 text-center">
      <img [src]="product()!.imageUrl" alt="" />
      <div>Price: {{ product()!.price | intlCurrency : "EUR" : "es-ES" }}</div>
      <div>Available since: {{ product()!.available | date : "dd/MM/y" }}</div>
      <div>
        <star-rating [rating]="product()!.rating" (ratingChange)="changeRating($event)"></star-rating>
      </div>
      <div class="mt-3">
        <button type="button" class="btn btn-danger" (click)="goBack()">Go back</button>
      </div>
    </div>
  </div>
} @else {
  <p>Cargando datos del producto...</p>
}

Aunque este apartado currespondería más a la sección anterior (Enrutamiento básico), es un buen momento para introducir un ejemplo en nuestra aplicación de productos.

Para navegar a otra página desde código, debemos inyectar en el componente el servicio Router de Angular. Con el método navigate, le podemos pasar un array con los componentes de la ruta donde queremos ir, igual que se los pasamos a la directiva routerLink. También está el método navigateByUrl que recibe un string con la ruta completa.

Vamos a crear un método para volver a la página del listado de productos desde el detalle.

<!-- ... -->
<button type="button" class="btn btn-danger" (click)="goBack()">
  Go back
</button>
<!-- ... -->

<< Enrutamiento básico Route guards >>