Menú lateral con opción de compartir con IONIC 4+JavaScript

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 content-id="content">
  <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 onclick="toggleReorder()">
          <ion-label>Enable/Disable reorder</ion-label>
          <ion-icon slot="end" name="reorder"></ion-icon>
        </ion-item>
        <ion-item onclick="share()">
          <ion-label>Share</ion-label>
          <ion-icon slot="end" name="share"></ion-icon>
        </ion-item>
        ...
      </ion-list>
    </ion-menu-toggle>
  </ion-content>
</ion-menu>

<div class="ion-page" id="content">
  <ion-header>
    ...
      <ion-menu-button></ion-menu-button>
    ...
  </ion-header>
  <ion-content>
    ...
  </ion-content>
</div>

Observaremos que el elemento <ion-menu></ion-menu> por defecto creará un menú que aparece desde la izquierda de la pantalla actual, tal como se especifica en la documentación de IONIC. Se podrá ocultar pulsando de nuevo el 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.

El fichero «index.html» completo

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>ToDo!</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">

  <script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.esm.js"></script>
  <script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/css/ionic.bundle.css"/>

  <script src="cordova.js"></script>
  <script src="todo.js"></script>
</head>

<body onload="onLoad()">
  <ion-app>
    <ion-menu content-id="content">
      <ion-header>
        <ion-toolbar mode="ios" color="danger">
          <ion-buttons slot="start">
            <ion-menu-button></ion-menu-button>
          </ion-buttons>  
          <ion-title>Menu</ion-title>
        </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 onclick="toggleReorder()">
              <ion-label>Enable/Disable reorder</ion-label>
              <ion-icon slot="end" name="reorder"></ion-icon>
            </ion-item>
            <ion-item onclick="share()">
              <ion-label>Share</ion-label>
              <ion-icon slot="end" name="share"></ion-icon>
            </ion-item>
            <ion-item-divider>
              <ion-label>Delete</ion-label>
            </ion-item-divider>          
            <ion-item onclick="deleteItem()">
              <ion-label>Delete all</ion-label>
              <ion-icon slot="end" name="trash" color="danger"></ion-icon>
            </ion-item>
          </ion-list>
        </ion-menu-toggle>
      </ion-content>
    </ion-menu>

    <div class="ion-page" id="content">
      <ion-header>
        <ion-toolbar mode="ios" color="primary">
          <ion-buttons slot="start">
            <ion-menu-button></ion-menu-button>
          </ion-buttons>          
          <ion-title>ToDo!</ion-title>
          <ion-buttons slot="end">
            <ion-button onclick="addEditItem()">
              <ion-icon name="add" slot="icon-only"></ion-icon>
            </ion-button>
          </ion-buttons>            
        </ion-toolbar>
      </ion-header>
      <ion-content>
        <ion-tabs>
          <ion-tab tab="school">
            <ion-list lines="full">
              <ion-reorder-group></ion-reorder-group>
            </ion-list>
          </ion-tab>
          <ion-tab tab="home">
            <ion-list lines="full">
              <ion-reorder-group></ion-reorder-group>
            </ion-list>
          </ion-tab>
          <ion-tab-bar slot="bottom" color="primary">
            <ion-tab-button tab="school">
              <ion-icon name="school"></ion-icon>
            </ion-tab-button>
            <ion-tab-button tab="home">
              <ion-icon name="home"></ion-icon>
            </ion-tab-button>
          </ion-tab-bar>
        </ion-tabs>
      </ion-content>
    </div>
  </ion-app>
</body>
</html>

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 incluir en nuestra aplicación el plugin necesario para acceder a dicha funcionalidad, bastará con añadir la siguiente línea al archivo config.xml:

<plugin name="cordova-plugin-x-socialsharing" spec="5.4.0" />

A continuación modificaremos el código fuente del archivo todo.js para añadir la función share(), que creará una cadena de texto y se la proporcionará al plugin de compartir para que se pueda enviar utilizando otra aplicación.

Bastará con crear la función share() con el código necesario para obtener en una única cadena de texto todas las tareas una detrás de otra, separadas por saltos de línea. Después sólo necesitamos llamar a la función socialsharing.share() del plugin para activar y mostrar el menú de compartir de nuestros móviles:

function share() {
    let text = "";
    getList().forEach((task, index) => {
      text += (index+1) + ". " + task.text + " - " + task.date.slice(0,10) + "\n";
    });
    window.plugins.socialsharing.share(text);
}

El archivo «config.xml» completo

<widget xmlns   = "http://www.w3.org/ns/widgets"
    xmlns:gap   = "http://phonegap.com/ns/1.0"
    id          = "com.fernandoruizrico.todo"
    versionCode = "1"
    version     = "0.0.1" >

    <name>ToDo!</name>

    <description>
        ToDo List.
    </description>

    <author href="https://fernandoruizrico.com" email="[email protected]">
        Fernando Ruiz Rico
    </author>

    <access origin="*" />
    <plugin name="cordova-plugin-whitelist" />
    <plugin name="cordova-plugin-vibration" />
    <plugin name="cordova-plugin-x-socialsharing" spec="5.4.0" />
</widget>

El archivo «todo.js» completo

function onLoad() { 
    document.addEventListener("ionModalDidDismiss", closeItems);
    document.addEventListener("ionAlertDidDismiss", closeItems);
    document.addEventListener("ionDidOpen", closeItems);
    document.addEventListener('ionItemReorder', (event) => { moveItem(event.detail); });
    document.querySelectorAll('ion-tab').forEach(function(t) { printList(t); });
}

function getTab() {
    return(document.querySelector('ion-tab:not(.tab-hidden)'));
}

function getList(tab = getTab()) {
    let list = localStorage.getItem('todo-list-'+tab.tab);
    return list ? JSON.parse(list) : [];
}

function saveList(tab, list) {
    localStorage.setItem('todo-list-'+tab.tab, JSON.stringify(list));
    printList(tab);
}

function printList(tab) {
    tab.querySelector('ion-reorder-group').innerHTML = "";

    getList(tab).forEach((item, index) => {
        tab.querySelector('ion-reorder-group').innerHTML +=
        `<ion-item-sliding>
           <ion-item onClick="addEditItem(`+index+`)">
             <ion-label text-wrap>
               <h2>`+item.text+`</h2>
               <p>`+item.date.slice(0,10)+`</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" onClick="deleteItem(`+index+`)">
                <ion-icon slot="icon-only" name="trash"></ion-icon>
            </ion-item-option>
          </ion-item-options>
        </ion-item-sliding>`;
    });    
}

function error(message) {
  const alert = document.createElement('ion-alert');
  alert.message = message;
  alert.buttons = ['OK'];

  document.querySelector('ion-app').appendChild(alert);
  alert.present();
}

function toggleReorder() {
    closeItems();
    let reorder = getTab().querySelector('ion-reorder-group');
    reorder.disabled = !reorder.disabled;
}

function closeItems() {
    getTab().querySelector('ion-list').closeSlidingItems();
}

function addEditItem(index = false) {
    closeItems();
    let list = getList();
    let item = null;

    if (index !== false) item = list[index];
    else item = { text:"", date:new Date().toISOString(), icon:"radio-button-off" };

    const modal = document.createElement('ion-modal');
    modal.component = document.createElement('div');
    modal.component.innerHTML = `
        <ion-header>
            <ion-toolbar>
                <ion-title>ToDo - `+(index !== false ? 'Edit task' : 'New task')+`</ion-title>
                <ion-buttons slot="primary">
                    <ion-button color="danger"><ion-icon slot="icon-only" name="close"></ion-icon></ion-button>
                    <ion-button color="primary"><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" value="`+item.date+`"></ion-datetime>            
                </ion-item>
                <ion-item>
                    <ion-label position="floating">Enter task</ion-label>
                    <ion-input value="`+item.text+`"></ion-input>
                </ion-item>
            </ion-list>
            <ion-segment value="`+item.icon+`">
                <ion-segment-button value="radio-button-off">
                    <ion-icon name="radio-button-off"></ion-icon>
                </ion-segment-button>  
                <ion-segment-button value="radio-button-on">
                    <ion-icon name="radio-button-on"></ion-icon>
                </ion-segment-button>  
                <ion-segment-button value="snow">
                    <ion-icon name="snow"></ion-icon>
                </ion-segment-button>
                <ion-segment-button value="flame">
                    <ion-icon name="flame"></ion-icon>
                </ion-segment-button>
            </ion-segment>
        </ion-content>`;

    modal.component.querySelector('[color="danger"]').addEventListener('click', () => {
        modal.dismiss();
    });

    modal.component.querySelector('[color="primary"]').addEventListener('click', () => {
        let newDate = modal.component.querySelector('ion-datetime').value;
        let newText = modal.component.querySelector('ion-input').value;
        let newIcon = modal.component.querySelector('ion-segment').value;

        if (!newText.length) {
            error('The task cannot be empty');
        }
        else {
            let newItem = { text:newText, date:newDate, icon:newIcon };
            if (index !== false) list[index] = newItem; 
            else list.unshift(newItem);
            saveList(getTab(), list);
            modal.dismiss();
        }
    });

    document.querySelector('ion-app').appendChild(modal);
    modal.present();
}

function moveItem(indexes) {
    let tab = getTab();
    let list = getList(tab);
    let item = list[indexes.from];
    list.splice(indexes.from, 1);
    list.splice(indexes.to, 0, item);
    indexes.complete();
    saveList(tab, list);
}

function deleteItem(index = false) {
    const alert = document.createElement('ion-alert');
  
    alert.header = index !== false ? 'Delete item' : 'Delete all',
    alert.message = 'Are you sure?',
    alert.buttons =  
            [{
                text: 'YES',
                handler: () => {
                    let list = getList();
                    if (index !== false) { list.splice(index, 1); }
                    else { list.length = 0; }
                    saveList(getTab(), list);
                }
            }, {
                text: 'NO',
                role: 'cancel'
            }]

    document.querySelector('ion-app').appendChild(alert);
    alert.present();
}

function share() {
    let text = "";
    getList().forEach((task, index) => {
      text += (index+1) + ". " + task.text + " - " + task.date.slice(0,10) + "\n";
    });
    window.plugins.socialsharing.share(text);
}

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+Javascript

El objetivo

En este ejercicio vamos a desarrollar una aplicación para gestionar varias listas de tareas.

La funcionalidad a implementar será la siguiente:

  • Gestionar dos listas de tareas independientes.
  • Añadir tareas indicando la fecha, la descripción de la tarea, y un icono identificativo de la prioridad.
  • 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.
  • Visualizar y editar los detalles de cada tarea haciendo clic sobre el elemento correspondiente de la lista.
  • Permitir ordenar las tareas arrastrando cualquier elemento a una nueva posición.

Elementos HTML de IONIC 4

El encabezado principal de la aplicación

Utilizaremos tres botones de tipo icono para acceder a las funciones básicas desde la pantalla principal:

  • <ion-icon name="trash"></ion-icon>: Para borrar todas las tareas de la lista seleccionada.
  • <ion-icon name="reorder"></ion-icon>: Para activar los botones que nos permitirán reordenar las tareas arrastrando y soltando.
  • <ion-icon name="add"></ion-icon>: Para crear una nueva tarea.

Los agruparemos junto con el título de la aplicación en el encabezado del fichero index.html de la siguiente forma:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>ToDo!</ion-title>
    <ion-buttons slot="primary">
      <ion-button onclick="deleteItem()" color="danger">
        <ion-icon slot="icon-only" name="trash"></ion-icon>
      </ion-button>  
      <ion-button onclick="toggleReorder()">
        <ion-icon slot="icon-only" name="reorder"></ion-icon>
      </ion-button>
      <ion-button onclick="addEditItem()">
        <ion-icon slot="icon-only" name="add"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>       
</ion-header>

Más adelante veremos el código JavaScript necesario para llevar a cabo las funciones correspondientes:

  • deleteItem()
  • toggleReorder()
  • addEditItem()

Las listas de tareas

Para mantener dos listas separadas, desde el archivo index.html utilizaremos el elemento <ion-tabs></ion-tabs>  que personalizaremos escogiendo un icono y un texto descriptivo. Por ejemplo, si quisiéramos utilizar una lista de tareas para el instituto y otra para casa podríamos utilizar los iconos school y home respectivamente:

<ion-tabs color="primary">
  <ion-tab tab="school">
    <ion-list lines="full">
      <ion-reorder-group>
        ...
      </ion-reorder-group>
    </ion-list>
  </ion-tab>
  <ion-tab tab="home">
    <ion-list lines="full">
      <ion-reorder-group>
        ...
      </ion-reorder-group>
    </ion-list>
  </ion-tab>
  <ion-tab-bar slot="bottom" color="primary">
    <ion-tab-button tab="home"><ion-icon name="home"></ion-icon></ion-tab-button>
    <ion-tab-button tab="school"><ion-icon name="school"></ion-icon></ion-tab-button>
  </ion-tab-bar>
