Detalle producto (tabs)

<< Añadir producto Proyecto Android >>

Esta sección va a ser algo más compleja que las páginas anteriores, ya que vamos a hacer una página de detalle de producto (product-details) con una navegación interna por pestañas (tabs). En este caso tendremos 2 pestañas que mostrarán 2 páginas internas:

  • info (product-info) → Mostrará la información del producto (descripción, precio e imagen), así como la información de su creador.
  • comments (product-comments) → Mostrará los comentarios de otros usuarios sobre el producto y permitirá añadir un nuevo comentario.

Lo primero que vamos a hacer es crear la página de detalle y las subpáginas que contendrá.

ionic g page products/product-detail

ionic g page products/product-detail/product-info

ionic g page products/product-detail/product-comments

Creando las rutas

Vamos a crear la ruta primero para cargar la página principal (product-detail). En esta ruta especificaremos tanto el componente, como las rutas anidadas que tendrá dentro. Estas rutas las vamos a poner (no es obligatorio) en un archivo aparte dentro de la carpeta de product-detail, llamado product-detail.routes.ts.

import { Routes } from "@angular/router";

export const productsRoutes: Routes = [
  //...
  {
    path: ':id',
    loadComponent: () =>
      import('./product-detail/product-detail.page').then(
        (m) => m.ProductDetailPage
      ),
    loadChildren: () => // Rutas internas (dentro de product-detail)
      import('./product-detail/product-detail.routes').then((m) => m.productDetailRoutes),
  },
];

Página de pestañas (ion-tabs)

Vamos a implementar la página que contiene las pestañas de detalle de producto (product-detail.page.ts). Esta página va a tener unas determinadas características:

  • Vamos a poner una cabecera (ion-header) con la descripción del producto y un botón para volver a la página anterior (/products). En el resto de páginas internas pondremos un encabezado vacío (simplemente para que se cree un margen con el contenido) ya que cuando ponemos el encabezado en la página que contiene ion-tabs, este se superpone al de las páginas internas. Además, no funcionaría el botón de volver atrás en páginas internas, ya que se crea dentro de la página que contiene las pestañas un segundo ion-router-outlet, y por lo tanto un sistema de navegación interno separado de la navegación principal que encontramos en app.component.
  • Cada pestaña o botón (ion-tab-button) tiene una propiedad llamada tab que contiene la ruta interna a la que se navegará. Es decir, si estamos en '/products/24' y cargamos la ruta interna info, estaremos redirigiendo a  '/products/24/info'.
  • Necesitamos cargar la información del producto ya que vamos a poner su descripción en el título de la página. También necesitaremos la información del producto dentro de la página interna product-info, por lo que se opta por guardar el producto en una señal para que como veremos a continuación sea relativamente simple compartir esa información con las páginas internas. En versiones anteriores a la 17 de Angular, al ser información asíncrona, tendríamos que haber optado por una solución diferente como un observable (BehaviorSubject) para compartir ese dato y no tener que volverlo a obtener.

Importante: Para que Angular nos pase parámetros de la ruta, en este caso la id del producto en parámetros de tipo Input, debemos configurar el router de Angular con la función withComponentInputBinding() en el archivo main.ts

Para cargar el producto a partir de la id recibida, utilizaremos la API rxResource introducida en Angular 19.

<ion-header [translucent]="true">
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-back-button defaultHref="products"></ion-back-button>
    </ion-buttons>
    @if (product()) {
      <ion-title>{{product()!.description}}</ion-title>
    }
  </ion-toolbar>
</ion-header>

<ion-tabs>
  <ion-tab-bar slot="bottom">
    <ion-tab-button tab="info">
      <ion-icon name="information-circle"></ion-icon>
      <ion-label>Info</ion-label>
    </ion-tab-button>
    <ion-tab-button tab="comments">
        <ion-icon name="chatbox-ellipses"></ion-icon>
        <ion-label>Comments</ion-label>
      </ion-tab-button>
  </ion-tab-bar>
