Menú lateral con opción de compartir

El objetivo

En esta ocasión añadiremos al ejercicio anterior la opción de compartir, de forma que desde nuestra aplicación podamos enviar la lista de tareas mediante un mensaje de WhatsApp, un correo electrónico, un SMS, etc.

Además, añadiremos el código necesario para disponer de un menú lateral donde podremos añadir más opciones, y donde sugerimos además colocar la opción de borrar la lista de tareas, para que no esté tan accesible desde la pantalla principal:

El menú lateral

Primero añadiremos en la barra superior de la pantalla principal de nuestra aplicación un botón para desplegar el menú lateral:

<ion-header>
  ...
  <ion-menu-button></ion-menu-button>
  ...    
</ion-header>

Como podemos leer en la documentación de IONIC, el elemento <ion-menu-button></ion-menu-button> se encarga de todo, ya que crea automáticamente el icono del botón y añade la funcionalidad necesaria para desplegar el menú en la página actual.

Sólo nos falta añadir el código necesario con el menú lateral, que tendrá su propio encabezado y una lista con las opciones que deseemos. En nuestro caso pondremos la opción de reordenar, de compartir y de borrado de todos los elementos de la lista seleccionada. Además, añadiremos también un botón para volver a ocultar el menú:

<ion-menu>
  <ion-header>
    ...
    <ion-menu-button></ion-menu-button>
    ...
  </ion-header>
  <ion-content>
    <ion-menu-toggle>
      <ion-list>
        <ion-item-divider><ion-label>Reorder and Share</ion-label></ion-item-divider>   
        <ion-item (click)="toggleReorder()">
          <span *ngIf="reorder">Disable reorder</span>
          <span *ngIf="!reorder">Enable reorder</span>
          <ion-icon slot="end" name="reorder"></ion-icon>
        </ion-item>
        <ion-item (click)="share()">
          Share<ion-icon slot="end" name="share"></ion-icon>
        </ion-item>
        ...
      </ion-list>
    </ion-menu-toggle>
  </ion-content>
</ion-menu>
<ion-router-outlet main></ion-router-outlet>

Observaremos que el elemento <ion-menu></ion-menu> también se encarga de todo. Por defecto creará un menú que aparezca desde la izquierda de la pantalla actual, tal como se especifica en la documentación de IONIC. Se podrá ocultar pulsando de nuevo un botón de menú, o con un gesto swipe hacia la izquierda, o incluso pulsando fuera del menú. Además, utilizaremos el elemento <ion-menu-toggle></ion-menu-toggle> para conseguir que al pulsar en cualquier opción, el menú también se cierre automáticamente.

Aprovechando además el potencial que nos proporciona Angular con la directiva ngIf (más detalles aquí), haremos que el texto de la opción de reordenar cambie en función de si se encuentra activada o no, utilizando el atributo reorder :

... 
<span *ngIf="reorder">Disable reorder</span>
<span *ngIf="!reorder">Enable reorder</span>
...

El fichero «home.page.html» completo

<ion-menu>
  <ion-header>
    <ion-toolbar color="danger">
      <ion-title>Menu</ion-title>
      <ion-buttons slot="primary">
          <ion-menu-button></ion-menu-button>
      </ion-buttons>
    </ion-toolbar>
  </ion-header>
  <ion-content>
    <ion-menu-toggle>
      <ion-list lines="full">
        <ion-item-divider><ion-label>Reorder and Share</ion-label></ion-item-divider>   
        <ion-item (click)="toggleReorder()">
          <ion-icon slot="end" name="reorder"></ion-icon>
          <span *ngIf="reorder">Disable reorder</span>
          <span *ngIf="!reorder">Enable reorder</span>
        </ion-item>
        <ion-item (click)="share()">
          <ion-icon slot="end" name="share"></ion-icon>Share
        </ion-item>
        <ion-item-divider><ion-label>Delete</ion-label></ion-item-divider>
        <ion-item (click)="deleteItem()">
          <ion-icon slot="end" name="trash" color="danger"></ion-icon>Delete all
        </ion-item>
      </ion-list>
    </ion-menu-toggle>
  </ion-content> 
</ion-menu>
<ion-router-outlet main></ion-router-outlet>
<ion-header>
  <ion-toolbar color="primary">
    <ion-title>ToDo!</ion-title>
    <ion-buttons slot="primary">
      <ion-menu-button></ion-menu-button>
      <ion-button [routerLink]="['/AddEditItem', { tab:tabIndex, item:-1 }]"><ion-icon slot="icon-only" name="add"></ion-icon></ion-button>
    </ion-buttons>
  </ion-toolbar>       
