Componentes (Parte 2)

<< Componentes (Parte 1) Capacitor >>

En esta sección veremos componentes de Ionic que requieren de código para interactuar con ellos, o simplemente para mostrarlos.

Toast

Un mensaje toast es un formato notificación muy utilizado en aplicaciones moderna. En Ionic tenemos el componente ion-toast para representar una notificación de este tipo.

Una notificación toast se puede representar con un elemento ion-toast en la plantilla y vincularlo a un botón o a un booleano que indica cuando está visible o no. Sin embargo, normalmente será más útil crear el mensaje desde código, cuando ocurra algún evento que implique notificar algo al usuario.

Para crear una notificación toast, utilizaremos el servicio de Ionic: ToastController.

Algunas opciones que le podemos pasar al elemento ion-toast o al método create del servicio ToastController son:

  • duration → Duración en milisegundos que permanecerá visible la notificación hasta que se cierre automáticamente.
  • position → Posición donde se mostrará. Puede tener los valores top, bottom y middle. Se puede combinar con positionAnchor, al que le pasamos la id de un elemento de la página, y la notificación se mostrará justo encima (position="bottom"), o justo debajo (position="top") de dicho elemento.
  • swipeGesture → Permite cerrar la notificación arrastrando la notificación. El valor puede ser horizontal o vertical.
  • buttons → Permite pasar un array de botones. Estos botones tienen propiedades como text (texto del botón), icon (icono), role ('info', 'cancel', etc. Permite saber qué botón se ha pulsado en el evento onDidDismiss), y handler, que permite asociar una función que se ejecuta al pulsar el botón. Al pulsar un botón de tipo cancel, la notificación se cierra automáticamente.
  • icon → Icono que podemos mostrar a la izquierda
  • layout → Si le pasamos esta opción con el valor 'stacked' y le pasamos uno o más botones, estos se mostrarán debajo del mensaje en lugar de abajo. Es útil sobre todo cuando el botón sea grande o tengamos más de un botón.
  • color → Color de la notificación

El elemento ion-toast emite el evento onDidDismiss que permite realizar una acción cuando se cierra la notificación. Si la creamos con ToastController tendremos el método onDidDismiss() que nos devolverá una promesa cuando se cierre la notificación.

<!-- ... -->
<ion-button (click)="showToast()">Show Toast</ion-button>
<!-- ... -->

Alert

El componente ion-alert sirve para mostrar un cuadro de dialogo, tanto para notificar al usuario de alguna información, como para buscar algún tipo de interacción (preguntar o seleccionar algo, rellenar un pequeño formulario, etc.). Se puede ver como una versión más compleja de ion-toast.

Al igual que con ion-toast, podemos utilizar el componente ion-alert dentro de la plantilla y activarlo con un botón o un booleano como se puede ver en la documentación oficial. Si queremos tener más control sobre cuando y como mostrar estas notificaciones, podemos utilizar el servicio AlertController.

Algunas opciones que le podemos pasar al cuadro de diálogo son:

  • header → Título del cuadro de diálogo
  • subHeader → Subtítulo (más pequeño debajo del título)
  • message → Texto de la ventana con el mensaje o pregunta que mostraremos al usuario.
  • buttons → Permite pasar un array de botones. Estos botones tienen propiedades como text (texto del botón), role ('info', 'cancel', etc. Permite saber qué botón se ha pulsado en el evento onDidDismiss), y handler, que permite asociar una función que se ejecuta al pulsar el botón. Al pulsar un botón de tipo cancel, la notificación se cierra automáticamente. También puede ser simplemente un array de strings (en ese caso el texto y el rol del botón será el string pasado).
  • inputs → Array de inputs por si queremos mostrarle al usuario opciones para elegir o campos para rellenar. Si los inputs son de tipo radio, el resultado será el valor del campo seleccionado. Si son de tipo checkbox, será un array de los valores elegidos. Y si son campos de texto (text, password, email, ...), el resultado será un objeto con los nombres de los campos como propiedades y su valor correspondiente.