</ion-tabs>

No olvides añadir la importación de los iconos informationCircle y chatboxEllipses en app.component.ts.

Página de información

En esta página vamos a mostrar la información de un producto dentro de una card (ion-card). El producto lo podemos obtener inyectando el componente de product-detail (se puede inyectar un componente antecesor en el DOM con inject). Al ser un dato de tipo signal, cuando cambie de valor en product-detail, es decir, cuando pase de undefined a tener el objeto disponible.

También incluiremos la funcionalidad de borrar el producto desde aquí. Esta acción nos redirigirá a la página principal (/products).

<ion-header>
  <ion-toolbar></ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  @if (product()) {
    <ion-card>
      <img [src]="product()!.imageUrl">
      <ion-card-content>
        <ion-card-title>{{product()!.description}}</ion-card-title>
        <ion-card-subtitle>{{product()!.price | currency:'EUR':'symbol'}}</ion-card-subtitle>
        <ion-button expand="block" color="danger" (click)="delete()">
          <ion-icon name="trash" slot="start"></ion-icon>
          <ion-label>Delete</ion-label>
        </ion-button>
      </ion-card-content>
      <ion-item lines="none">
        <ion-avatar slot="start">
          <img [src]="product()!.creator?.avatar">
        </ion-avatar>
        <ion-label>{{ product()!.creator?.name }}</ion-label>
      </ion-item>
    </ion-card>
  }
</ion-content>

Página de comentarios

La segunda página interna de detalle de producto contendrá los comentarios de los usuarios. En este caso no necesitaremos la información del producto, únicamente su id que podemos obtener de la ruta (input). Llamaremos al servicio de obtener comentarios y los mostraremos en una lista.

A la página añadiremos un ion-refresher para recargar los comentarios al deslizar el dedo hacia abajo. También un botón flotante (ion-fab) para añadir un nuevo comentario. El formulario para añadir comentarios se mostrará directamente en un alert (ion-alert).

Otra cosa que vamos a tener en cuenta es que añadiremos notificaciones Push a este proyecto (en otra sección del curso). La notificación informará que alguien ha comentado un producto que has creado. Al pulsar en la notificación, la aplicación cargará la página de comentarios de ese producto (y por lo tanto se obtendrán los comentarios del servidor). Sin embargo, si la aplicación está en segundo plano (pausada) en esa ruta en concreto, Angular no recargará la página y no se cargarán los comentarios otra vez (no se verá el nuevo). Para detectar eso tenemos el observable resume dentro del servicio Platform (también podríamos usar el evento appStateChange del plugin App).

Importante: Por defecto no se puede acceder desde rutas internas (hijas) a parámetros de navegación de rutas padre. Es decir, como dijimos antes, las páginas internas se cargan dentro de un ion-router-outlet interno y diferente al de app.component. Para heredar los parámetros de la ruta padre y que se pueda acceder a ellos directamente con Input, hay que habilitar la opción withRouterConfig({paramsInheritanceStrategy: 'always'}) en el archivo main.ts.

<ion-header>
  <ion-toolbar></ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-refresher #refresher slot="fixed" (ionRefresh)="loadComments(refresher)">
    <ion-refresher-content>
    </ion-refresher-content>
  </ion-refresher>
  <ion-fab vertical="bottom" horizontal="end" slot="fixed">
    <ion-fab-button color="secondary" (click)="addComment()">
      <ion-icon name="add"></ion-icon>
    </ion-fab-button>
  </ion-fab>
  <ion-list style="padding-bottom: 50px">
    @for (com of comments(); track com.id) {
      <ion-item>
        <ion-avatar slot="start">
          <ion-img [src]="com.user?.avatar"></ion-img>
        </ion-avatar>
        <ion-label text-wrap>
          <p>{{com.user?.name}}</p>
          <h4>{{com.text}}</h4>
        </ion-label>
      </ion-item>
    }
  </ion-list>
</ion-content>

<< Añadir producto Proyecto Android >>