</ion-header>
<ion-content>
  <ion-tabs #myTabs color="primary">
    <ion-tab *ngFor="let tab of tabs; let i=index" [label]="tab.label" [icon]="tab.icon" (ionSelect)="setTab(i)">
      <ion-list #myList lines="full">
        <ion-reorder-group [disabled]="!reorder" (ionItemReorder)="moveItem($event.detail)">
          <ion-item-sliding *ngFor="let item of tabs[i].list; let j=index">
            <ion-item [routerLink]="['/AddEditItem', { tab:i, item:j }]">
              <ion-label text-wrap>
                <h2>{{item.task}}</h2>
                <p>{{item.date}}</p>
              </ion-label>
              <ion-icon slot="end" [name]="item.icon"></ion-icon>
              <ion-reorder slot="end"></ion-reorder>
            </ion-item>
            <ion-item-options side="start">
              <ion-item-option color="danger" (click)="deleteItem(j)">
                <ion-icon slot="icon-only" name="trash"></ion-icon>
              </ion-item-option>
            </ion-item-options>
          </ion-item-sliding>
        </ion-reorder-group>
      </ion-list>
    </ion-tab>
  </ion-tabs>
</ion-content>

La opción de compartir

Utilizaremos la funcionalidad nativa de nuestros dispositivos móviles para poder enviar la lista de tareas a alguno de nuestros contactos. Para añadir el plugin necesario para acceder a dicha funcionalidad, nos colocaremos dentro de la carpeta de nuestro proyecto, y seguiremos los pasos indicados en la documentación de IONIC:

cd tareas
ionic cordova plugin add cordova-plugin-x-socialsharing
npm install --save @ionic-native/social-sharing@beta

Respecto a las modificaciones en el código fuente, en primer lugar deberemos añadir el import y la clase SocialSharing al array providers del archivo app.module.ts, tal como se indica en la documentación de IONIC:

...
import { SocialSharing } from '@ionic-native/social-sharing/ngx';

@NgModule({
  ...
  providers: [
    ...
    SocialSharing,
    ...
  ],
  ...
})
...

A continuación modificaremos el código fuente del archivo home.page.ts para añadir el método share que creará la cadena de texto con todas las tareas, y se la proporcionará al plugin para que pueda ser compartida:

import { SocialSharing } from '@ionic-native/social-sharing/ngx';
...
export class HomePage {
  ...
  constructor(private listService: ListService,
              private alertController: AlertController,
              private socialSharing: SocialSharing){
    ...
  }
  ...
  share() {
    let list:string = this.tabs[this.tabIndex].label + ":\n";
    this.tabs[this.tabIndex].list.forEach((task, index) => {
      list += (index+1) + ". " + task.task + " - " + task.date + "\n";
    });
    this.socialSharing.share(list);
  }
}

En resumen, en este archivo debemos realizar tres modificaciones. La primera de ellas es añadir el import:

import { SocialSharing } from '@ionic-native/social-sharing/ngx';

La segunda, modificar el constructor para recibir el código necesario de la clase SocialSharing:

constructor(private listService: ListService,
            private alertController: AlertController,
            private socialSharing: SocialSharing){
  ...
}

Y por último, crear el método share() cuya funcionalidad será obtener en una única cadena todas las tareas una detrás de otra, separadas por saltos de línea, y llamar a la función share del plugin para mostrar el menú de compartir de nuestros móviles:

share() {
  let list:string = this.tabs[this.tabIndex].label + ":\n";
  this.tabs[this.tabIndex].list.forEach((task, index) => {
    list += (index+1) + ". " + task.task + " - " + task.date + "\n";
  });
  this.socialSharing.share(list);
}

El archivo «app.module.ts» completo

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { SocialSharing } from '@ionic-native/social-sharing/ngx';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    SocialSharing,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

El archivo «home.page.ts» completo

import { Component, ViewChild } from '@angular/core';
import { Tabs, List, AlertController } from '@ionic/angular';
import { ListService } from '../list.service';
import { SocialSharing } from '@ionic-native/social-sharing/ngx';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})

export class HomePage {
  @ViewChild('myTabs') tabRef: Tabs;
  @ViewChild('myList') listRef: List;

  tabs: any;
  tabIndex: number;
  reorder: boolean;

  constructor(private listService: ListService,
              private alertController: AlertController,
              private socialSharing: SocialSharing){
    this.tabs = [
      {label: 'School', icon: 'school', list: []},
      {label: 'Home', icon: 'home', list: []}
    ];
    this.tabs.forEach((tab, index) => {
      tab.list = this.listService.getList(index);
    });
    this.tabIndex = 0;
    this.reorder = false;
  }

  ionViewDidEnter() {
    this.tabRef.select(this.tabIndex);
  }

  toggleReorder() {
    this.reorder = !this.reorder;
  }

  setTab(tabIndex) {
    this.tabIndex = tabIndex;
  } 