</ion-tabs>

Además utilizaremos el nuevo elemento <ion-reorder-group></ion-reorder-group> que nos permitirá reordenar las tareas que coloquemos dentro. Bastará con pulsar encima de un botón habilitado a tal efecto, y sin soltar arrastrar la tarea en cuestión a la nueva posición. En la documentación de IONIC podemos observar cómo esta simple etiqueta gestiona todo el proceso de arrastrar y soltar.

Las tareas dentro de las listas

Utilizaremos elementos <ion-item-sliding></ion-item-sliding> para cada tarea dentro de las listas. De esta forma podremos tener oculto un botón rojo con una papelera, que aparecerá al deslizar la tarea hacia la derecha, y nos permitirá borrar de la lista sólo ese elemento:

<ion-item-sliding>
  ...
  <ion-item-option color="danger" onclick="...">
    <ion-icon slot="icon-only" name="trash"></ion-icon>
  </ion-item-option>
</ion-item-sliding>

Utilizaremos también un elemento <ion-item></ion-item> para agrupar todos los datos de la tarea. De esta forma, bastará con hacer clic sobre una determinada tarea para acceder a los detalles de la misma: <ion-item onclick="..."></ion-item> . Un poco más adelante detallaremos la funcionalidad JavaScript necesaria para crear y mostrar un formulario que nos permita visualizar y editar la tarea seleccionada.

Resaltaremos la descripción de la tarea con una etiqueta <h2></h2>, y a continuación mostremos la fecha de la misma dentro de un párrafo (etiqueta <p></p>). Encerraremos ambos elementos con una etiqueta <ion-label text-wrap></ion-label> que permitirá que el tamaño de cada elemento de la lista se ajuste para contener todo el texto descriptivo de la tarea.

Resumiendo, cada elemento de la lista de tareas vendrá especificado por el siguiente código HTML:

<ion-item-sliding>
  <ion-item onclick="...">
    <ion-label text-wrap>
      <h2>...</h2>
      <p>...</p>
    </ion-label>
    <ion-icon slot="end" name="..."></ion-icon>
    <ion-reorder slot="end"></ion-reorder>
  </ion-item>
  <ion-item-options side="start">
    <ion-item-option color="danger" onclick="...">
      <ion-icon slot="icon-only" name="trash"></ion-icon>
    </ion-item-option>
  </ion-item-options>
</ion-item-sliding>

El formulario con los detalles de cada tarea

Para visualizar y editar los detalles de cada tarea (al crear una nueva, o al hacer clic sobre una ya existente) utilizaremos el siguiente código HTML:

<ion-header>
  <ion-toolbar>
    <ion-title>ToDo - Task details</ion-title>
    <ion-buttons slot="primary">
      <ion-button color="danger"><ion-icon slot="icon-only" name="close"></ion-icon></ion-button>
      <ion-button color="primary"><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" value="..."></ion-datetime>            
    </ion-item>
    <ion-item>
      <ion-label position="floating">Enter task</ion-label>
      <ion-input value="..."></ion-input>
    </ion-item>
  </ion-list>

  <ion-segment value="...">
    <ion-segment-button value="radio-button-off">
      <ion-icon name="radio-button-off"></ion-icon>
    </ion-segment-button>  
    <ion-segment-button value="radio-button-on">
      <ion-icon name="radio-button-on"></ion-icon>
    </ion-segment-button>  
    <ion-segment-button value="snow">
      <ion-icon name="snow"></ion-icon>
    </ion-segment-button>
    <ion-segment-button value="flame">
      <ion-icon name="flame"></ion-icon>
    </ion-segment-button>
  </ion-segment>
</ion-content>

En el encabezado (<ion-header></ion-header>) simplemente tenemos dos botones para confirmar o cancelar la edición o creación de la tarea.

En el contenido principal del formulario (<ion-content></ion-content>)  tendremos tres campos:

  • <ion-datetime><ion-datetime>: Para indicar la fecha de la tarea. Si estamos creando una nueva tarea, en dicho valor introduciremos la fecha actual utilizando código JavaScript.
  • <ion-input></ion-input>: Para introducir o modificar la descripción de la tarea. Estará en blanco si se trata de una tarea nueva.
  • <ion-segment></ion-segment>: Para seleccionar la prioridad de la tarea. En nuestro ejemplo proponemos 4 opciones (radio-button-off para tareas pendientes, radio-button-on para tarea finalizada, snow para una prioridad baja, y flame para una prioridad alta), aunque los iconos utilizados son una simple sugerencia, y se pueden cambiar fácilmente según el gusto de cada uno.

El código JavaScript

Accediendo a cada una de las listas

Por simplificar un poco el código, utilizaremos tres funciones para acceder y actualizar los dos elementos principales (tabslists):

function getTab() {
    return(document.querySelector('ion-tab:not(.tab-hidden)'));
}

function getList(tab = getTab()) {
    let list = localStorage.getItem('todo-list-'+tab.tab);
    return list ? JSON.parse(list) : [];
}

function saveList(tab, list) {
    localStorage.setItem('todo-list-'+tab.tab, JSON.stringify(list));
    printList(tab);
}

La última función (saveList()) nos permitirá guardar de manera persistente cada una de las listas de tareas, utilizando localStorage, de forma que al actualizar el contenido del navegador (o cerrar la app y volverla a abrir), podamos volver a cargar la lista de tareas.

Construyendo cada elemento de la lista de tareas

Mediante la siguiente función obtendremos todo el código necesario para cada tarea, variando simplemente el texto descriptivo de la misma, la fecha, y el icono indicativo de la prioridad:

function printList(tab) {
    tab.querySelector('ion-reorder-group').innerHTML = "";

    getList(tab).forEach((item, index) => {
        tab.querySelector('ion-reorder-group').innerHTML +=
        `<ion-item-sliding>
           <ion-item onClick="addEditItem(`+index+`)">
             <ion-label text-wrap>
               <h2>`+item.text+`</h2>
               <p>`+item.date.slice(0,10)+`</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" onClick="deleteItem(`+index+`)">
                <ion-icon slot="icon-only" name="trash"></ion-icon>
            </ion-item-option>
          </ion-item-options>
        </ion-item-sliding>`;
    });    
}

Para añadir una nueva tarea o editar los detalles de una tarea existente

Utilizaremos la misma función (addEditItem(index)) para añadir una nueva tarea o modificar una existente. En ella, siguiendo las instrucciones indicadas en la documentación de IONIC, crearemos un cuadro de diálogo modal que mostrará el formulario para introducir o actualizar los detalles de la tarea:

function addEditItem(index = false) {
    closeItems();
    let list = getList();
    let item = null;

    if (index !== false) item = list[index];
    else item = { text:"", date:new Date().toISOString(), icon:"radio-button-off" };

    const modal = document.createElement('ion-modal');
    modal.component = document.createElement('div');
    modal.component.innerHTML = `
        <ion-header>
            ...
        </ion-header>
        <ion-content>
            ...
        </ion-content>`;
    
 modal.component.querySelector('[color="danger"]').addEventListener('click', () => {
        modal.dismiss();
    });

    modal.component.querySelector('[color="primary"]').addEventListener('click', () => {
        let newDate = modal.component.querySelector('ion-datetime').value;
        let newText = modal.component.querySelector('ion-input').value;
        let newIcon = modal.component.querySelector('ion-segment').value;

        if (!newText.length) {
            error('The task cannot be empty');
        }
        else {
            let newItem = { text:newText, date:newDate, icon:newIcon };
            if (index !== false) list[index] = newItem; 
            else list.unshift(newItem);
            saveList(getTab(), list);
            modal.dismiss();
        }
    });

    document.querySelector('ion-app').appendChild(modal);
    modal.present();
}

En la primera parte de la función, si recibimos como parámetro la tarea a modificar, recuperaremos los detalles de la misma (fecha, texto descriptivo e icono con la prioridad):

if (index !== false) item = list[index];
else item = { text:"", date:new Date().toISOString(), icon:"radio-button-off" };

Como se puede observar en la fecha, si en vez de modificar una tarea existente, estamos creando una nueva (parámetro de la función con valor false), entonces obtendremos la fecha actual.

A continuación, crearemos el formulario de edición o creación de tareas, con un encabezado con los botones correspondientes para cancelar y confirmar, y también con el contenido principal, utilizando el código HTML indicado previamente:

const modal = document.createElement('ion-modal');
modal.component = document.createElement('div');
modal.component.innerHTML = `
    <ion-header>
      ...
    </ion-header>
    <ion-content>
      ...
    </ion-content>`;

Finalmente, nos queda capturar los eventos de clic en el botón de cancelar (color="danger") o confirmar (color="primary"). En el primer caso simplemente cerraremos el cuadro de diálogo sin hacer nada más:

modal.component.querySelector('[color="danger"]').addEventListener('click', () => {
        modal.dismiss();
    });

En caso de confirmar la edición o creación de una nueva tarea, primero cogeremos los valores introducidos en el formulario:

modal.component.querySelector('[color="primary"]').addEventListener('click', () => {
        let newDate = modal.component.querySelector('ion-datetime').value;
        let newText = modal.component.querySelector('ion-input').value;
        let newIcon = modal.component.querySelector('ion-segment').value;

    ...
});

En segundo lugar comprobaremos la longitud de la descripción de la tarea, para asegurarnos que no está vacía, en cuyo caso mostraríamos un error:

function error(message) {
  const alert = document.createElement('ion-alert');
  alert.message = message;
  alert.buttons = ['OK'];

  document.querySelector('ion-app').appendChild(alert);
  alert.present();
}

function addEditItem(index = false) {
    ...
        if (!newText.length) {
            error('The task cannot be empty');
        }
    ...
}});

Si estamos modificando una tarea existente, crearemos la nueva tarea con los nuevos datos del formulario, la reemplazaremos por la existente, guardaremos la lista, y cerraremos el cuadro de diálogo.

Si estamos añadiendo una nueva tarea, crearemos un nuevo elemento <ion-item-sliding></ion-item-sliding> que contendrá la fecha, descripción e icono introducidos en el formulario. A continuación añadiremos dicha tarea a la lista que estemos modificando, la guardaremos, y cerraremos el cuadro de diálogo:

let newItem = { text:newText, date:newDate, icon:newIcon };

if (index !== false) { list[index] = newItem; }
else { list.unshift(newItem); }

saveList(getTab(), list);
modal.dismiss();

Reordenando las tareas

La totalidad de la funcionalidad necesaria para reordenar las tareas de una lista ya nos la proporciona IONIC mediante el elemento <ion-reorder></ion-reorder>. Lo único que debemos hacer por nuestra parte, es cambiar el valor del atributo disabled del elemento <ion-reorder-group></ion-reorder-group> de la lista seleccionada:

function toggleReorder() {
    closeItems();
    let reorder = getTab().querySelector('ion-reorder-group');
    reorder.disabled = !reorder.disabled;
}

En caso de que se esté mostrando algún botón de borrado de una tarea, se ocultará para habilitar correctamente la funcionalidad de reordenar elementos.

Cuando la opción de reordenar se encuentra activa, se mostrará un nuevo icono en todas las tareas. Si lo mantenemos pulsado, nos permitirá arrastrar la tarea correspondiente para soltarla en una nueva posición.

Al volver a pulsar el botón de reordenar ubicado en el encabezado de la aplicación, la opción de reordenar se desactivará, evitando que cambiemos accidentalmente el orden de las tareas.

Borrando tareas

Para borrar todas las tareas de la lista seleccionada, pulsaremos el botón que hemos puesto en el encabezado principal de la aplicación. Y para borrar una tarea específica, pulsaremos el botón que aparece al deslizar la tarea hacia la derecha. En ambos casos, utilizaremos la misma función JavaScript (deleteItem(index)), pasando como parámetro el elemento HTML que contiene la tarea a borrar, o el valor false para borrar todas las tareas de la lista seleccionada.

Para evitar borrados accidentales, pediremos confirmación al usuario utilizando un cuadro de diálogo de tipo Alert que ya nos proporciona IONIC:

function deleteItem(index = false) {
    const alert = document.createElement('ion-alert');
  
    alert.header = index !== false ? 'Delete item' : 'Delete all',
    alert.message = 'Are you sure?',
    alert.buttons =  
            [{
                text: 'YES',
                handler: () => {
                    let list = getList();
                    if (index !== false) { list.splice(index, 1); }
                    else { list.length = 0; }
                    saveList(getTab(), list);
                }
            }, {
                text: 'NO',
                role: 'cancel'
            }]

    document.querySelector('ion-app').appendChild(alert);
    alert.present();
}