<!-- ... -->
<ion-button (click)="showLogin()">Show alert with login</ion-button>
<p>Your email: {{email()}}</p>
<p>Your password: {{pass()}}</p>
<!-- ... -->

Como se puede observar, el resultado de la interacción del usuario la devuelve el evento onDidDismiss.

Action sheet

El componente ion-action-sheet es un tipo de diálogo que aparece en la parte inferior de la ventana y presenta algunas opciones para elegir.

Al igual que con ion-toast y ion-alert, se puede representar como un componente en la plantilla (ion-action-sheet), o crearlo con el servicio ActionSheetController. Se le pueden pasar las siguientes opciones:

  • header → Título del menú
  • subHeader → Subtítulo (más pequeño debajo del título)
  • buttons → Permite pasar un array de botones. Cada botón tiene un texto (text) y una función (handler) que se ejecuta al hacer clic sobre el botón. Después de hacer clic en un botón, el comportamiento predeterminado es cerrar la Action Sheet, a menos que la función handler devuelva false.

Un botón también puede tener un icono y un rol (cuyo estilo solo se aplica en iOS) . El rol puede ser "destructive" (solo el primer botón, aparece en rojo) o "cancel" (solo el último botón, aparece separado del resto). 

<!-- ... -->
<ion-button color="primary" (click)="showAction()">Show Action Sheet</ion-button>
<!-- ... -->

Loading

Por lo general, cuando realizamos una tarea en segundo plano que puede llevar más de unos pocos milisegundos (como conectarse a un servicio web cuando la red es lenta, por ejemplo), queremos informar al usuario de que debe esperar a que se complete una tarea importante. Mostrar un indicador de carga que bloquee la pantalla suele ser la mejor manera de hacerlo. Para ello tenemos el componente ion-loading.

Creamos estos indicadores de carga utilizando el elemento ion-loading, o desde código con el servicio LoadingController. Es similar a mostrar una alerta o un mensaje tipo toast. Después de crearlo, debemos mostrarlo llamando a present(), y desaparecerá una vez que llamemos a dismiss() en él (después de que la tarea finalice) o después de que transcurra la duración máxima (si se establece).

//...
export class LoadingPage {
  loading!: HTMLIonLoadingElement;
  data?: string;

  #loadingCtrl = inject(LoadingController);

  async ionViewWillEnter() {
    this.loading = await this.#loadingCtrl.create({
      message: 'Loading data',
      spinner: 'bubbles',
      cssClass: 'primary',
    });
    await this.loading.present(); // Show the loading element

    // Simulate a server call (2 seconds)
    setTimeout(() => {
      this.data = 'Data loaded';
      this.loading.dismiss(); // Close the loading element
    }, 2000);
  }
}

Popover

El componente ion-popover representa un menú contextual o un cuadro de diáologo asociado a un elemento de la aplicación. Es decir, a diferencia de un cuadro de diálogo creado por un ion-alert, o ion-action-sheet, que aparecen en una posición predeterminada de la pantalla, este elemento aparece siempre asociado y pegado a un elemento concreto de la aplicación.

Al igual que otros elementos de notificación, menú, o cuadro de diálogo que hemos visto, se puede añadir el elemento ion-popover asociado a un elemento como un botón mediante la id para que se muestre al hacer clic, o a un valor booleano que indica cuando se muestra. También se puede usar el servicio PopoverController para crearlo.

Excepto en el caso de la vinculación por id, debemos pasarle el objeto del evento asociado a su aparición (click, mouseover, ...) para que obtenga las coordenadas del elemento asociado al cuadro de diálogo. Si no, se mostrará en el centro de la pantalla.

Si usamos PopoverController para crear el elemento, debemos crear a su vez un componente (de Angular) que tendrá el contenido del popover y cuya clase gestionará la interacción del usuario con el mismo. Desde el componente podemos cerrar el popover en cualquier momento llamando al método dismiss de PopoverController. Es posible cerrarlo devolviendo un valor pasándoselo a este método. En este caso, se recogerá en el componente principal con la llamada a onDidDismiss (promesa). Vamos a ver un ejemplo:

<!-- ... -->
<ion-button (click)="showPopover($event)">Show popover</ion-button>
<p>Selected color: {{color()}}</p>
<!-- ... -->

Este sería el contenido del componente que le pasamos al elemento popover:

<ion-content>
  <ion-list>
    <ion-list-header>{{title()}}</ion-list-header>
    <ion-item [button]="true" (click)="close('green')">
      <ion-label>Green</ion-label>
    </ion-item>
    <ion-item [button]="true" (click)="close('red')">
      <ion-label>Red</ion-label>
    </ion-item>
    <ion-item [button]="true" (click)="close('blue')">
      <ion-label>Blue</ion-label>
    </ion-item>
    <ion-item [button]="true" (click)="close('yellow')">
      <ion-label>Yellow</ion-label>
    </ion-item>
  </ion-list>
</ion-content>

Refresher

El componente ion-refresher de Ionic te permite implementar la funcionalidad de "pull to refresh" (arrastrar para actualizar). Esto significa que cuando arrastras hacia abajo lo suficiente el contenido con el dedo, se mostrará un ícono de carga y opcionalmente texto, hasta que lo ocultes (cuando obtienes nuevos datos del servidor). Los íconos y textos predeterminados se pueden cambiar con parámetros que encontrarás en la documentación. Este componente se coloca al principio del elemento ion-content.

<!-- ... -->
<ion-content [fullscreen]="true">
  <ion-refresher #refresher slot="fixed" (ionRefresh)="refresh(refresher)">
    <ion-refresher-content>
    </ion-refresher-content>
  </ion-refresher>
  <ion-list>
    @for (n of items(); track $index) {
      <ion-item>
        <ion-label>{{n}}</ion-label>
      </ion-item>
    }
  </ion-list>
</ion-content>

Es importante llamar al método complete del objeto IonRefresher para parar la animación una vez se hayan actualizado los datos.

Infinite Scroll

El componente ion-infinite-scroll permite llamar automáticamente a un método (generalmente para cargar más elementos) cuando haces scroll hasta cerca del final del contenido. Este componente se coloca al final del elemento ion-content.

Podemos personalizar este componente cambiando el ícono del spinner y el texto mostrado. También podemos establecer un umbral (threshold) para indicar cuando se dispara el evento (una cadena en píxeles, % u otras unidades. Por defecto → 15%). Además, podemos desactivarlo con el atributo disabled (cuando no es necesario cargar más elementos).

El evento ionInfinite es el que indica que se ha activado el componente y es el momento de cargar más datos.

<!-- ... -->
<ion-content [fullscreen]="true">
  <ion-list>
    @for (item of items; track $index) {
    <ion-item>{{item}}</ion-item>
    }
  </ion-list>

  <ion-infinite-scroll
    #infinite
    (ionInfinite)="loadMoreItems(infinite)"
    [disabled]="finished"
  >
    <ion-infinite-scroll-content
      loadingSpinner="bubbles"
      loadingText="Loading more data..."
    >
    </ion-infinite-scroll-content>
  </ion-infinite-scroll>
</ion-content>

Es importante llamar al método complete del objeto IonInfiniteScroll para parar la animación una vez se hayan cargado los datos.

El componente ion-searchbar de Ionic es como un input de texto (ion-input), pero con más opciones orientadas a construir una barra de búsqueda. Se puede situar dentro del contenido (ion-content) o dentro de una barra de herramientas (ion-toolbar). En el siguiente ejemplo, utilizamos la propiedad debounce para establecer el tiempo entre que el usuario deja de escribir y se dispara el evento de nuevo valor (por defecto: 250 ms).