  async deleteItem(item?) {
    const alert = await this.alertController.create({
      header: item === undefined ? 'Delete all' : 'Delete item',
      message: 'Are you sure?',
      buttons: [
        {
          text: 'OK',
          handler: () => {
            this.listRef.closeSlidingItems();            
            if (item === undefined) {
              this.listService.deleteList(this.tabIndex);
            }
            else {
              this.listService.deleteItem(this.tabIndex, item);              
            }
          }
        },       
        {
          text: 'CANCEL',
          role: 'cancel'
        }
      ]
    });
    await alert.present();
  }

  moveItem(indexes) {
    this.listService.moveItem(this.tabIndex, indexes.from, indexes.to);
  }

  share() {
    let list:string = this.tabs[this.tabIndex].label + ":\n";
    this.tabs[this.tabIndex].list.forEach((task, index) => {
      list += (index+1) + ". " + task.task + " - " + task.date + "\n";
    });
    this.socialSharing.share(list);
  }
  
}

Compilando la aplicación con un solo comando

Debemos recordar que para probar la aplicación en nuestros dispositivos móviles, tenemos que compilar el código fuente. En este ejercicio vamos a utilizar sólo el cliente de IONIC, que nos permite generar el fichero APK en un único comando (más detalles aquí):

ionic cordova build android --prod

O también lo podemos ejecutar directamente  en el móvil que tengamos conectado a nuestro ordenador mediante el siguiente comando (más detalles aquí):

ionic cordova run android --prod

El resultado

Puedes hacer clic aquí para observar el aspecto final que tendrá la aplicación de la lista de tareas.

Lista de tareas con IONIC 4+Angular+Cordova

El objetivo: IONIC + Angular

En los ejercicios que hemos desarrollado hasta ahora sólo hemos utilizado IONIC de manera independiente, sin echar mano de ningún otro framework adicional, ya que hemos interactuado con los nuevos elementos HTML utilizando código JavaScript.

Para aplicaciones sencillas, puede ser una opción completamente válida y muy recomendable, dado el bajo nivel de requerimientos. Sólo necesitamos conocimientos básicos de desarrollo web, y con una simple referencia desde el código HTML, ya tenemos a nuestro alcance todo el potencial ofrecido por IONIC 4.

Sin embargo, si pretendemos desarrollar aplicaciones más complejas, resulta muy recomendable usar la segunda posibilidad que nos ofrece IONIC 4, al permitirnos combinar unos resultados visuales muy buenos, con un estilo de programación muy estructurado, y con amplias posibilidades dentro del desarrollo web.

En este ejercicio vamos a dar un paso adelante, y observaremos el potencial del que disponemos al usar IONIC con un framework tan utilizado y conocido como Angular. Desarrollaremos la misma aplicación que en el ejercicio anterior, pero veremos que el código fuente estará mucho mejor estructurado, y nos ofrecerá muchas más posibilidades que el simple código JavaScript.

Además, al tener la suerte de poder utilizar IONIC 4, disponemos de toda la documentación por duplicado para que podamos escoger la opción que más nos interese. En las versiones anteriores, IONIC se encontraba empaquetado con Angular, con lo que sólo teníamos una alternativa. Ahora mismo tenemos a nuestro alcance las mismas facilidades, tanto si escogemos desarrollar nuestra app utilizando HTML+JavaScript o Angular. Además, sea cual sea la opción por la que nos declinemos, IONIC nos proporciona la funcionalidad y la documentación necesaria para cada uno de sus componentes en ambos casos:

Como opinión personal, me gustaría decir que una vez se aprende Angular, resulta difícil desprenderse de él, ya que se pueden desarrollar aplicaciones más complejas y mucho más estables, con un código fuente más legible y mucho más fácil de mantener y actualizar.

La funcionalidad de la aplicación

La funcionalidad a implementar será la misma que en el ejercicio anterior, aunque en esta ocasión vamos a estructurar mejor el código y dividiremos la aplicación en dos pantallas.

Vamos a enumerar de nuevo los aspectos principales de la aplicación, y observaremos que podemos obtener el mismo resultado desarrollando la aplicación con Angular.

La pantalla principal

  • Gestionar varias listas de tareas independientes.
  • Borrar de golpe todas las tareas de una lista.
  • Borrar una sola tarea de una lista determinada mediante un botón oculto que se visualizará al deslizar hacia la derecha la tarea a borrar.
  • Pedir confirmación antes de borrar cualquier tarea.
  • Permitir ordenar las tareas arrastrando cualquier elemento a una nueva posición.

La pantalla de detalles

  • Añadir tareas indicando la fecha, la descripción de la tarea, y un icono identificativo de la prioridad.
  • Visualizar y editar los detalles de cada tarea haciendo clic sobre el elemento correspondiente de la lista.

Primeros pasos

Requisitos

La mayoría de las aplicaciones de IONIC se crean y desarrollan principalmente utilizando la herramienta de la línea de comandos. A continuación enumeraremos los pasos necesarios para instalar todo el software necesario en nuestros ordenadores para poder desarrollar y compilar desde la consola aplicaciones de IONIC utilizando Angular.