Como se puede observar, al confirmar el borrado, si se recibe un elemento por parámetro, sólo se borra dicho elemento. En caso contrario, se borrará toda la lista, utilizando una cadena vacía para eliminar todo el código HTML que contenía. Finalmente, guardaremos la lista correspondiente:

...
    let list = getList();
    if (index !== false) { list.splice(index, 1); }
    else { list.length = 0; }
    saveList(getTab(), list);
...

Eventos a tener en cuenta

Cada vez que reordenemos las tareas de alguna lista, deberemos volverla a guardar para que aparezcan en el orden correcto al volver a abrir la aplicación. Para ello capturaremos el evento ionItemReorder, tal como se indica en la documentación de IONIC, apartado de JavaScript:

document.addEventListener('ionItemReorder', (event) => { moveItem(event.detail); });
...
function moveItem(indexes) {
    let tab = getTab();
    let list = getList(tab);
    let item = list[indexes.from];
    list.splice(indexes.from, 1);
    list.splice(indexes.to, 0, item);
    indexes.complete();
    saveList(tab, list);
}

Cada vez que cerremos un cuadro de diálogo (eventos ionModalDidDismiss y ionAlertDidDismiss) cerraremos los elementos de la lista que pudieran estar deslizados hacia la derecha.

document.addEventListener("ionModalDidDismiss", closeItems);
document.addEventListener("ionAlertDidDismiss", closeItems);
...
function closeItems() {
    getTab().querySelector('ion-list').closeSlidingItems();
}

Y por último, cada vez que iniciemos la aplicación (evento onload), leeremos las listas que se encuentran guardadas en el navegador:

function onLoad() {
    ...
    document.querySelectorAll('ion-tab').forEach(function(t) { printList(t); });
}

En resumen…

El fichero index.html

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>Reorder</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">

  <script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.esm.js"></script>
  <script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/css/ionic.bundle.css"/>

  <script src="cordova.js"></script>
  <script src="todo.js"></script>
</head>

<body onload="onLoad()">
  <ion-app>
    <ion-menu content-id="content">
      <ion-header>
        <ion-toolbar mode="ios" color="danger">
          <ion-buttons slot="start">
            <ion-menu-button></ion-menu-button>
          </ion-buttons>  
          <ion-title>Menu</ion-title>
        </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 onclick="toggleReorder()">
              <ion-label>Enable/Disable reorder</ion-label>
              <ion-icon slot="end" name="reorder"></ion-icon>
            </ion-item>
            <ion-item onclick="share()">
              <ion-label>Share</ion-label>
              <ion-icon slot="end" name="share"></ion-icon>
            </ion-item>
            <ion-item-divider>
              <ion-label>Delete</ion-label>
            </ion-item-divider>          
            <ion-item onclick="deleteItem()">
              <ion-label>Delete all</ion-label>
              <ion-icon slot="end" name="trash" color="danger"></ion-icon>
            </ion-item>
          </ion-list>
        </ion-menu-toggle>
      </ion-content>
    </ion-menu>

    <div class="ion-page" id="content">
      <ion-header>
        <ion-toolbar mode="ios" color="primary">
          <ion-buttons slot="start">
            <ion-button>
              <ion-menu-button></ion-menu-button>
            </ion-button>
          </ion-buttons>          
          <ion-title>ToDo!</ion-title>
          <ion-buttons slot="end">
            <ion-button onclick="addEditItem()">
              <ion-icon name="add" slot="icon-only"></ion-icon>
            </ion-button>
          </ion-buttons>            
        </ion-toolbar>
      </ion-header>
      <ion-content>
        <ion-tabs>
          <ion-tab tab="school">
            <ion-list lines="full">
              <ion-reorder-group></ion-reorder-group>
            </ion-list>
          </ion-tab>
          <ion-tab tab="home">
            <ion-list lines="full">
              <ion-reorder-group></ion-reorder-group>
            </ion-list>
          </ion-tab>
          <ion-tab-bar slot="bottom" color="primary">
            <ion-tab-button tab="school">
              <ion-icon name="school"></ion-icon>
            </ion-tab-button>
            <ion-tab-button tab="home">
              <ion-icon name="home"></ion-icon>
            </ion-tab-button>
          </ion-tab-bar>
        </ion-tabs>
      </ion-content>
    </div>
  </ion-app>
</body>
</html>

El fichero todo.js

function onLoad() { 
    document.addEventListener("ionModalDidDismiss", closeItems);
    document.addEventListener("ionAlertDidDismiss", closeItems);
    document.addEventListener("ionDidOpen", closeItems);
    document.addEventListener('ionItemReorder', (event) => { moveItem(event.detail); });
    document.querySelectorAll('ion-tab').forEach(function(t) { printList(t); });
}

function getTab() {
    return(document.querySelector('ion-tab:not(.tab-hidden)'));
}

function getList(tab = getTab()) {
    let list = localStorage.getItem('todo-list-'+tab.tab);
    return list ? JSON.parse(list) : [];
}

function saveList(tab, list) {
    localStorage.setItem('todo-list-'+tab.tab, JSON.stringify(list));
    printList(tab);
}

function printList(tab) {
    tab.querySelector('ion-reorder-group').innerHTML = "";

    getList(tab).forEach((item, index) => {
        tab.querySelector('ion-reorder-group').innerHTML +=
        `<ion-item-sliding>
           <ion-item onClick="addEditItem(`+index+`)">
             <ion-label text-wrap>
               <h2>`+item.text+`</h2>
               <p>`+item.date.slice(0,10)+`</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" onClick="deleteItem(`+index+`)">
                <ion-icon slot="icon-only" name="trash"></ion-icon>
            </ion-item-option>
          </ion-item-options>
        </ion-item-sliding>`;
    });    
}

function error(message) {
  const alert = document.createElement('ion-alert');
  alert.message = message;
  alert.buttons = ['OK'];

  document.querySelector('ion-app').appendChild(alert);
  alert.present();
}

function toggleReorder() {
    closeItems();
    let reorder = getTab().querySelector('ion-reorder-group');
    reorder.disabled = !reorder.disabled;
}

function closeItems() {
    getTab().querySelector('ion-list').closeSlidingItems();
}

function addEditItem(index = false) {
    closeItems();
    let list = getList();
    let item = null;

    if (index !== false) item = list[index];
    else item = { text:"", date:new Date().toISOString(), icon:"radio-button-off" };

    const modal = document.createElement('ion-modal');
    modal.component = document.createElement('div');
    modal.component.innerHTML = `
        <ion-header>
            <ion-toolbar>
                <ion-title>ToDo - `+(index !== false ? 'Edit task' : 'New task')+`</ion-title>
                <ion-buttons slot="primary">
                    <ion-button color="danger"><ion-icon slot="icon-only" name="close"></ion-icon></ion-button>
                    <ion-button color="primary"><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" value="`+item.date+`"></ion-datetime>            
                </ion-item>
                <ion-item>
                    <ion-label position="floating">Enter task</ion-label>
                    <ion-input value="`+item.text+`"></ion-input>
                </ion-item>
            </ion-list>
            <ion-segment value="`+item.icon+`">
                <ion-segment-button value="radio-button-off">
                    <ion-icon name="radio-button-off"></ion-icon>
                </ion-segment-button>  
                <ion-segment-button value="radio-button-on">
                    <ion-icon name="radio-button-on"></ion-icon>
                </ion-segment-button>  
                <ion-segment-button value="snow">
                    <ion-icon name="snow"></ion-icon>
                </ion-segment-button>
                <ion-segment-button value="flame">
                    <ion-icon name="flame"></ion-icon>
                </ion-segment-button>
            </ion-segment>
        </ion-content>`;

    modal.component.querySelector('[color="danger"]').addEventListener('click', () => {
        modal.dismiss();
    });

    modal.component.querySelector('[color="primary"]').addEventListener('click', () => {
        let newDate = modal.component.querySelector('ion-datetime').value;
        let newText = modal.component.querySelector('ion-input').value;
        let newIcon = modal.component.querySelector('ion-segment').value;

        if (!newText.length) {
            error('The task cannot be empty');
        }
        else {
            let newItem = { text:newText, date:newDate, icon:newIcon };
            if (index !== false) list[index] = newItem; 
            else list.unshift(newItem);
            saveList(getTab(), list);
            modal.dismiss();
        }
    });

    document.querySelector('ion-app').appendChild(modal);
    modal.present();
}

function moveItem(indexes) {
    let tab = getTab();
    let list = getList(tab);
    let item = list[indexes.from];
    list.splice(indexes.from, 1);
    list.splice(indexes.to, 0, item);
    indexes.complete();
    saveList(tab, list);
}

function deleteItem(index = false) {
    const alert = document.createElement('ion-alert');
  
    alert.header = index !== false ? 'Delete item' : 'Delete all',
    alert.message = 'Are you sure?',
    alert.buttons =  
            [{
                text: 'YES',
                handler: () => {
                    let list = getList();
                    if (index !== false) { list.splice(index, 1); }
                    else { list.length = 0; }
                    saveList(getTab(), list);
                }
            }, {
                text: 'NO',
                role: 'cancel'
            }]

    document.querySelector('ion-app').appendChild(alert);
    alert.present();
}

El resultado

Puedes hacer clic aquí para observar el aspecto que tiene la aplicación de lista de tareas y probar la funcionalidad resultante utilizando el código especificado.

Galería de imágenes con IONIC 4

El objetivo

En este ejercicio pretendemos desarrollar una aplicación para visualizar imágenes que se encuentran agrupadas en categorías. Para ello sugerimos implementar la siguiente funcionalidad:

  • Crear pestañas o tabs para cada una de las categorías, agrupando así las imágenes, y permitiendo con un solo click cambiar entre una categoría u otra.
  • Utilizando la pantalla táctil de nuestros dispositivos, habilitar el gesto swipe para acceder a la imagen anterior o posterior.
  • Activar la reproducción automática mediante un botón, permitiendo a su vez pausar la visualización mediante otro botón.
  • Permitir ajustar la velocidad en la que se van mostrando las imágenes.

Una posible sugerencia de la interfaz de la aplicación sería la siguiente:

Elementos HTML de IONIC 4

Slides

La funcionalidad principal de este ejercicio nos la proporciona IONIC mediante el elemento <ion-slides></ion-slides>, donde ya se encuentra encapsulada la respuesta a los gestos de swipe y la reproducción automática, tal como se explica en la documentación.

Por ejemplo, para poder visualizar de manera secuencial las imágenes alicante1.jpg, alicante2.jpg y alicante3.jpg, ubicadas en la carpeta img, bastaría con insertarlas dentro de elementos <ion-slide></ion-slide>, y agruparlas todas con <ion-slides></ions-slides> de la siguiente forma:

<ion-slides>
  <ion-slide><img src="img/alicante1.jpg" /></ion-slide>
  <ion-slide><img src="img/alicante2.jpg" /></ion-slide>
  <ion-slide><img src="img/alicante3.jpg" /></ion-slide>
</ion-slides>

También podríamos haber utilizado el elemento <ion-img></ion-img>, que tiene un comportamiento casi idéntico al <img> de HTML. La diferencia es que esta última versión de IONIC optimiza especialmente aquellas páginas que tienen una lista muy grande de imágenes, ya que sólo se cargarían cuando fueran a estar visibles.

Tabs

El siguiente elemento clave que utilizaremos es <ion-tabs></ion-tabs>, ya que nos permitirá agrupar las fotos por categoría. Nos proporciona la funcionalidad necesaria para mostrar unas secciones de nuestro código HTML mientras se ocultan otras.

Conviene destacar además las numerosas opciones de personalización, tales como el color, que se pueden ajustar especificando el valor deseado en el atributo correspondiente (se puede consultar la documentación para más detalles). Por ejemplo, si queremos que se muestren dos pestañas con el fondo de color rojo, una de ellas etiquetada con el texto Madrid y el icono train, y la otra con París y airplane, procederíamos de la siguiente forma:

<ion-tabs>
  <ion-tab tab="madrid">
      ...
  </ion-tab>
  <ion-tab tab="paris">
      ...
  </ion-tab>
  <ion-tab-bar slot="bottom" color="danger">
    <ion-tab-button tab="madrid">
      <ion-icon name="train"></ion-icon>
      <ion-label>Madrid</ion-label>
    </ion-tab-button>
    <ion-tab-button tab="paris">
      <ion-icon name="airplane"></ion-icon>
      <ion-label>París</ion-label>
    </ion-tab-button>
  </ion-tab-bar>
</ion-tabs>

En nuestro ejemplo, donde queremos agrupar las diapositivas con las imágenes en varias categorías, procederíamos de la siguiente forma:

<ion-tabs>
  <ion-tab tab="madrid">
    <ion-slides>
      <ion-slide><img src="img/madrid1.jpg" /></ion-slide>
      <ion-slide><img src="img/madrid2.jpg" /></ion-slide>
      <ion-slide><img src="img/madrid3.jpg" /></ion-slide>
    </ion-slides>
  </ion-tab>
  <ion-tab tab="paris">
    <ion-slides>
      <ion-slide><img src="img/paris1.jpg" /></ion-slide>
      <ion-slide><img src="img/paris2.jpg" /></ion-slide>
      <ion-slide><img src="img/paris3.jpg" /></ion-slide>
    </ion-slides>
  </ion-tab>
  <ion-tab-bar slot="bottom">
    <ion-tab-button tab="madrid">
      <ion-icon name="train"></ion-icon>
      <ion-label>Madrid</ion-label>
    </ion-tab-button>
    <ion-tab-button tab="paris">
      <ion-icon name="airplane"></ion-icon>
      <ion-label>París</ion-label>
    </ion-tab-button>
  </ion-tab-bar>
</ion-tabs>

Al hacer clic en la categoría Madrid, se ocultarían las diapositivas de la categoría París, y viceversa.

Además, hacemos uso de los elementos ion-label e ion-icon, que nos permiten establecer el texto y el icono que aparecen en la pestaña, respectivamente. El elemento ion-label acepta un texto libre, mientras que ion-icon permite elegir uno de entre todos los posibles iconos que nos proporciona IONIC.

Botones play/pause

Para iniciar y pausar la reproducción automática de las imágenes, insertaremos dos botones formados simplemente por los iconos  play y pause que nos proporciona IONIC:

<ion-button onclick="play()"><ion-icon slot="icon-only" name="play"></ion-icon></ion-button>
<ion-button onclick="pause()"><ion-icon slot="icon-only" name="pause"></ion-icon></ion-button>

Y los insertaremos junto con el título en la parte superior de la pantalla:

<ion-header>
  <ion-toolbar>
    <ion-title>Gallery!</ion-title>
    <ion-buttons slot="primary">
      <ion-button onclick="play()"><ion-icon slot="icon-only" name="play"></ion-icon></ion-button>
      <ion-button onclick="pause()"><ion-icon slot="icon-only" name="pause"></ion-icon></ion-button>
    </ion-buttons>
  </ion-toolbar>  
</ion-header>

Un poco más adelante veremos la funcionalidad JavaScript que se ejecutará al pulsar dichos botones.

Para ajustar la velocidad

Para poder ajustar el tiempo que permanece cada diapositiva en la pantalla, sugerimos utilizar un elemento range (<ion-range></ion-range>). Por ejemplo, para poder establecer un tiempo que oscile entre 0 y 5 segundos (5000 milisegundos), podríamos utilizar el siguiente código:

<ion-range min="0" max="5000">
  <ion-icon slot="start" size="small" name="speedometer"></ion-icon>
  <ion-icon slot="end" name="speedometer"></ion-icon>
</ion-range>

Con el objetivo de mejorar el impacto visual del control deslizante, en el ejemplo utilizamos el icono speedometer, que será más pequeño a la izquierda (slot="start" size="small") para indicar que las imágenes irán apareciendo lentamente, y más grande a la derecha (slot="end") para dar a entender que las diapositivas se mostrarán una tras otra más rápidamente.

Sugerimos insertar dicho control también en el elemento <ion-header></ion-header> de la aplicación, para que aparezca justo debajo de la barra que contiene el título y los botones:

<ion-header>
  <ion-toolbar>
    <ion-title>...</ion-title>
    <ion-buttons>
      ...
    </ion-buttons>
  </ion-toolbar>
  <ion-toolbar>
    <ion-item>
      <ion-range>
        ...
      </ion-range>
    </ion-item>
  </ion-toolbar>    
</ion-header>

El código JavaScript

Accediendo a cada elemento

Por simplificar un poco el código, utilizaremos tres funciones para acceder a cada uno de los elementos clave (range, tabs y slides):

function getDelay() {
    return(document.querySelector('ion-range'));
}

function getTabs() {
    return(document.querySelector('ion-tabs'));
}

function getSlides() {
    return(document.querySelectorAll('ion-slides'));
}

Al pulsar play/pause

Para iniciar la reproducción automática de diapositivas bastará con acceder al elemento <ion-slides></ion-slides> correspondiente y ejecutar el método startAutoplay(), tal como se indica en la documentación.

Puesto que tendremos varias pestañas (una por cada categoría) primero deberemos acceder al tab que se encuentre seleccionado (getTabs().getSelected()), que será devuelto en una promesa. Utilizaremos luego simplemente el atributo id para acceder a la categoría activa, iniciando (s.startAutoplay()) o parando (s.stopAutoplay()) la reproducción según el botón que se haya pulsado (play o pause):

function play() {
    getTabs().getSelected().then(function(tab) {
        document.getElementById(tab).startAutoplay();
    });
}

function pause() {
    getSlides().forEach(function(s) {
        s.stopAutoplay();
    });
}

Al ajustar la velocidad de reproducción

Cada vez que movamos el control deslizante, se generará un evento ionChange, que nos indicará que tenemos que cambiar la velocidad con la que se muestran las diapositivas:

getDelay().addEventListener("ionChange", init);

...

function init() {
    getSlides().forEach(function(s) {
        s.options = {
            width: window.innerWidth,
            autoplay: {
                delay: 5000 - getDelay().value
            }
        };
    });
}

Puesto que la posición izquierda del control deslizante indicará menos velocidad (tiempo de espera mayor), y la posición derecha mayor velocidad (tiempo de espera menor), bastará con hacer una resta del tiempo máximo de visualización (5000 milisegundos) para calcular el tiempo (delay) que deberá permanecer cada imagen en pantalla:

autoplay: {
    delay: 5000 - getDelay().value
}

Inicializamos también la anchura de cada diapositiva indicando que ocupan toda la ventana:

s.options = {
    width: window.innerWidth,
    ...
};

Al cambiar de pestaña

Cada vez que cambiemos de pestaña, pararemos la reproducción:

getTabs().addEventListener("ionTabsWillChange", pause);

...

function pause() {
    getSlides().forEach(function(s) {
        s.stopAutoplay();
    });
}

Velocidad de reproducción inicial

Utilizaremos los eventos onload y resize para inicializar la velocidad de reproducción de las diapositivas que hemos especificado en el control deslizante, así como la anchura de cada una para que ocupen toda la pantalla:

<body onload="onLoad()">

...

function onLoad() {
    window.addEventListener('resize', init);
    ...
    init();
}

...

function init() { 
    getSlides().forEach(function(s) {
        ...
    });
}

En resumen…

El fichero index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">

  <script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.esm.js"></script>
  <script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/css/ionic.bundle.css"/>

  <script src="gallery.js"></script>
  <title>Gallery!</title>
</head>
<body onload="onLoad()">
  <ion-app>
    <ion-header>
      <ion-toolbar>
        <ion-title>Gallery!</ion-title>
        <ion-buttons slot="primary">
          <ion-button onclick="play()"><ion-icon slot="icon-only" name="play"></ion-icon></ion-button>
          <ion-button onclick="pause()"><ion-icon slot="icon-only" name="pause"></ion-icon></ion-button>
        </ion-buttons>
      </ion-toolbar>
      <ion-toolbar>
        <ion-item>
          <ion-range min="0" max="5000" value="4000">
            <ion-icon slot="start" size="small" name="speedometer"></ion-icon>
            <ion-icon slot="end" name="speedometer"></ion-icon>
          </ion-range>
        </ion-item>
      </ion-toolbar>    
    </ion-header>
    <ion-content>
      <ion-tabs>
        <ion-tab tab="alicante">
          <ion-slides id="alicante">
            <ion-slide><img src="img/alicante1.jpg" /></ion-slide>
            <ion-slide><img src="img/alicante2.jpg" /></ion-slide>
            <ion-slide><img src="img/alicante3.jpg" /></ion-slide>
          </ion-slides>
        </ion-tab>
        <ion-tab tab="madrid">
          <ion-slides id="madrid">
            <ion-slide><img src="img/madrid1.jpg" /></ion-slide>
            <ion-slide><img src="img/madrid2.jpg" /></ion-slide>
            <ion-slide><img src="img/madrid3.jpg" /></ion-slide>
          </ion-slides>
        </ion-tab>
        <ion-tab tab="paris">
          <ion-slides id="paris">
            <ion-slide><img src="img/paris1.jpg" /></ion-slide>
            <ion-slide><img src="img/paris2.jpg" /></ion-slide>
            <ion-slide><img src="img/paris3.jpg" /></ion-slide>
          </ion-slides>
        </ion-tab>
        <ion-tab-bar slot="bottom">
          <ion-tab-button tab="alicante">
            <ion-icon name="walk"></ion-icon>
            <ion-label>Alicante</ion-label>
          </ion-tab-button>
          <ion-tab-button tab="madrid">
            <ion-icon name="train"></ion-icon>
            <ion-label>Madrid</ion-label>
          </ion-tab-button>
          <ion-tab-button tab="paris">
            <ion-icon name="airplane"></ion-icon>
            <ion-label>París</ion-label>
          </ion-tab-button>
        </ion-tab-bar>
      </ion-tabs>
    </ion-content>
  </ion-app>
</body>
</html>

El fichero gallery.js

function onLoad() {
    getDelay().addEventListener("ionChange", init);
    window.addEventListener('resize', init);
    getTabs().addEventListener("ionTabsWillChange", pause);
    init();
}

function getDelay() {
    return(document.querySelector('ion-range'));
}

function getTabs() {
    return(document.querySelector('ion-tabs'));
}

function getSlides() {
    return(document.querySelectorAll('ion-slides'));
}

function init() {
    getSlides().forEach(function(s) {
        s.options = {
            width: window.innerWidth,
            autoplay: {
                delay: 5000 - getDelay().value
            }
        };
    });
}

function play() {
    getTabs().getSelected().then(function(tab) {
        document.getElementById(tab).startAutoplay();
    });
}

function pause() {
    getSlides().forEach(function(s) {
        s.stopAutoplay();
    });
}

El resultado

Puedes hacer clic aquí para observar el aspecto que tiene la galería de imágenes y probar la funcionalidad resultante utilizando el código especificado.

Compilando una App con PhoneGap Build

Adobe PhoneGap

Tal como ya sabemos, el uso de dispositivos móviles ha crecido de manera exponencial en los últimos años. Como consecuencia, el número de aplicaciones para dichos dispositivos se han incrementado en la misma medida. Esto ha motivado a su vez que se haya realizado un esfuerzo muy importante para desarrollar frameworks como IONIC que facilitan y mejoran tanto el proceso de programación de aplicaciones híbridas, como el resultado ofrecido por las mismas. Es aquí donde PhoneGap juega un papel importante.

Las aplicaciones híbridas nos proporcionan una gran ventaja respecto a las las nativas: con un sólo código fuente, podremos usar nuestra aplicación en cualquier dispositivo, independientemente del sistema operativo que tenga (iOS, Android, etc).

PhoneGap es un framework que brinda la posibilidad de desarrollar aplicaciones híbridas de manera gratuita utilizando tecnologías web muy simples tales como HTML, CSS y JavaScript. También podremos incluir IONIC en nuestros proyectos y compilar de la misma forma, obteniendo un resultado muy satisfactorio con un esfuerzo mínimo.

Además, dentro de las principales características de PhoneGap, nos encontramos con una serie de especificaciones para controlar los diferentes recursos del dispositivo, como por ejemplo:

  • Cámara
  • Acelerómetro
  • GPS
  • Notificaciones
  • Vibrador
  • Almacenamiento
  • Sistema de ficheros

Y por último queremos destacar que Adobe nos ofrece de forma gratuita el servicio online Phonegap Build. Se trata de un compilador al que podemos acceder vía web. Nos permite adjuntar un archivo a través de una cuenta en GitHub o un archivo .zip desde nuestro equipo:

Phonegap Build cuenta con varios tipos de planes, uno gratuito y otros de pago. En nuestro caso, podemos utilizar el servicio gratuito, ya que cubre perfectamente nuestras necesidades. La principal limitación que presenta es que sólo permite almacenar una aplicación privada, pero para nosotros será más que suficiente. También puedes encontrar más información aquí (en castellano) o en la página oficial (en inglés).

Desde mi punto de vista, PhoneGap Build nos proporciona una magnífica  herramienta para desarrollar aplicaciones multiplataformaya que nos brinda una manera de compilar nuestro código fuente de manera gratuita, sin necesidad de instalar nada en nuestros equipos. Incluso se puede utilizar desde cualquier ordenador por cualquier usuario, ya que no precisa disponer de software adicional ni permisos de administrador.

En resumen, PhoneGap nos brinda toda la funcionalidad del compilador, y no presenta ningún requisito. Sólo nos pide nuestros archivos HTML, CSS y JavaScript, y nos proporciona automáticamente el archivo APK con toda la funcionalidad nativa que necesitemos.