<ion-header [translucent]="true">
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>Searchbar</ion-title>
  </ion-toolbar>
  <ion-toolbar>
    <ion-searchbar
      debounce="500"
      [(ngModel)]="search"
      (ionInput)="filterItems()"
    >
    </ion-searchbar>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-list>
    @for (item of filteredItems; track item) {
    <ion-item>{{item}}</ion-item>
    }
  </ion-list>
</ion-content>

Reorder

El componente ion-reorder permite reordenar los elementos de una lista arrastrándolos a otra posición. Los elementos que queramos poder cambiar de orden deben agruparse dentro de un elemento ion-reorder-group. Podemos utilizar la propiedad "disable" para desactivar la opción de reordenar (oculta el icono que nos permite arrastrar el elemento).

El evento ionItemReorder se dispara cada vez que cambiamos la posición de algún elemento. Necesitamos llamar al método complete() del componente ion-reorder para confirmar los cambios, pasándole el array con los datos. Devolverá el nuevo array con el cambio resultante. Si no hacemos esto, se cancelará el cambio.

<!-- ... -->
<ion-content [fullscreen]="true">
  <ion-button expand="block" (click)="toggleReordering()">
    {{disableOrdering ? 'Enable' : 'Disable' }} reorder
  </ion-button>
  <ion-list>
    <ion-reorder-group (ionItemReorder)="reorder($event)" [disabled]="disableOrdering">
      @for (food of foods; track food) {
        <ion-item>
          <ion-label>{{food}}</ion-label>
          <ion-reorder slot="end"></ion-reorder>
        </ion-item>
      }
    </ion-reorder-group>
  </ion-list>
</ion-content>

Segment

El elemento ion-segment engloba a un grupo de botones (ion-segment-button) relacionados entre sí. Es decir, de los cuales solo podemos tener uno seleccionado. Básicamente es seleccionar una opción entre varias como hacemos con ion-select (lista de selección) o ion-radio-group (para agrupar botones ion-radio).

Este tipo de componente se suele utilizar muchas veces, sobre todo si lo colocamos en la cabecera de las páginas, para ocultar y mostrar diferente contenido en función del botón seleccionado. De una manera similar a la navegación por pestañas, pero en lugar de cargar diferentes páginas, todo está en la misma página y lo vamos ocultando o mostrando. También se puede usar en un formulario para que el usuario elija una entre varias opciones.

<ion-header [translucent]="true">
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>Segment</ion-title>
  </ion-toolbar>
  <ion-toolbar color="light">
    <ion-segment [(ngModel)]="type" (ngModelChange)="typeChanged()">
      <ion-segment-button value="heroes"> Heroes </ion-segment-button>
      <ion-segment-button value="villains"> Villains </ion-segment-button>
      <ion-segment-button value="weapons"> Weapons </ion-segment-button>
    </ion-segment>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-list>
    @switch (type) {
      @case ('heroes') {
        @for (heroe of heroes; track heroe) {
          <ion-item> {{ heroe }} </ion-item>
        }
      }
      @case ('villains') {
        @for (villain of villains; track villain) {
          <ion-item> {{ villain }} </ion-item>
        }
      }
      @case ('weapons') {
        @for (weapon of weapons; track weapon) {
          <ion-item> {{ weapon }} </ion-item>
        }
      }
    }
  </ion-list>
</ion-content>

Gestures

Ionic (y Angular) admite múltiples eventos táctiles utilizando la biblioteca HammerJS. Todo lo que necesitamos hacer es instalarla (npm i hammerjs) e importarla en el archivo polyfills.ts (import 'hammerjs'). Después de eso, importamos proveedores (servicios) de HammerModule en el archivo main.ts:

//...
bootstrapApplication(AppComponent, {
  providers: [
    //...
    importProvidersFrom(HammerModule)
  ],
});

Puedes detectar nuevos eventos táctiles como tap (toque con un dedo), press (dedo presionado durante unos segundos), pan (dedo movido mientras está presionado), swipe (dedo movido de un lado a otro mientras está presionado), rotate (rotación con dos dedos) y pinch (pellizco con dos dedos). En cada evento, la variable especial $event representa el objeto de evento (con información adicional como la posición del dedo, etc.).