Puesto que pretendemos compilar la aplicación para utilizarla en nuestros dispositivos móviles, tendremos que instalar como mínimo las herramientas de consola de Android (sdk-tools) o el paquete completo con Android Studio, que se pueden descargar desde aquí.

En segundo lugar, deberemos instalar Node.js (la versión 8, tal como se indica en la web, recomendada para la mayoría de usuarios). Podremos comprobar si se ha instalado correctamente de la siguiente forma:

node --version
npm --version

A continuación deberemos instalar cordova y el cliente de IONIC, de la siguiente forma:

npm install -g cordova ionic

La opción  -g significa que se va a instalar de manera global en nuestros equipos, y por lo tanto necesitaremos permisos de administrador. En el caso de utilizar un sistema operativo Windows es recomendable abrir un terminal en modo administrador. Para sistemas operativos Mac/Linux, el comando se deberá ejecutar con sudo.

Código base de la aplicación

Para comenzar a desarrollar nuestra aplicación deberemos partir de un código base generado automáticamente por IONIC. Bastará con ejecutar el siguiente comando para generar los ficheros necesarios:

ionic start tareas blank --type=angular --cordova --no-git --no-link

Al ejecutar el comando, IONIC nos informará del proceso con algún aviso y  si todo va bien veremos algo similar a esto:

[WARN] Error: Could not determine project type (project config: ./ionic.config.json).
       
       - For ionic/angular 4 projects, make sure @ionic/angular is listed as a dependency in package.json.
       - For Ionic Angular 3 projects, make sure ionic-angular is listed as a dependency in package.json.
       - For Ionic 1 projects, make sure ionic is listed as a dependency in bower.json.
       
       Alternatively, set type attribute in ionic.config.json to one of: angular, ionic-angular, ionic1, custom.
       
       If the Ionic CLI does not know what type of project this is, ionic build, ionic serve, and other commands may not 
       work. You can use the custom project type if that's okay.

✔ Preparing directory ./tareas - done!
✔ Downloading and extracting blank starter - done!
> ionic integrations enable cordova --quiet
[INFO] Downloading integration cordova
[INFO] Copying integrations files to project
[OK] Integration cordova added!
...

Al finalizar el proceso, se habrá creado un directorio con el nombre tareas, que constituye el directorio raíz de nuestro proyecto, donde tendremos todo el código necesario para continuar con el desarrollo de nuestra aplicación. Para realizar los siguientes pasos deberemos acceder primero a dicha carpeta:

cd tareas

Como todavía no disponemos de la versión definitiva de IONIC, vamos a instalar la versión beta más estable hasta el momento:

npm install @ionic/[email protected]

Y por último, puesto que la aplicación básica que acabamos de crear sólo dispone de una pantalla principal, procederemos además a añadir una página adicional, y un fichero auxiliar (servicio) donde ubicaremos la funcionalidad básica para gestionar las listas y sus elementos:

ionic generate page AddEditItem
ionic generate service list

Y a partir de ahora ya podemos comenzar a añadir el código específico de nuestra aplicación. Aunque la funcionalidad sea la misma, al utilizar Angular, el código fuente sí tendrá variaciones, que como vamos a observar a continuación, nos permitirán generar un código mejor estructurado y más legible que el de una aplicación del mismo tamaño desarrollada simplemente con JavaScript.

Si hemos ejecutado correctamente los comandos anteriores, IONIC ya nos habrá creado todos los ficheros que vamos a utilizar en este ejercicio:

  1. src/app/list.service.ts: Funciones auxiliares.
  2. src/app/home/home.page.html: Código HTML de la pantalla principal.
  3. src/app/home/home.page.ts: Código TypeScript de la pantalla principal.
  4. src/app/add-edit-item/add-edit-item.page.html: Código HTML de la pantalla con los detalles de las tareas.
  5. src/app/add-edit-item/add-edit-item.page.ts: Código TypeScript de la pantalla con los detalles de las tareas.

Probando la aplicación en el navegador

Una de las primeras ventajas con las que nos encontramos es que la misma herramienta de consola de IONIC se puede utilizar como servidor web, con lo que nos permitirá  probar nuestra aplicación en el navegador al mismo tiempo que se actualiza automáticamente después de realizar cualquier cambio en el código fuente. Bastará con ejecutar el siguiente comando desde la carpeta raíz del proyecto donde se encuentra nuestra aplicación, dejando el terminal abierto:

ionic serve

Si todo ha ido bien, se compilará la aplicación y deberíamos visualizar en el navegador algo del siguiente estilo:

(bastará con pulsar Ctrl+C en el terminal para finalizar la ejecución del comando)

Funciones auxiliares