Cambios en el archivo index.html

En primer lugar, deberemos enlazar desde nuestro código HTML (index.html) el archivo JavaScript cordova.js (<script src="cordova.js"></script>) que nos permitirá acceder a la funcionalidad nativa desde nuestras propias funciones:

<!doctype html>
<html lang="en">
  <head>
    ...
    <script src="cordova.js"></script>
    <script src="calculator.js"></script>
    ...
  </head>
  <body>
  ...
  </body>
</html>

El archivo cordova.js se generará automáticamente por PhoneGap Build y se incluirá en nuestro proyecto. Nos permitirá acceder por ejemplo a recursos tales como la cámara, las notificaciones, etc., desde nuestras propias funciones JavaScript utilizando las especificaciones ampliamente documentadas de Cordova. En este punto convienen aclarar que PhoneGap es simplemente una distribución de Apache Cordova, y en la actualidad ambos nombres se utilizan para referirse al mismo framework.

Para poder apreciar la diferencia entre la ejecución en el navegador y la ejecución como app en el móvil, añadiremos por ejemplo la funcionalidad de vibración a nuestra calculadora. Además, para poder ajustar el nivel de vibración, vamos a añadir un control adicional:

<ion-list>
  ...
  <ion-item>
    <ion-range min="0" max="150" value="50" id="vibration">
      <ion-icon slot="start" name="trending-down"></ion-icon>
      <ion-icon slot="end" name="trending-up"></ion-icon>
    </ion-range>
  </ion-item>
</ion-list>

Podemos añadir dicho código por ejemplo justo después de los desplegables para ajustar el tamaño y tipo de los botones.

Utilizaremos el elemento <ion-range></ion-range> para especificar la duración de la vibración por cada pulsación de tecla. En el ejemplo proponemos utilizar un tiempo de vibración que varíe de 0 (min) a 150 (max) milisegundos, con un valor por defecto de 50 (value). Además añadiremos el icono trending-down al principio y trending-up al final del control, añadiendo los respectivos elementos <ion-icon></ion-icon>:

<ion-icon slot="start" name="trending-down"></ion-icon>
<ion-icon slot="end" name="trending-up"></ion-icon>

Se puede consultar la documentación oficial de IONIC para ampliar detalles sobre todas las opciones de configuración del elemento range, y los iconos disponibles.

Resumiendo, desde nuestro archivo principal index.html deberemos enlazar el fichero cordova.js, y añadir el control para el ajuste del tiempo de la vibración:

<!doctype html>
<html lang="en">
  <head>
    ...
    <script src="cordova.js"></script>
    <script src="calculator.js"></script>
    ...
  </head>
  <body>
  ...
    <ion-list>
      ...
      <ion-item>
        <ion-range min="0" max="150" value="50" id="vibration">
          <ion-icon slot="start" name="trending-down"></ion-icon>
          <ion-icon slot="end" name="trending-up"></ion-icon>
        </ion-range>
      </ion-item>
    </ion-list>  
  ...
  </body>
</html>

Cambios en el archivo calculator.js

En este ejercicio vamos a añadir una pequeña vibración cuando se pulse alguna tecla de la calculadora. Conseguiremos acceder a la vibración simplemente llamando a la función nativa desde nuestro código JavaScript. Por ejemplo, para provocar una vibración en nuestro dispositivo de 50 ms, bastaría con utilizar la siguiente línea de código:

navigator.vibrate(50);

Ahora deberemos añadir esa línea a nuestro código JavaScript. Además, para poder ajustar el valor de la vibración, en vez de poner un valor fijo, leeremos el valor seleccionado en el nuevo control implementado con el elemento <ion-range> e identificado con el id vibration. Por ejemplo, lo podemos hacer de la siguiente forma:

function setResult(value) {
    navigator.vibrate(document.getElementById("vibration").value);
    document.getElementById("result").innerHTML = value;
}

Dicho código provocará una vibración cada vez que se pulse cualquier tecla y se actualice el contenido de la pantalla de la calculadora.

Se puede encontrar la documentación completa relativa a la funcionalidad de vibración en la página correspondiente del plugin en Github.

El archivo config.xml

PhoneGap Build nos pide un archivo de configuración donde le podremos especificar por ejemplo el nombre de la aplicación, el icono a utilizar, o la funcionalidad nativa del móvil a la que nos gustaría acceder.

El contenido básico del archivo config.xml podría ser el siguiente:

<?xml version="1.0" encoding="UTF-8" ?>
<widget xmlns   = "http://www.w3.org/ns/widgets"
    xmlns:gap   = "http://phonegap.com/ns/1.0"
    id          = "com.fernandoruizrico.calculator"
    versionCode = "1"
    version     = "0.0.1" >

  <name>Calculator!</name>

  <description>
      A simple calculator.
  </description>

  <author href="https://fernandoruizrico.com" email="[email protected]">
      Fernando Ruiz Rico
  </author>

  <access origin="*" />
  <plugin name="cordova-plugin-whitelist" />
  <plugin name="cordova-plugin-vibration" />
</widget>

El elemento <widget> debe encontrarse en el nivel principal del archivo. Dentro de dicho elemento, podremos especificar el valor de los siguientes atributos:

  • id: El identificador único de nuestra aplicación. Se debe utilizar la notación de nombre de dominio inverso (por ejemplo, com.tuempresa.tuapp).
  • version: Código de versión de tres cifras (cambio mayor/cambio menor/corrección errores), como por ejemplo 0.0.1. Se incrementará la primera cifra cuando realicemos grandes cambios y mejoras. La segunda cuando hagamos pequeños cambios y correcciones de errores, y la tercera cuando simplemente se realice alguna corrección.
  • versionCode: Este atributo nos permite especificar el código de versión. Es opcional y sólo se utiliza cuando compilamos la aplicación para Android. Para más información puedes consultar la documentación oficial aquí.

Utilizando el elemento <name></name> podremos especificar el nombre de la aplicación que aparecerá debajo del icono. PhoneGap Build colocará de momento un icono por defecto, y ya más adelante (en otro ejercicio) proporcionaremos los detalles sobre cómo especificar y proporcionar el icono de la app.

El elemento <access /> nos permite especificar si damos permiso para acceder a archivos externos, como por ejemplo, para enlazar librerías directamente desde un servidor utilizando la conexión de Internet del móvil. Por ejemplo, nos permitirá acceder al servidor de IONIC desde nuestro archivo index.html, sin necesidad de instalar o descargar nada, tal como hemos hecho en la calculadora:

...
<script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.esm.js"></script>
<script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/css/ionic.bundle.css"/>
...

De momento permitiremos acceso a cualquier servidor (<access origin="*" />), y más adelante veremos cómo incluir directamente en la app el código de IONIC para poder prescindir incluso de la conexión de Internet en aplicaciones que no requieran de la misma.

En las últimas versiones de Cordova, resulta necesario añadir el plugin  cordova-plugin-whitelist para que se puedan hacer efectivos los permisos especificados con la etiqueta anterior. Le indicaremos a PhoneGap Build que añada automáticamente el código del plugin mediante la siguiente línea:

<plugin name="cordova-plugin-whitelist" />

Además, utlizaremos el plugin cordova-plugin-vibration para hacer que el móvil vibre cada vez que se pulse una tecla. Para pedirle a PhoneGap Build que incluya en nuestra aplicación la funcionalidad de vibración, deberemos añadir la línea correspondiente al archivo config.xml:

<plugin name="cordova-plugin-vibration" />

Puedes encontrar aquí un resumen de la documentación original (en inglés) relativa al fichero config.xml, y aquí la documentación completa (también en inglés).

Sobre los dos plugins utilizados en este ejemplo, puedes obtener documentación adicional aquí: cordova-plugin-whitelist y cordova-plugin-vibration.

¿Cómo generar el archivo APK?

En este ejercicio seguiremos sin instalar nada en nuestros ordenadores. Para compilar nuestra aplicación y obtener el archivo APK, utilizaremos el servicio online gratuito PhoneGap Build, tal como hemos sugerido. Para ello, deberemos acceder a la página principal de dicho servicio y pulsar en la opción Sign in (en la esquina superior derecha).

Puesto que en los últimos meses han estado realizando algunos cambios en la página web de PhoneGap Build y en los servicios que ofrecen, a continuación indicamos un par de capturas antiguas. Si aparecieran de nuevo las opciones de pago, en nuestro caso deberemos escoger la opción gratuita (Free Plan):

Para poder compilar la aplicación deberemos autentificarnos utilizando usuario y contraseña. Para ello, se puede crear una cuenta directamente desde dicha página web, o también podemos acceder mediante una cuenta de Facebook o Google.

Una vez hayamos accedido, si hacemos clic en Apps -> private, veremos la siguiente pantalla:

Ahí podemos seleccionar si queremos generar la aplicación utilizando el código disponible desde un enlace de Github, o utilizando un archivo zip, que será nuestro caso. Para ello utilizaremos el botón Upload a .zip file.

Deberemos haber comprimido previamente todos los archivos de nuestra aplicación en un mismo fichero ZIP. Por ejemplo, para nuestro ejercicio de la calculadora con IONIC 4, el archivo comprimido debería contener tres ficheros: index.html, calculator.jsconfig.xml.

Si se ha subido correctamente el archivo, estaréis delante de la siguiente pantalla:

Bastará con hacer click en Ready to build, y esperar un poco, hasta que PhoneGap Build compile la aplicación y genere el archivo APK.

Si todo ha ido bien, podréis descargar e instalar directamente vuestra aplicación mediante un código QR, o haciendo click en el icono de Android:

Finalmente, conviene destacar que podemos repetir este proceso tantas veces como queramos. Podemos cambiar, subir, compilar e instalar de nuevo la aplicación utilizando el botón Update Code. Y todo eso de forma gratuita y sin instalar nada en nuestros equipos… no se puede pedir más.

En resumen…

El archivo index.html

Desde nuestro archivo principal index.html deberemos enlazar el fichero cordova.js, y añadir el control para el ajuste del tiempo de la vibración:

<!doctype html>
<html lang="en">
  <head>
    ...
    <script src="cordova.js"></script>
    <script src="calculator.js"></script>
    ...
  </head>
  <body>
  ...
    <ion-list>
      ...
      <ion-item>
        <ion-range min="0" max="150" value="50" id="vibration">
          <ion-icon slot="start" name="trending-down"></ion-icon>
          <ion-icon slot="end" name="trending-up"></ion-icon>
        </ion-range>
      </ion-item>
    </ion-list>  
  ...
  </body>
</html>

El archivo calculator.js

Desde nuestro propio archivo donde hayamos ubicado nuestras funciones de JavaScript (por ejemplo, calculator.js), deberemos recoger el valor seleccionado en el nuevo control, y llamar a la funcion vibrate para acceder a la funcionalidad nativa correspondiente:

...
function setResult(value) {
    navigator.vibrate(document.getElementById("vibration").value);
    document.getElementById("result").innerHTML = value;
}
...

El archivo config.xml

Para poder compilar la aplicación desde PhoneGap Build, debemos proporcionar un archivo de configuración config.xml, donde especificaremos el nombre de la aplicación y los plugins utilizados:

<?xml version="1.0" encoding="UTF-8" ?>
<widget xmlns   = "http://www.w3.org/ns/widgets"
    xmlns:gap   = "http://phonegap.com/ns/1.0"
    id          = "com.fernandoruizrico.calculator"
    versionCode = "1"
    version     = "0.0.1" >

  <name>Calculator!</name>

  <description>
      A simple calculator.
  </description>

  <author href="https://fernandoruizrico.com" email="[email protected]">
      Fernando Ruiz Rico
  </author>

  <access origin="*" />
  <plugin name="cordova-plugin-whitelist" />
  <plugin name="cordova-plugin-vibration" />
</widget>

El archivo .zip

A PhoneGap Build le deberemos proporcionar un fichero comprimido con formato ZIP que contenga los tres archivos especificados: index.html, calculator.js y config.xml.

El resultado

Calculadora con estilo personalizable utilizando IONIC 4

¿Cómo podemos utilizar IONIC 4?

Si deseamos utilizar IONIC 4, sólo necesitaremos enlazar los archivos JavaScript y CSS, tal como se especifica en la documentación y en github:

...
<script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.esm.js"></script>
<script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/css/ionic.bundle.css"/>
...

De esta forma, quedarán definidos de forma automática los nuevos elementos, haciéndolos completamente accesibles al navegador para que nosotros sólo tengamos que escribir la etiqueta correspondiente en nuestro archivo HTML. A su vez además podremos interactuar con ellos utilizando código JavaScript.

Esta magnífica innovación que nos proporciona IONIC en su versión 4 simplifica muchísimo el acceso a sus archivos, ya que podemos utilizarlos sin necesidad de descargar ni instalar absolutamente nada.