Skeleton

El componente ion-skeleton-text permite crear contenido de marcador de posición (barras grises para texto, rectángulos grises para imágenes, con animaciones) antes de que el contenido real se cargue y esté listo para mostrarse. Esto proporciona una mejor experiencia para el usuario.

<!-- ... -->
@if (data()) {
  <ion-list>
    <ion-list-header color="light">
      People
    </ion-list-header>
    <ion-item>
      <ion-avatar slot="start">
        <img src="./assets/tiofeo.jpg">
      </ion-avatar>
      <ion-label>
        <h2>Ugly man</h2>
        <h3>I'm a big deal</h3>
        <p>Listen, I've had a pretty messed up day...</p>
      </ion-label>
    </ion-item>
    <ion-item>
      <ion-avatar slot="start">
        <img src="./assets/tioraro.jpg">
      </ion-avatar>
      <ion-label>
        <h2>Weird man</h2>
        <h3>I don't know who am I</h3>
        <p>More text...</p>
      </ion-label>
    </ion-item>
  </ion-list>
} @else {
  <ion-list>
    <ion-list-header color="light">
      <ion-skeleton-text animated style="width: 50%"></ion-skeleton-text>
    </ion-list-header>
    @for (i of [1,2,3]; track $index) {
      <ion-item>
        <ion-avatar slot="start">
          <ion-skeleton-text animated></ion-skeleton-text>
        </ion-avatar>
        <ion-label>
          <ion-skeleton-text animated style="width: 50%"></ion-skeleton-text>
          <ion-skeleton-text animated style="width: 70%"></ion-skeleton-text>
          <ion-skeleton-text animated></ion-skeleton-text>
        </ion-label>
      </ion-item>
    }
  </ion-list>
}
<!-- ... -->

Las ventanas modales ion-modal se crean de manera parecida a otros componentes como ion-popover, y como se puede observar en la documentación oficial, pueden crearse tanto en la plantilla del componente (elemento ion-modal), como usando el servicio ModalController. En este último caso habría que pasarle un componente de Angular como parámetro.

Por defecto, las ventanas modales se muestran a pantalla completa en dispositivos móviles (con la pantalla en vertical), y más pequeñas en otros dipositivos con mayor ancho de pantalla.

Igual que pasa con ion-popover, las ventanas modales se pueden cerrar llamando al método dismiss() de ModalController. Se puede devolver un valor pasándoselo a dicho método, que luego se recogerá con el método onDidDismiss en la página principal.

<!-- ... -->
<ion-content [fullscreen]="true">
  <ion-item>
    <ion-input
      label="Name"
      labelPlacement="floating"
      type="text"
      [(ngModel)]="name"
      required
    >
    </ion-input>
  </ion-item>
  <ion-button (click)="openModal()" color="secondary" expand="block">
    <ion-label>Open modal</ion-label>
  </ion-button>
  @if (food()) {
  <p>You picked {{food()}}</p>
  }
</ion-content>
<ion-header>
  <ion-toolbar color="primary">
    <ion-title>Modal Content</ion-title>
    <ion-buttons slot="end">
      <ion-button (click)="close()">
        <ion-icon slot="icon-only" name="close"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-list-header color="light">
      Hello {{name}}, pick a food
    </ion-list-header>
    <ion-radio-group [(ngModel)]="food">
      <ion-item>
        <ion-radio value="pizza" color="primary">Pizza</ion-radio>
      </ion-item>
      <ion-item>
        <ion-radio value="hamburger" color="secondary">Hamburger</ion-radio>
      </ion-item>
      <ion-item>
        <ion-radio value="salad" color="danger">Salad</ion-radio>
      </ion-item>
    </ion-radio-group>
  </ion-list>
  <ion-button color="success" expand="block" (click)="chooseFood()">
    <ion-label>Choose food</ion-label>
  </ion-button>
</ion-content>

<< Componentes (Parte 1) Capacitor >>