En el fichero list.service.ts desarrollaremos el código de una clase que contenga un array de listas de tareas y las funciones básicas para acceder a las mismas y a sus elementos:

  1. getList(index): Nos devolverá la lista completa ubicada en la pestaña index (la primera pestaña vendrá indicada por el valor cero).
  2. saveList(listIndex): Guardará la lista indicada de manera permanente utilizando localStorage.
  3. getItem(listIndex, itemIndex): Devolverá una tarea en particular.
  4. setItem(listIndex, item, itemIndex?): Añadirá una tarea a la lista indicada, o si se indica el índice de una tarea concreta, la actualizará en vez de añadirla.
  5. deleteItem(listIndex, itemIndex): Borrará la tarea indicada.
  6. deleteList(listIndex): Borrará una lista entera.
  7. moveItem(listIndex, from, to): Cambiará de orden una tarea, moviéndola de la posición from a la posición to.

El fichero «list.service.ts» completo

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class ListService {

  list: any;

  constructor() { 
    this.list = [[]];
  }

  getList(index) {
    let list = localStorage.getItem('todo-list-'+index);
    if (list !== 'undefined' && list !== null) {
      this.list[index] = JSON.parse(list);
    }
    if (index>=this.list.length) this.list.push([]);
    return(this.list[index]);
  }

  saveList(listIndex) {
    localStorage.setItem('todo-list-'+listIndex, JSON.stringify(this.list[listIndex]));
  }

  getItem(listIndex, itemIndex) {
    return(this.list[listIndex][itemIndex]);
  }

  setItem(listIndex, item, itemIndex?) {
    if (itemIndex == undefined) this.list[listIndex].push(Object.assign({}, item));
    else this.list[listIndex][itemIndex] = Object.assign({}, item);
    this.saveList(listIndex);  
  }

  deleteItem(listIndex, itemIndex) {
    this.list[listIndex].splice(itemIndex,1);
    this.saveList(listIndex);
  }

  deleteList(listIndex) {
    this.list[listIndex].length = 0;
    this.saveList(listIndex);    
  }

  moveItem(listIndex, from, to) {
    let element = this.list[listIndex][from];
    this.list[listIndex].splice(from, 1);
    this.list[listIndex].splice(to, 0, element);
    this.saveList(listIndex);    
  }
}

La pantalla principal

En el fichero home.page.ts desarrollaremos el código de una clase que contendrá las variables y las funciones necesarias para manipular las listas y las tareas que se visualizan en la pantalla principal:

  1. constructor(ListService, AlertController): En el constructor inicializaremos el array de objetos con las propiedades que definen las diferentes listas (etiqueta, icono y lista de tareas). Si ya existen tareas al iniciar la aplicación, se leerán para que aparezcan en pantalla.
  2. ionViewDidEnter(): Al arrancar la aplicación, se activará la primera pestaña.
  3. toggleReorder(): Para activar o desactivar el botón de los elementos para reordenarlos.
  4. setTab(tabIndex): Activará la pestaña con el índice indicado.
  5. deleteItem(item?): Después de pedir confirmación, borrará todos los elementos de la lista de tareas activa, o sólo el elemento indicado, si éste se ha pasado por parámetro.
  6. moveItem(indexes): Recibe un objeto con la posición inicial de una tarea en la lista y la posición final donde se debe mover.

Para implementar la barra superior de la pantalla principal, ahora utilizaremos el siguiente código:

<ion-title>ToDo!</ion-title>
<ion-buttons slot="primary">
  <ion-button (click)="deleteItem()" color="danger"><ion-icon slot="icon-only" name="trash"></ion-icon></ion-button>  
  <ion-button (click)="toggleReorder()"><ion-icon slot="icon-only" name="reorder"></ion-icon></ion-button>
  <ion-button [routerLink]="['/AddEditItem', { tab:tabIndex, item:-1 }]"><ion-icon slot="icon-only" name="add"></ion-icon></ion-button>
</ion-buttons>

Ya podemos observar algunas diferencias respecto al código JavaScript. En primer lugar, en vez de utilizar el atributo del evento onclick utilizaremos la sintaxis (click) (más detalles en la documentación de Angular). En segundo lugar, observamos una gran diferencia respecto a la opción de añadir un nuevo elemento. En este caso vamos a diseñar una nueva página (a la que accederemos con la url /AddEditItem) en vez de abrir un cuadro de diálogo modal. La sintaxis de Angular utilizada en este caso, nos permite pasar información ({ tab:tabIndex, item:-1 }) de una página a otra, de forma que sepamos a qué lista debemos añadir la nueva tarea, colocando además el índice del item a -1 para indicar que no queremos modificar ninguna tarea existente, sino añadir una nueva:

...
<ion-button [routerLink]="['/AddEditItem', { tab:tabIndex, item:-1 }]"><ion-icon slot="icon-only" name="add"></ion-icon></ion-button>
...