Como se puede observar, estamos utilizamos la versión 4.9.1 de IONIC, que es la última que actualmente podemos encontrar en repositorio. Por otro lado, si deseáramos utilizar siempre la última versión, también podríamos acceder a dichos archivos utilizando la referencia «@ionic/core@latest». En tal caso, debemos tener en cuenta que con el paso del tiempo deberíamos comprobar si los últimos cambios pudieran afectar al código que escribimos inicialmente.

Más adelante describiremos el procedimiento para acceder a la funcionalidad de IONIC sin enlazar el código directamente desde Internet y así evitaremos que se descargue cada vez. Sin embargo, de momento podemos centrarnos en el código y el resultado, utilizando un simple editor de texto, sin necesidad de tener que instalar nada en nuestros equipos para poder utilizar IONIC.

Colocando los botones

En el ejemplo anterior hemos utilizado una tabla de HTML para distribuir los botones en filas y columnas. Sin embargo, las tablas se usan cada vez menos con este propósito. En su lugar se suele utilizar código CSS para ubicar o distribuir los diferentes elementos.

En este aspecto, IONIC nos proporciona una herramienta increíble conocida como grid, que tiene un comportamiento muy similar al de otras librerías de desarrollo web, como por ejemplo Bootstrap.

IONIC ha creado para nosotros tres nuevos elementos HTML que nos permiten establecer una cuadrícula muy fácilmente. Delimitamos los elementos que vamos a colocar mediante <ion-grid></ion-grid>  y luego establecemos las filas mediante <ion-row></ion-row> y las columnas con <ion-col></ion-col>.

Gracias a esto, podemos distribuir los botones de la calculadora utilizando el elemento grid de IONIC con un código casi idéntico al que utilizaríamos con una simple tabla de HTML, simplemente reemplazando las etiquetas de la siguiente forma:

  • <table></table> -> <ion-grid></ion-grid>
  • <tr></tr> -> <ion-row></ion-row>
  • <td></td> -> <ion-col></ion-col>

Y no sólo nos proporciona una forma sencilla de realizar dicha distribución, sino que nos aporta mucha más flexibilidad que las tablas de HTML, tal como se puede observar en la documentación oficial. Además, por defecto ocupará todo el ancho de la pantalla (el comportamiento esperado en cualquier dispositivo móvil) y cada fila podrá tener un número diferente de columnas, cuya anchura se distribuirá por defecto de manera uniforme.

Por ejemplo, para dibujar la siguiente tabla:

0
123+
456
789*
0/
AC.=

En el ejercicio anterior hemos utilizado el siguiente código HTML:

<table>
  <tr>
    <td colspan="4">0</td>
  </tr>
  <tr>
    <td>1</td>
    <td>2</td>
    <td>3</td>
    <td>+</td>
  </tr>
  <tr>
    <td>4</td>
    <td>5</td>
    <td>6</td>
    <td>-</td>
  </tr>
  <tr>
    <td>7</td>
    <td>8</td>
    <td>9</td>
    <td>*</td>
  </tr>
  <tr>
    <td colspan="3">0</td>
    <td>/</td>          
  </tr>
  <tr>
    <td colspan="2">AC</td>
    <td>.</td>
    <td>=</td>            
  </tr>
</table>

Si utilizamos los nuevos elementos que nos proporciona IONIC 4, obtendríamos el mismo resultado con el siguiente código:

<ion-grid>
  <ion-row>
    <ion-col>0</ion-col>
  </ion-row>
  <ion-row>
    <ion-col>1</ion-col>
    <ion-col>2</ion-col>
    <ion-col>3</ion-col>
    <ion-col>+</ion-col>
  </ion-row>
  <ion-row>
    <ion-col>4</ion-col>
    <ion-col>5</ion-col>
    <ion-col>6</ion-col>
    <ion-col>-</ion-col>
  </ion-row>
  <ion-row>
    <ion-col>7</ion-col>
    <ion-col>8</ion-col>
    <ion-col>9</ion-col>
    <ion-col>*</ion-col>
  </ion-row>
  <ion-row>
    <ion-col size="9">0</ion-col>
    <ion-col>/</ion-col>
  </ion-row>
  <ion-row>
    <ion-col size="6">AC</ion-col>
    <ion-col>.</ion-col>
    <ion-col>=</ion-col>
  </ion-row>
</ion-grid>

Tenemos que destacar la forma que nos ofrece IONIC 4 para establecer el tamaño de una celda en concreto. Por ejemplo, en nuestro caso, para conseguir que la tecla del cero ocupe 3/4 de la pantalla, y que la tecla de borrado ocupe la mitad, ajustamos el valor del atributo size en el elemento ion-col:

...
<ion-row>
  <ion-col size="9">0</ion-col>
  <ion-col>/</ion-col>
</ion-row>
<ion-row>
  <ion-col size="6">AC</ion-col>
  <ion-col>.</ion-col>
  <ion-col>=</ion-col>
</ion-row>
...

IONIC establece por defecto que en la pantalla caben 12 columnas. Por lo tanto, al especificar <ion-col size="9">0</ion-col>, estamos diciéndole al navegador que esa celda ocupará 9 columnas de un total de 12, es decir, 3/4 de la pantalla. Y al especificar <ion-col size="6">AC</ion-col>, estamos diciéndole al navegador que esa celda ocupará 6 columnas de un total de 12, es decir, la mitad de la pantalla.

Observamos además que en la primera fila, donde mostramos el resultado, no hace falta especificar el tamaño, ya que por defecto, la celda se expande automáticamente para ocupar todo el ancho de la fila.

Veremos más adelante que el número de columnas por defecto se puede configurar cambiando el valor de la variable CSS --ion-grid-columns.

Los nuevos botones

Con IONIC 4 disponemos de un nuevo elemento <ion-button></ion-button>. Se utiliza de forma similar al <button></button> de HTML, pero como podemos leer en la documentación, ahora podemos cambiar el aspecto del botón mediante atributos específicos tales como expand, fill, size o color. Por ejemplo, podríamos definir el botón de borrado de la siguiente forma, sin necesidad de utilizar ningún otro código adicional:

<ion-button color="danger" expand="block" onclick="del()">AC</ion-button>

El navegador nos mostrará automáticamente un botón de color rojo que ocupará todo el ancho de la columna.

Como podemos observar, la sencillez y similitud respecto al código básico HTML son obvias. El mayor cambio en este nuevo elemento es que ahora no necesitamos ajustar ninguna propiedad CSS para obtener el aspecto deseado, tal como hacíamos antes:

.ac {
  color: red;
}
<button class="ac">AC</button>

Para cambiar el estilo

En este apartado comenzaremos a apreciar la verdadera magia de IONIC 4. Fijémos en la similitud entre este código:

<select>
    <option value="clear">Sin borde ni color</option>
    <option value="outline">Sólo borde</option>
    <option value="solid" selected>Con color de fondo</option>
</select>

y este otro:

<ion-select value="solid">
  <ion-select-option value="clear">Sin borde ni color</ion-select-option>
  <ion-select-option value="outline">Sólo borde</ion-select-option>
  <ion-select-option value="solid">Con color de fondo</ion-select-option>
</ion-select>

A simple vista podemos intuir que el resultado debería ser el mismo. Sin embargo, al pulsar sobre el segundo select, nos aparecerá un cuadro de diálogo con un diseño muy cuidado que además será diferente en móviles IOS y en Android. Además, por defecto dispondremos de un botón para aceptar y otro para cancelar.

En la documentación de IONIC podemos deleitarnos observando el resultado que llegaremos a obtener haciendo uso de este nuevo elemento. Encontraremos además en ese enlace todos los posibles atributos con sus posibles respectivos valores, que nos permitirán ajustar el comportamiento del mismo a nuestros gustos y necesidades.

Debemos matizar que para obtener una correcta visualización, deberemos incluir cada elemento dentro de una lista, tal como se observa en los ejemplos, y que utilizaremos el elemento <ion-label></ion-label> para establecer el texto descriptivo, que también disparará el evento onclick automáticamente al pulsar encima. Por ejemplo, en nuestro caso:

<ion-list>
  <ion-item>
    <ion-label>Tamaño botones</ion-label>
    <ion-select value="default" id="size">
      <ion-select-option value="small">Pequeños</ion-select-option>
      <ion-select-option value="default">Medianos</ion-select-option>
      <ion-select-option value="large">Grandes</ion-select-option>
    </ion-select>
  </ion-item>
  <ion-item>
    <ion-label>Tipo botones</ion-label>
    <ion-select value="solid" id="type">
      <ion-select-option value="clear">Sin borde ni color</ion-select-option>
      <ion-select-option value="outline">Sólo borde</ion-select-option>
      <ion-select-option value="solid">Con color de fondo</ion-select-option>
    </ion-select>
  </ion-item>
</ion-list>

¿Estilo IOS o Android?

La verdad es que resulta difícil contestar a esa pregunta, y por eso los desarrolladores de IONIC nos permiten elegir el aspecto que van a tener los diferentes elementos de nuestros formularios. Para ello han añadido el atributo mode a muchos elementos, de forma que podamos especificar los valores «ios» o «md».

En caso de que queramos cambiar el estilo de todos los elementos de la calculadora al mismo tiempo, IONIC también nos permite hacerlo modificando la url, tal como se especifica en la documentación:

  • index.html?ionic:mode=md
  • index.html?ionic:mode=ios

Para mejorar el diseño de la calculadora, añadiremos a la misma un encabezado (elemento <ion-header></ion-header>) que contendrá un título y dos botones con un atributo href que apuntarán a esas dos urls. Además, observaremos al probar la nueva calculadora que el propio estilo del encabezado que contiene esos dos botones también se actualizará:

<ion-header>
  <ion-toolbar>
    <ion-title>Calculator</ion-title>
    <ion-buttons slot="secondary">
      <ion-button href="index.html?ionic:mode=md">Android</ion-button>
    </ion-buttons>
    <ion-buttons slot="primary">
      <ion-button href="index.html?ionic:mode=ios">IOS</ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

Cuando iniciemos la aplicación, ésta se mostrará automáticamente utilizando el estilo de la plataforma en la que se esté ejecutando.

¿Código JavaScript adicional?

Pues la verdad es que muy poco:

function onLoad() {
    document.addEventListener("ionChange", setStyle);
    setStyle();
}

function setStyle() {
    document.querySelectorAll("ion-content ion-button").forEach(function(b) {
        b.expand = "block";
        b.strong = "true";
        b.fill = document.getElementById("type").value;
        b.size = document.getElementById("size").value;
    });
}

Los elementos <ion-select></ion-select> generarán un evento ionChange automáticamente cada vez que cambie el valor seleccionado. Lo único que tenemos que hacer es estar atentos y volver a ajustar el diseño de la calculadora cuando se dispare dicho evento. Para que se ejecute una acción automáticamente al detectar cualquier cambio, utilizamos la siguiente línea:

document.addEventListener("ionChange", setStyle);

Una vez se ejecute la función setStyle(), lo único que tendremos que hacer es recorrer todos los botones y actualizar las propiedades que queramos. En nuestro caso podemos cambiar las siguientes propiedades:

  • expand: «block»
  • strong: «true»
  • fill: «clear», «outline» o «solid»
  • size: «small», «default» o «large»

Podemos volver a consultar la respectiva documentación si deseamos realizar cualquier modificación en el código.

En resumen…

El fichero index.html

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">

  <script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.esm.js"></script>
  <script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/dist/ionic/ionic.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/[email protected]/css/ionic.bundle.css"/>

  <script src="calculator.js"></script>

  <title>Calculator!</title>
</head>

<body onload="onLoad()">
  <ion-app>
    <ion-header>
      <ion-toolbar>
        <ion-title>Calculator</ion-title>
        <ion-buttons slot="secondary">
          <ion-button href="index.html?ionic:mode=md">Android</ion-button>
        </ion-buttons>
        <ion-buttons slot="primary">
          <ion-button href="index.html?ionic:mode=ios">IOS</ion-button>
        </ion-buttons>
      </ion-toolbar>
    </ion-header>

    <ion-content>
      <ion-list>
        <ion-item>
          <ion-label>Tamaño botones</ion-label>
          <ion-select value="default" id="size">
            <ion-select-option value="small">Pequeños</ion-select-option>
            <ion-select-option value="default">Medianos</ion-select-option>
            <ion-select-option value="large">Grandes</ion-select-option>
          </ion-select>
        </ion-item>
        <ion-item>
          <ion-label>Tipo botones</ion-label>
          <ion-select value="solid" id="type">
            <ion-select-option value="clear">Sin borde ni color</ion-select-option>
            <ion-select-option value="outline">Sólo borde</ion-select-option>
            <ion-select-option value="solid">Con color de fondo</ion-select-option>
          </ion-select>
        </ion-item>
      </ion-list>
      <ion-grid>
        <ion-row>
          <ion-col><ion-button color="dark"><span id="result">0</span></ion-button></ion-col>
        </ion-row>
        <ion-row>
          <ion-col><ion-button color="primary" onclick="add('1')">1</ion-button></ion-col>
          <ion-col><ion-button color="primary" onclick="add('2')">2</ion-button></ion-col>
          <ion-col><ion-button color="primary" onclick="add('3')">3</ion-button></ion-col>
          <ion-col><ion-button color="tertiary" onclick="add('+')">+</ion-button></ion-col>
        </ion-row>
        <ion-row>
          <ion-col><ion-button color="primary" onclick="add('4')">4</ion-button></ion-col>
          <ion-col><ion-button color="primary" onclick="add('5')">5</ion-button></ion-col>
          <ion-col><ion-button color="primary" onclick="add('6')">6</ion-button></ion-col>
          <ion-col><ion-button color="tertiary" onclick="add('-')">-</ion-button></ion-col>
        </ion-row>
        <ion-row>
          <ion-col><ion-button color="primary" onclick="add('7')">7</ion-button></ion-col>
          <ion-col><ion-button color="primary" onclick="add('8')">8</ion-button></ion-col>
          <ion-col><ion-button color="primary" onclick="add('9')">9</ion-button></ion-col>
          <ion-col><ion-button color="tertiary" onclick="add('*')">*</ion-button></ion-col>
        </ion-row>
        <ion-row>
          <ion-col size="9"><ion-button color="primary" onclick="add('0')">0</ion-button></ion-col>
          <ion-col><ion-button color="tertiary" onclick="add('/')">/</ion-button></ion-col>
        </ion-row>
        <ion-row>
          <ion-col size="6"><ion-button color="danger" onclick="del()">AC</ion-button></ion-col>
          <ion-col><ion-button color="primary" onclick="add('.')">.</ion-button></ion-col>
          <ion-col><ion-button color="tertiary" onclick="calc()">=</ion-button></ion-col>
        </ion-row>
      </ion-grid>
    </ion-content>
  </ion-app>
</body>
</html>

El fichero calculator.js

function onLoad() {
    document.addEventListener("ionChange", setStyle);
    setStyle();
}

function setStyle() {
    document.querySelectorAll("ion-content ion-button").forEach(function(b) {
        b.expand = "block";
        b.strong = "true";
        b.fill = document.getElementById("type").value;
        b.size = document.getElementById("size").value;
    });
}

function setResult(value) {
    document.getElementById("result").innerHTML = value;
}

function getResult() {
    return(document.getElementById("result").innerHTML);
}

function add(key) {
    var result = getResult();
    if (result!="0" || isNaN(key)) setResult(result + key);
    else setResult(key);
}

function calc() {
    var result = eval(getResult());
    setResult(result);
}

function del() {
    setResult(0);
}

El resultado

Puedes hacer clic aquí para observar el aspecto que tiene la calculadora y probar la funcionalidad resultante utilizando el código especificado.

Una app muy sencilla para guardar sitios interesantes con ubicaciones y fotografías con IONIC 4+Angular+Cordova

El objetivo: Guardar ubicaciones e imágenes

En este ejercicio vamos a demostrar que puede resultar muy sencillo desarrollar una aplicación que nos permita mantener un registro de lugares interesantes, guardando la ubicación precisa y una imagen identificativa de cada uno.

Será el mismo navegador el que nos proporcione la ubicación actual de forma automática, y nos permita elegir las imágenes a utilizar, de entre las que tengamos en nuestros equipos. Además, al ejecutarla como app en nuestros móviles, podremos utilizar también la cámara en el mismo momento, pudiendo hacer una foto que se quedará guardada en nuestra aplicación. De esa forma, tendremos imágenes asociadas con la latitud y la longitud de los sitios donde hayamos hecho las fotos, pudiendo acceder además al mapa correspondiente con un solo clic.

Con menos de 100 líneas de código

Veremos que al utilizar IONIC 4 + Angular, con menos de 100 líneas de código podemos desarrollar una aplicación multiplataforma, que funcionará perfectamente en cualquier navegador, o instalada como app en nuestros dispositivos móviles. Y todo ello, con el mismo código fuente, sin cambiar ni una sola línea.

La funcionalidad de la aplicación

La aplicación dispondrá de un cuadro de texto donde podamos especificar la descripción del nuevo lugar que queramos registrar.

Si ya hemos introducido alguna descripción, se habilitará un nuevo control para poder elegir una imagen. Si estamos ejecutando la aplicación desde nuestro ordenador de escritorio, podremos elegir cualquier imagen de las que se encuentran en nuestro equipo. Si ejecutamos la aplicación desde nuestros dispositivos móviles, podremos hacer una foto en ese mismo momento.

Una vez introducida la descripción y elegida o hecha la foto, la aplicación obtendrá automáticamente la ubicación en la que nos encontremos y creará un elemento en la lista de lugares en el que aparecerá la foto y la descripción.

Al hacer clic sobre cada elemento de la lista, si estamos utilizando el navegador de nuestro ordenador de escritorio, abrirá la página web correspondiente de Google Maps para mostrarnos la ubicación precisa. Si estamos ejecutando la aplicación desde nuestros dispositivos móviles, nos permitirá elegir qué aplicación se utilizará para visualizar la ubicación.

Además, podremos reordenar los elementos de la lista activando un control por cada elemento, que se podrá ocultar o mostrar mediante un botón ubicado en la barra superior de la aplicación.

Por último, permitiremos borrar cualquier elemento de la lista utilizando un botón oculto que se mostrará al deslizar el elemento correspondiente a la derecha.

Primeros pasos

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 lugares blank --type=angular --cordova --no-git --no-link

Al finalizar el proceso, se habrá creado un directorio con el nombre lugares, 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 lugares

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 a partir de ahora ya podemos comenzar a añadir el código específico de nuestra aplicación. Puesto que toda la funcionalidad se encuentra ubicada en una única pantalla, sólo vamos a modificar dos ficheros:

  1. src/app/home/home.page.html: Código HTML.
  2. src/app/home/home.page.ts: Código TypeScript.

Probando la aplicación en el navegador

La misma herramienta de consola de IONIC se puede utilizar como servidor web, lo que nos permitirá  probar nuestra aplicación fácilmente en el navegador. Además, cada vez que realicemos cualquier cambio en el código fuente, veremos cómo se actualiza el resultado automáticamente. 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)

Código HTML

El encabezado

Como ya viene siendo habitual, colocaremos una barra superior que contenga el título y el botón de reordenar:

<ion-header>
  ...
  <ion-title>Places!</ion-title>
  ...
  <ion-button (click)="reorder=!reorder">
  ...    
</ion-header>

En cada pulsación del botón de reordenar, activaremos o desactivaremos dicha funcionalidad cambiando simplemente el valor del atributo correspondiente.

El formulario para la descripción y la imagen

Utilizaremos un simple cuadro de texto para introducir la descripción del lugar que queramos registrar y un elemento HTML estándar de tipo fichero para elegir la imagen:

<ion-item>
  <ion-input placeholder="Enter description" [(ngModel)]="text"></ion-input>
</ion-item>
<ion-item *ngIf="text.length">
  <input type="file" accept="image/*" (change)="add($event)" />
  <ion-icon slot="end" name="camera" (click)="add()"></ion-icon>
</ion-item>

Debemos destacar de nuevo el uso de la directiva ngModel, que nos permitirá enlazar el valor del atributo text, de forma que cualquier modificación en el código HTML cambiará automáticamente el valor del atributo en el código TypeScript y viceversa.

En segundo lugar, también destacamos el uso de la directiva ngIf, que automatiza la creación del campo de tipo fichero, de forma que dicho campo no existirá hasta que se introduzca algún texto en el campo de la descripción. Además, al inicializar de nuevo la descripción desde el código TypeScript, dicho campo desaparecerá automáticamente.

Por último, al final del cuadro de elección de imagen colocaremos un icono para acceder a la cámara de nuestros dispositivos móviles, utilizando como imagen la fotografía que realicemos en ese momento. Esta funcionalidad no estará operativa cuando utilicemos la aplicación desde el navegador.

Los elementos de la lista

Cada elemento de la lista será un enlace a la página web correspondiente de Google Maps para conocer la ubicación del lugar, de forma que podamos acceder fácilmente con un simple clic en el elemento correspondiente. Para ello bastará con especificar la url mediante el atributo href del elemento <ion-item></ion-item> (más información aquí).

Además, utilizaremos el elemento <ion-thumbnail></ion-thumbnail> para mostrar una miniatura de la imagen. Por último, añadiremos dos botones, uno para reordenar (<ion-reorder></ion-reorder>) y otro para borrar (<ion-item-option></ion-item-option>):

<ion-item [href]='item.url'>
  <ion-thumbnail slot="start"><img [src]="item.img" /></ion-thumbnail>{{item.name}}
  <ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item-options side="start">
  <ion-item-option color="danger" (click)="delete(i)">
    <ion-icon slot="icon-only" name="trash"></ion-icon>
  </ion-item-option>
</ion-item-options>

Mostrando todos los elementos

Vamos a hacer uso del potencial de Angular para mostrar la lista completa de lugares:

<ion-reorder-group [disabled]="!reorder" (ionItemReorder)="move($event.detail)">
  <ion-item-sliding *ngFor="let item of list; let i=index">
    ...
  </ion-item-sliding>
</ion-reorder-group>

En primer lugar utilizaremos la expresión [disabled]="!reorder" para activar o desactivar la opción de reordenar los elementos. Mediante los corchetes enlazamos el valor del atributo reorder de TypeScript para poder acceder a su valor directamente. El evento (ionItemReorder) nos indicará que se ha cambiado de sitio algún elemento, y por lo tanto ejecutaremos el método move(), cuya funcionalidad veremos más adelante.

Por último, sólo necesitamos realizar un bucle utilizando la directiva ngFor, que además nos proporcionará cada elemento item del array que representa la lista, así como el índice i de cada uno de ellos.

El fichero «home.page.html» completo

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>Places!</ion-title>
    <ion-buttons slot="primary">
      <ion-button (click)="reorder=!reorder">
        <ion-icon slot="icon-only" name="reorder"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>       
</ion-header>
<ion-content>
  <ion-list #myList lines="full">
    <ion-item color="light">
      <ion-input placeholder="Enter description" [(ngModel)]="text"></ion-input>
    </ion-item>
    <ion-item color="light" *ngIf="text.length">
      <input type="file" accept="image/*" (change)="add($event)" />
      <ion-icon slot="end" name="camera" (click)="add()"></ion-icon>
    </ion-item>    
    <ion-reorder-group [disabled]="!reorder" (ionItemReorder)="move($event.detail)">
      <ion-item-sliding *ngFor="let item of list; let i=index">
        <ion-item [href]='item.url'>
          <ion-thumbnail slot="start"><img [src]="item.img" /></ion-thumbnail>{{item.name}}
          <ion-reorder slot="end"></ion-reorder>
        </ion-item>
        <ion-item-options side="start">
          <ion-item-option color="danger" (click)="delete(i)">
            <ion-icon slot="icon-only" name="trash"></ion-icon>
          </ion-item-option>
        </ion-item-options>
      </ion-item-sliding>
    </ion-reorder-group>
  </ion-list>
</ion-content>

Código TypeScript

Los atributos

En este ejercicio sólo necesitamos cuatro campos para guardar todos los datos necesarios:

@ViewChild('myList') listRef: List;
reorder: boolean;
list: any;
text: string;

El propósito de cada uno de ellos es el siguiente:

  • listRef: Referencia a la lista de html para poder cerrar todos los elementos antes de borrarlos.
  • reorder: Su valor (falso o verdadero) indicará si se encuentra habilitada o no la funcionalidad de reordenar.
  • list: Array que contendrá todos los lugares.
  • text: Descripción del nuevo lugar que queremos registrar.

Los métodos

  • constructor: Recuperará la lista guardada para que se muestre al iniciar la aplicación.
  • add: Añadirá un nuevo lugar a la lista de lugares.
  • save: Guardará en localStorage el listado de lugares para que no se pierda al cerrar la aplicación o recargar el contenido del navegador.
  • delete: Borrará un elemento de la lista.
  • move: Moverá un elemento de la lista a otra posición.

Obteniendo la ubicación

Para conocer la ubicación desde la que vamos a registrar un nuevo lugar, utilizaremos la funcionalidad de geolocalización de HTML, de forma que consigamos que el código funcione perfectamente tanto en el navegador, como en la app una vez compilada (más información aquí):

navigator.geolocation.getCurrentPosition(pos => {
  let url = "https://maps.google.com/maps?&z=15&t=k&q="+pos.coords.latitude+" "+pos.coords.longitude;
  ...
});