Para mostrar las diferentes pestañas (tabs) utilizaremos el siguiente código:

<ion-tab *ngFor="let tab of tabs; let i=index" [label]="tab.label" [icon]="tab.icon" (ionSelect)="setTab(i)">
  <ion-list lines="full">
    <ion-reorder-group [disabled]="!reorder" (ionItemReorder)="moveItem($event.detail)">
      <ion-item-sliding *ngFor="let item of tabs[i].list; let j=index">
        <ion-item color="danger" [routerLink]="['/AddEditItem', { tab:i, item:j }]">
          <ion-label text-wrap>
            <h2>{{item.task}}</h2>
            <p>{{item.date}}</p>
          </ion-label>
          <ion-icon slot="end" [name]="item.icon"></ion-icon>
          <ion-reorder slot="end"></ion-reorder>
        </ion-item>
        <ion-item-options side="start">
          <ion-item-option color="danger" (click)="deleteItem(j)">
            <ion-icon slot="icon-only" name="trash"></ion-icon>
          </ion-item-option>
        </ion-item-options>
      </ion-item-sliding>
    </ion-reorder-group>
  </ion-list>
</ion-tab>

Respecto a la versión con JavaScript, la mayor diferencia la observamos en la siguiente línea:

<ion-tab *ngFor="let tab of tabs; let i=index" [label]="tab.label" [icon]="tab.icon" (ionSelect)="setTab(i)">

Utilizando Angular podemos insertar bucles dentro de nuestro código HTML que nos permitirán mostrar muy fácilmente varios elementos que tengan las mismas propiedades (consultar la documentación de angular para más detalles). De esta forma tendremos el mismo código independientemente de la cantidad de listas de tareas que vayamos a utilizar. Bastará con especificar en el constructor las etiquetas y los iconos de cada pestaña utilizando un simple array:

constructor(...) {
  this.tabs = [
    {label: 'School', icon: 'school', list: []},
    {label: 'Home', icon: 'home', list: []}
  ];
  ...
}

Para gestionar el botón de reordenar, también mejora mucho la sintaxis respecto a JavaScript:

<ion-reorder-group [disabled]="!reorder" (ionItemReorder)="moveItem($event.detail)">
...
</ion-reorder-group>

En este caso, la propiedad disabled se encierra entre corchetes para unirla a la variable reorder, y el evento ionItemReorder se encierra entre paréntesis.

El fichero «home.page.html» completo

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>ToDo!</ion-title>
    <ion-buttons slot="primary">
      <ion-button (click)="deleteItem()" color="danger"><ion-icon slot="icon-only" name="trash"></ion-icon></ion-button>  
      <ion-button (click)="toggleReorder()"><ion-icon slot="icon-only" name="reorder"></ion-icon></ion-button>
      <ion-button [routerLink]="['/AddEditItem', { tab:tabIndex, item:-1 }]"><ion-icon slot="icon-only" name="add"></ion-icon></ion-button>
    </ion-buttons>
  </ion-toolbar>       
</ion-header>

<ion-content>
  <ion-tabs #myTabs color="primary">
    <ion-tab *ngFor="let tab of tabs; let i=index" [label]="tab.label" [icon]="tab.icon" (ionSelect)="setTab(i)">
      <ion-list #myList lines="full">
        <ion-reorder-group [disabled]="!reorder" (ionItemReorder)="moveItem($event.detail)">
          <ion-item-sliding *ngFor="let item of tabs[i].list; let j=index">
            <ion-item [routerLink]="['/AddEditItem', { tab:i, item:j }]">
              <ion-label text-wrap>
                <h2>{{item.task}}</h2>
                <p>{{item.date}}</p>
              </ion-label>
              <ion-icon slot="end" [name]="item.icon"></ion-icon>
              <ion-reorder slot="end"></ion-reorder>
            </ion-item>
            <ion-item-options side="start">
              <ion-item-option color="danger" (click)="deleteItem(j)">
                <ion-icon slot="icon-only" name="trash"></ion-icon>
              </ion-item-option>
            </ion-item-options>
          </ion-item-sliding>
        </ion-reorder-group>
      </ion-list>
    </ion-tab>
  </ion-tabs>
</ion-content>

El fichero «home.page.ts» completo

import { Component, ViewChild } from '@angular/core';
import { Tabs, List, AlertController } from '@ionic/angular';
import { ListService } from '../list.service';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})

export class HomePage {

  @ViewChild('myTabs') tabRef: Tabs;
  @ViewChild('myList') listRef: List;
  tabs: any;
  tabIndex: number;
  reorder: boolean;

  constructor(private listService: ListService,
              private alertController: AlertController){
    this.tabs = [
      {label: 'School', icon: 'school', list: []},
      {label: 'Home', icon: 'home', list: []}
    ];
    this.tabs.forEach((tab, index) => {
      tab.list = this.listService.getList(index);
    });
    this.tabIndex = 0;
    this.reorder = false;
  }

  ionViewDidEnter() {
    this.tabRef.select(this.tabIndex);
  }

  toggleReorder() {
    this.reorder = !this.reorder;
  }

  setTab(tabIndex) {
    this.tabIndex = tabIndex;
  } 

  async deleteItem(item?) {
    const alert = await this.alertController.create({
      header: item === undefined ? 'Delete all' : 'Delete item',
      message: 'Are you sure?',
      buttons: [
        {
          text: 'OK',
          handler: () => {
            this.listRef.closeSlidingItems();
            if (item === undefined) {
              this.listService.deleteList(this.tabIndex);
            }
            else {
              this.listService.deleteItem(this.tabIndex, item);              
            }
          }
        },       
        {
          text: 'CANCEL',
          role: 'cancel'
        }
      ]
    });

    await alert.present();
  }

  moveItem(indexes) {
    this.listService.moveItem(this.tabIndex, indexes.from, indexes.to);
  }

}

El formulario para añadir o editar tareas

En el fichero add-edit-item-page.ts desarrollaremos el código de una clase que contendrá las variables y las funciones necesarias para editar o crear una tarea:

  1. constructor(Router, ActivatedRoute, AlertController, ListService): En el constructor inicializaremos por un lado un array con los nombres de los iconos para indicar las prioridades de las tareas, y a continuación recogeremos los parámetros proporcionados desde la pantalla principal (índices de la lista y elemento a modificar, si es el caso).
  2. error(message): Para mostrar mensajes de error si por ejemplo no se ha especificado ningún texto en la descripción de la tarea y pulsamos el botón de confirmar.
  3. save(): Comprobará si desde la pantalla principal se ha elegido una tarea a modificar o si por el contrario se ha pulsado el botón de nueva tarea, y llamará a la función del servicio, volviendo a la pantalla principal a continuación.

La barra superior de esta pantalla sólo contendrá un título y dos botones, uno para cancelar y otro para confirmar. Ambos nos llevarán de nuevo a la pantalla principal, pero si pulsamos el botón de confirmar, previamente se grabarán los datos introducidos:

<ion-title>ToDo</ion-title>
<ion-buttons slot="primary">
    <ion-button color="danger" href="/home"><ion-icon slot="icon-only" name="close"></ion-icon></ion-button>
    <ion-button color="primary" (click)="save()"><ion-icon slot="icon-only" name="checkmark"></ion-icon></ion-button>
</ion-buttons>

El formulario para introducir los datos de una nueva tarea o modificar los de una existente es muy sencillo:

<ion-list>
  <ion-item>
    <ion-label position="floating">Select date</ion-label>
    <ion-datetime display-format="D MMM YYYY" max="2050-12-31" [(ngModel)]="item.date"></ion-datetime>      
  </ion-item>
  <ion-item>
    <ion-label position="floating">Enter task</ion-label>
    <ion-input [(ngModel)]="item.task"></ion-input>
  </ion-item>
</ion-list>
<ion-segment [(ngModel)]="item.icon">
  <ion-segment-button *ngFor="let button of buttons" [value]="button">
    <ion-icon [name]="button"></ion-icon>
  </ion-segment-button>
</ion-segment>

La novedad a destacar es el uso de [(ngModel)], una de las indudables ventajas que nos ofrece Angular, ya que nos permite enlazar un atributo de la clase con un campo del formulario, de forma que las modificaciones que se produzcan se queden reflejadas en los dos sentidos: el valor que tenga dicho atributo en el código TypeScript se mostrará en el código HTML, y cualquier modificación que hagamos en el formulario, también cambiará el valor del atributo de la clase. Se puede consultar la documentación oficial para más detalles.

Por otro lado, también estamos implementando un bucle para mostrar los posibles iconos indicativos de la prioridad de la tarea, lo que sin duda nos ahorra código HTML:

<ion-segment-button *ngFor="let button of buttons" [value]="button">
    <ion-icon [name]="button"></ion-icon>
</ion-segment-button>

Al igual que en el constructor de la página principal, bastará con especificar mediante un array todos los botones que deseemos visualizar:

constructor(...) { 
  this.buttons = ["radio-button-off", "radio-button-on", "snow", "flame"];
  ...
}

El fichero «add-edit-item.page.html» completo

<ion-header>
  <ion-toolbar>
    <ion-title>ToDo</ion-title>
    <ion-buttons slot="primary">
        <ion-button color="danger" href="/home"><ion-icon slot="icon-only" name="close"></ion-icon></ion-button>
        <ion-button color="primary" (click)="save()"><ion-icon slot="icon-only" name="checkmark"></ion-icon></ion-button>
    </ion-buttons>       
  </ion-toolbar>
</ion-header>
<ion-content>
  <ion-list>
    <ion-item>
      <ion-label position="floating">Select date</ion-label>
      <ion-datetime display-format="D MMM YYYY" max="2050-12-31" [(ngModel)]="item.date"></ion-datetime>      
    </ion-item>
    <ion-item>
      <ion-label position="floating">Enter task</ion-label>
      <ion-input [(ngModel)]="item.task"></ion-input>
    </ion-item>
  </ion-list>
  <ion-segment [(ngModel)]="item.icon">
    <ion-segment-button *ngFor="let button of buttons" [value]="button">
      <ion-icon [name]="button"></ion-icon>
    </ion-segment-button>
  </ion-segment>
</ion-content>

El fichero «add-edit-item.page.ts» completo

import { Component } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { AlertController } from '@ionic/angular';
import { ListService } from '../list.service';

@Component({
  selector: 'app-add-edit-item',
  templateUrl: './add-edit-item.page.html',
  styleUrls: ['./add-edit-item.page.scss'],
})

export class AddEditItemPage {

  item: any;
  tabIndex: number;
  itemIndex: number;
  buttons: Array<string>;

  constructor(private router: Router,
              private route: ActivatedRoute,
              public alertController: AlertController,
              private ListService: ListService) { 
    this.buttons = ["radio-button-off", "radio-button-on", "snow", "flame"];

    this.tabIndex = +this.route.snapshot.paramMap.get('tab');
    this.itemIndex = +this.route.snapshot.paramMap.get('item'); 
    if (this.itemIndex >= 0) {
      this.item = Object.assign({}, this.ListService.getItem(this.tabIndex, this.itemIndex));
      this.item.date = new Date(this.item.date).toISOString();
    }
    else {
      this.item = { date: new Date().toISOString(), task: '', icon: 'radio-button-off'};
    } 
  }

  async error(message) {
    const alert = await this.alertController.create({
      message: message,
      buttons: ['OK']
    });

    await alert.present();
  }

  save() {
    if (!this.item.task.length) {
      this.error('The task cannot be empty');
    }
    else {
      this.item.date = document.querySelector('.datetime-text').innerHTML;
      if (this.itemIndex >= 0) {
        this.ListService.setItem(this.tabIndex, this.item, this.itemIndex);
      }
      else {
        this.ListService.setItem(this.tabIndex, this.item);      
      }
      this.router.navigate(['/home']);
    }
  }

}

Generando el archivo APK

Desde el directorio principal, bastará con ejecutar los siguiente comandos:

ionic cordova platform add android
ionic build --engine cordova --platform android --prod
cordova build android

Si la aplicación se ha compilado correctamente, al final nos indicará la ruta completa donde se encuentra el archivo APK.

Además, también podemos lanzar la ejecución de la aplicación directamente en el emulador (si lo tenemos instalado):

cordova emulate android

o en el móvil (si lo tenemos conectado a nuestro ordenador y se encuentra configurado correctamente):

cordova run android --device

Icono y pantalla de bienvenida

Otra de las ventajas de utilizar la consola de IONIC es la facilidad en el proceso de generar los archivos necesarios para utilizar un icono y una pantalla de bienvenida elegidos por nosotros.

Una vez dispongamos de los respectivos archivos icon.png y splash.png, bastará con colocarlos en la carpeta resources de nuestro proyecto (sobrescribiendo los que allí estuvieran, ya que normalmente IONIC nos habrá proporcionado unos por defecto). Después  un simple comando generará los archivos necesarios:

ionic cordova resources

De esta forma, el cliente de IONIC automáticamente enviará los dos ficheros a sus servidores, generará las imágenes con todas las resoluciones necesarias para utilizar la aplicación en cualquier dispositivo, y los escribirá en las rutas específicas. Si todo va bien, nos mostrará por pantalla algo del estilo:

Fernandos-MacBook-Pro:tareas fernandoruizrico$ ionic cordova resources
✔ Collecting resource configuration and source images - done!
✔ Filtering out image resources that do not need regeneration - done!
✔ Uploading source images to prepare for transformations: 2 / 2 complete - done!
✔ Generating platform resources: 18 / 18 complete - done!
✔ Modifying config.xml to add new image resources - done!

A continuación bastará con compilar la aplicación de nuevo para hacer efectivos los cambios:

ionic build --engine cordova --platform android --prod
cordova build android

El resultado

El resultado de esta aplicación supera obviamente al de la anterior, ya que ahora no sólo hemos mejorado a nivel de código, sino que hemos agilizado el proceso de desarrollo y actualización, obteniendo además una app que no requiere Internet, ya que ahora IONIC no se encuentra enlazado directamente como librería, sino empaquetado dentro de la propia aplicación.

Puedes hacer clic aquí para observar que el mismo código de la app generada también puede funcionar perfectamente en el navegador como una página web.