Además, crear un enlace a Google Maps conociendo la ubicación resulta muy sencillo. En nuestro caso especificamos un zoom razonable (z=15), el tipo de mapa satélite (t=k), y la latitud y la longitud (q=…). Se puede obtener más información en la página oficial de Google, o también por ejemplo aquí.

Indicador de ejecución en proceso

Mientras se obtiene la ubicación y se procesa la imagen, resulta conveniente bloquear la aplicación mediante algún indicador para informar al usuario que todavía no se ha completado la acción. Para ello mostraremos un indicador en pantalla utilizando la funcionalidad que nos proporciona IONIC (más información aquí):

async add(event) {
  let loading = await this.loadingController.create();
  loading.present();
  ...
  loading.dismiss();
}

Obteniendo la imagen

Utilizando simple código HTML y JavaScript, podemos obtener la imagen seleccionada en formato base 64, de forma que la podemos guardar como si se tratara de cualquier otra cadena de texto (más información aquí, o aquí):

...
<input type="file" (change)="add($event)" />
...
<img [src]="item.img" />
...
async add(event) {
  let reader = new FileReader();
  reader.onload = (data:any) => {
    item.img = data.target.result;
    ...
  }
  reader.readAsDataURL(event.target.files[0]);
}

Además, añadiremos la funcionalidad necesaria para abrir la cámara cuando ejecutemos la aplicación en nuestros dispositivos móviles (más información en la documentación de IONIC y en Cordova):

...
const options:CameraOptions = {targetWidth:100,destinationType:this.camera.DestinationType.DATA_URL};
this.camera.getPicture(options).then(data => {
  this.list.unshift({name:this.text,img:"data:image/jpeg;base64,"+data,url:url});
  this.save();         
});
...

El fichero «home.page.ts» completo

import { Component, ViewChild } from '@angular/core';
import { List, LoadingController } from '@ionic/angular';
import { Camera, CameraOptions } from '@ionic-native/camera/ngx';

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

export class HomePage {
  @ViewChild('myList') listRef: List;
  reorder: boolean;
  list: any;
  text: string;

  constructor(public loadingController:LoadingController,private camera:Camera) {
    this.text = "";
    this.list = localStorage.getItem('places-list');
    if (this.list) this.list = JSON.parse(this.list);
    else this.list = [];
  }

  async add(event?) {
    let loading = await this.loadingController.create({duration:15000});
    loading.present();

    navigator.geolocation.getCurrentPosition(pos => {
      let url = "https://maps.google.com/maps?&z=15&t=k&q="+pos.coords.latitude+" "+pos.coords.longitude;

      if (event) {
        let reader = new FileReader();
        reader.onload = (data:any) => {
          this.list.unshift({name:this.text,img:data.target.result,url:url});
          this.save();
          loading.dismiss();
        }
        reader.readAsDataURL(event.target.files[0]);
      }
      else {
        loading.dismiss();
        const options:CameraOptions = {targetWidth:100,destinationType:this.camera.DestinationType.DATA_URL};
        this.camera.getPicture(options).then(data => {
          this.list.unshift({name:this.text,img:"data:image/jpeg;base64,"+data,url:url});
          this.save();         
        });
      }
    });
  }

  save() {
    localStorage.setItem('places-list', JSON.stringify(this.list));
    this.text = "";
  }

  delete(item) {
    this.listRef.closeSlidingItems();
    this.list.splice(item, 1);
    this.save();
  }

  move(indexes) {
    let item = this.list[indexes.from];
    this.list.splice(indexes.from, 1);
    this.list.splice(indexes.to, 0, item);
    this.save();
  }
}

Icono y pantalla de bienvenida

Si queremos cambiar el icono o la pantalla de bienvenida, bastará con actualizar los archivos icon.png y splash.png respectivamente, colocándolos en la carpeta resources de nuestro proyecto (sobrescribiendo los que allí estuvieran, ya que normalmente IONIC nos habrá proporcionado unos por defecto). Después sólo debemos ejecutar el comando correspondiente para que ionic genere o actualice todos los archivos necesarios:

ionic cordova resources

Compilando la aplicación

Plugins de geolocalización y cámara

Para que la aplicación tenga los permisos correctos, y podamos utilizar la funcionalidad nativa para conocer la ubicación, añadiremos el plugin correspondiente (más información aquí):

ionic cordova plugin add cordova-plugin-geolocation --save

Además, para utilizar la cámara también echaremos mano de otro plugin, esta vez utilizando la API nativa que nos proporciona IONIC, ya que sólo podemos acceder a dicha funcionalidad en nuestros dispositivos móviles (más información aquí):

ionic cordova plugin add cordova-plugin-camera --save
npm install --save @ionic-native/camera@beta

Debemos recordar que toda la funcionalidad de la aplicación se encontrará disponible tanto en el navegador como en la app, exceptuando la cámara, ya que es específica del dispositivo, y no estará operativa en el navegador.

También será necesario modificar el archivo app.module.ts para poder acceder a la API de la cámara:

...
import { Camera } from '@ionic-native/camera/ngx';
...
@NgModule({
  ...
  providers: [
    ...
    Camera,
    ...
  ],
  ...
})
export class AppModule {}

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 { Camera } from '@ionic-native/camera/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

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

Generando el archivo APK

Para instalar la aplicación en nuestros dispositivos móviles, tenemos que compilar el código fuente:

ionic cordova build android --prod

O también lo podemos ejecutar directamente  en el móvil que tengamos conectado a nuestro ordenador:

ionic cordova run android --prod

El resultado

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

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.

Icono y pantalla de bienvenida con todas las resoluciones

El objetivo

Como habremos observado, PhoneGap Build utiliza un icono por defecto al compilar nuestras aplicaciones. En este ejercicio vamos a ver cómo podemos cambiar el icono de nuestra aplicación de lista de tareas fácilmente y sin necesidad de instalar ningún software adicional en nuestros equipos. Además, también veremos cómo añadir una pantalla de bienvenida, lo que sin duda le dará un toque más profesional a nuestra aplicación.

El icono nos servirá para distinguir claramente cuál es nuestra app de entre todas las instaladas en el móvil, pudiendo elegir una imagen cualquiera que podemos descargar de Internet, o también diseñándola nosotros mismos.

La pantalla de bienvenida se visualizará al arrancar la aplicación, y permanecerá unos segundos, mientras el móvil carga nuestra app, o incluso se puede prorrogar el tiempo que nosotros decidamos.

Por ejemplo, yo he buscado un par de imágenes etiquetadas para reutilización, y he escogido las siguientes como icono y pantalla de bienvenida respectivamente:

Cómo generar las imágenes con las resoluciones necesarias

Para que nuestra app cumpla los requisitos necesarios para su distribución, necesitaremos generar diversas imágenes con resoluciones específicas que sean adecuadas para diversos dispositivos, tal como especifican Google y Apple.

Más adelante veremos cómo la herramienta de consola de IONIC nos permitirá genera los archivos necesarios automáticamente partiendo de nuestras imágenes elegidas como icono y pantalla de bienvenida. Sin embargo, para ello deberemos instalar dicha utilidad previamente.

De momento utilizaremos alguna web que nos permite subir nuestras imágenes y nos generará automáticamente todos los archivos necesarios que debemos incluir, sin necesidad de instalar nada en nuestros ordenadores.

Podemos encontrar por ejemplo las siguientes (en orden de preferencia según mi criterio):

  1. PhoneGap / Cordova Resource Generator
  2. Abiro PhoneGap Image Generator
  3. Ape Tools – Image Gorilla

En mi caso he utilizado el primer enlace. Basta con subir las dos imágenes en el apartado de Input -> Logo / Icon e Input -> Splashscreen, y al pulsar el botón Upload files, la web ya nos genera todos los archivos necesarios, que se pueden descargar pulsando el botón Download your files.  Además, a la derecha veremos las líneas necesarias que debemos añadir al fichero config.xml:

El archivo config.xml

Deberemos incluir las secciones específicas en nuestro archivo config.xml para que PhoneGap Build pueda reconocer y enlazar todos los archivos generados. Además, deberemos añadir una línea con <plugin name="cordova-plugin-splashscreen" /> para poder utilizar el plugin para mostrar la pantalla de bienvenida:

<?xml version="1.0" encoding="UTF-8" ?>
<widget xmlns   = "http://www.w3.org/ns/widgets"
    ...
    <plugin name="cordova-plugin-splashscreen" />
    ...

    <platform name="android">
        <icon qualifier="ldpi" src="res/icons/android/drawable-ldpi-icon.png" />
        ...
        <splash qualifier="land-ldpi" src="res/screens/android/drawable-land-ldpi-screen.png" />
        ...
    </platform>  
</widget>

Como podemos observar, respecto a las imágenes a utilizar sólo he incluido la sección correspondiente a Android, ya que de momento no voy a compilar para IOS:

<platform name="android">
   ...
</platform>

Además, se puede configurar el tiempo que permanece mostrándose la pantalla de bienvenida. El valor por defecto es de tres segundos. Con la siguiente línea lo establecemos a cinco segundos para que la podamos ver un poco mejor:

<preference name="SplashScreenDelay" value="5000" />

También debemos tener en cuenta que por defecto, la pantalla de bienvenida sólo se mostrará cuando se abra la aplicación, no cuando se encuentre en segundo plano y pase a estar activa. Una vez se haya ejecutado la app, para volver a visualizar la imagen de nuevo, deberemos cerrar la aplicación por completo y volverla a abrir.

En las páginas oficiales de Cordova podemos obtener toda la documentación:

En resumen, el contenido del archivo config.xml podría ser el siguiente:

<?xml version="1.0" encoding="UTF-8" ?>
<widget xmlns   = "http://www.w3.org/ns/widgets"
    xmlns:gap   = "http://phonegap.com/ns/1.0"
    id          = "com.fernandoruizrico.todo"
    versionCode = "1"
    version     = "0.0.1" >

    <name>ToDo!</name>

    <description>
        ToDo List.
    </description>

    <author href="https://fernandoruizrico.com" email="[email protected]">
        Fernando Ruiz Rico
    </author>

    <access origin="*" />
    <plugin name="cordova-plugin-whitelist" />
    <plugin name="cordova-plugin-vibration" />
    <plugin name="cordova-plugin-splashscreen" />

    <preference name="SplashScreenDelay" value="5000" />

    <platform name="android">
        <icon qualifier="ldpi" src="res/icons/android/drawable-ldpi-icon.png" />
        <icon qualifier="mdpi" src="res/icons/android/drawable-mdpi-icon.png" />
        <icon qualifier="hdpi" src="res/icons/android/drawable-hdpi-icon.png" />
        <icon qualifier="xhdpi" src="res/icons/android/drawable-xhdpi-icon.png" />
        <icon qualifier="xxhdpi" src="res/icons/android/drawable-xxhdpi-icon.png" />
        <icon qualifier="xxxhdpi" src="res/icons/android/drawable-xxxhdpi-icon.png" />
        <splash qualifier="land-ldpi" src="res/screens/android/drawable-land-ldpi-screen.png" />
        <splash qualifier="land-mdpi" src="res/screens/android/drawable-land-mdpi-screen.png" />
        <splash qualifier="land-hdpi" src="res/screens/android/drawable-land-hdpi-screen.png" />
        <splash qualifier="land-xhdpi" src="res/screens/android/drawable-land-xhdpi-screen.png" />
        <splash qualifier="land-xxhdpi" src="res/screens/android/drawable-land-xxhdpi-screen.png" />
        <splash qualifier="land-xxxhdpi" src="res/screens/android/drawable-land-xxxhdpi-screen.png" />
        <splash qualifier="port-ldpi" src="res/screens/android/drawable-port-ldpi-screen.png" />
        <splash qualifier="port-mdpi" src="res/screens/android/drawable-port-mdpi-screen.png" />
        <splash qualifier="port-hdpi" src="res/screens/android/drawable-port-hdpi-screen.png" />
        <splash qualifier="port-xhdpi" src="res/screens/android/drawable-port-xhdpi-screen.png" />
        <splash qualifier="port-xxhdpi" src="res/screens/android/drawable-port-xxhdpi-screen.png" />
        <splash qualifier="port-xxxhdpi" src="res/screens/android/drawable-port-xxxhdpi-screen.png" />
    </platform>  
</widget>

El archivo ZIP

Si utilizamos la web https://www.resource-generator.com, nos generará un archivo ZIP que contendrá los siguientes ficheros:

De ahí deberemos copiar la carpeta res e incluirla en nuestro archivo ZIP, junto con el fichero config.xml modificado que subiremos a PhoneGap Build:

El resultado

Desde los siguientes enlaces se pueden descargar todos los archivos utilizados en este ejemplo: