HTML. Unidad 1. Estructura básica de un fichero HTML.

Presentación

Infografía

Diapositivas y vídeo

A través de esta presentación, estas diapositivas, y este vídeo, te familiarizarás con los contenidos clave de esta unidad.

Introducción

HTML («Hypertext Markup Language«) no es exactamente un lenguaje de programación. Es un lenguaje de marcado que le dice a los navegadores web cómo estructurar las páginas web que estás visitando. Puede ser tan complejo o tan simple como desee el desarrollador web. Este lenguaje está formado por varios elementos que definen diversos tipos de contenido. Un fichero HTML se puede editar con un simple editor de texto, pero hay algunos editores que usan colores y consejos para ayudar al desarrollador. Uno de los mejores editores es el Visual Studio Code. Se puede descargar desde aquí.

Elementos

HTML consiste en una serie de elementos, que puedes utilizar para encerrar, delimitar o marcar diferentes partes del contenido para hacer que aparezcan de una cierta manera, o actúen de determinada forma. Las etiquetas que delimitan un fragmento de contenido pueden hacer que dicho contenido enlace con otra página, ponga una palabra en cursiva, etcétera.

Cada elemento tiene dos partes, una al comienzo y otra al final, y el contenido se coloca en medio:

<etiqueta>Contenido...</etiqueta>

El elemento HTML quedará definido por todo ese bloque, desde la etiqueta de apertura, hasta la etiqueta de cierre. Por ejemplo:

<h1>Mi primer encabezado</h1>
<p>Mi primer párrafo</p>

Las etiquetas HTML no distinguen entre mayúsculas y minúsculas. Así que se pueden escribir tanto en mayúsculas como en minúsculas. Por ejemplo, una etiqueta <title> se puede escribir como <title><TITLE><Title><TiTle>, etc., y funcionará correctamente. Sin embargo, se recomienda escribir todas las etiquetas en minúsculas para mantener la coherencia y legibilidad, entre otros motivos.

Elementos vacíos

No todos los elementos siguen el patrón de etiqueta de apertura, contenido y etiqueta de cierre. Algunos elementos consisten solo en una etiqueta única, que se utiliza generalmente para insertar/incrustar algo en el documento en el lugar donde se quiera incluir. Por ejemplo, podemos utilizar el elemento  <br> para insertar un salto de línea, y el elemento <img> para insertar una imagen en la página de la siguiente forma:

<p>Mi perro es un gruñón.<br />Tiene muy malas pulgas.</p>

Atributos

Los elementos también pueden tener atributos. Los atributos contienen información extra sobre el elemento. Por ejemplo, con siguiente código podemos utilizar el atributo class para asignar al elemento un identificador que se puede utilizar para dotarlo de información de estilo:

<p class="estilo">Mi perro es un gruñón</p>

Para ver otro ejemplo, podemos utilizar el siguiente código para saltar a la página https://www.wikipedia.org (la etiqueta <a> define un hipervínculo, y el atributo href especifica la url donde queremos saltar):

<a href="https://www.wikipedia.org">Wikipedia</a>

Es muy importante que tengamos en cuenta que un atributo deberá tener:

  • Un espacio entre este y el nombre del elemento. Para un elemento con más de un atributo, los atributos también deben estar separados por espacios.
  • El nombre del atributo, seguido por un signo igual.
  • Un valor del atributo, rodeado de comillas de apertura y cierre.

Omitir comillas en valores de atributos

Cuando observas diferentes páginas web, puedes encontrarte con todo tipo de estilos de etiquetado extraños, que incluyen valores de atributos sin comillas. Esto se permite en ciertas circunstancias, pero provocará un error en otras. Por ejemplo, si volvemos a revisar el ejemplo del enlace, sería posible escribir una versión básica con solo el atributo href, así:

<a href=https://www.wikipedia.org/>Wikipedia</a>

Sin embargo, el código no sería correcto si añadimos el atributo title también sin comillas:

<a href=https://www.wikipedia.org/ title=La página de inicio de Wikipedia>Wikipedia</a>

En este punto el navegador interpretará mal el código y pensará que el elemento <a> tiene atributos incorrectos y esto causará errores o comportamientos inesperados. Por ello es muy importante incluir siempre las comillas en los atributos. Evitarás muchos problemas y además obtendrás un código más legible.

Comillas simples o dobles

En esta unidad todos los valores de los atributos se han incluido en comillas dobles. Sin embargo, se pueden ver comillas simples en algún código HTML. Es una cuestión de estilo. Puedes elegir libremente cuál prefieres. Ambas líneas de código son equivalentes:

<a href="http://www.ejemplo.com">Un enlace a mi ejemplo.</a>

<a href='http://www.ejemplo.com'>Un enlace a mi ejemplo.</a>

Asegúrate de no mezclar ambos tipos de comillas. El siguiente ejemplo muestra un código incorrecto:

<a href="http://www.ejemplo.com'>Un enlace a mi ejemplo.</a>

Si utilizas un tipo de comillas en tu documento HTML, puedes incluir el otro tipo de comillas para tus valores de atributo sin que esto te cause problemas:

<a href="http://www.ejemplo.com" title="¿A que es 'divertido'">Un enlace a mi ejemplo.</a>

Sin embargo, si deseas anidar unas comillas dentro de otras del mismo tipo (ya sea simples o dobles), tendrás que utilizar entidades HTML para las comillas. Por ejemplo, el siguiente código no va a funcionar:

<a href='http://www.ejemplo.com' title='¿A que es 'divertido'?'>Un enlace a mi ejemplo.</a>

Así que tendrás que hacer esto:

<a href='http://www.ejemplo.com' title='¿A que es &apos;divertido&apos;?'>Un enlace a mi ejemplo.</a>

Internet y páginas web

Para que un usuario pueda obtener y leer el contenido de una página web, se deben seguir algunos pasos básicos:

  • Para crear páginas web, el desarrollador tiene que crear archivos escritos en HTML (con cualquier editor de texto) y colocarlos en un servidor web (por ejemplo, usando un cliente ftp).
  • El código HTML de una página web le dice al navegador lo que necesita saber para mostrar la página. Si el programador completa el desarrollo de la página web correctamente, las páginas incluso se mostrarán bien en dispositivos móviles y de escritorio.
  • Una vez que los archivos se colocan en un servidor web, cualquier navegador puede recuperar las páginas web a través de Internet.
  • Finalmente, los usuarios utilizarán un ordenador o cualquier otro dispositivo conectado a Internet para visualizar la página web mediante un navegador.

El servidor web

Un servidor web es simplemente un ordenador conectado a Internet esperando peticiones de navegadores:

  • Los programadores almacenan HTML, archivos, imágenes, sonidos y otros tipos de archivos.
  • Los navegadores solicitan páginas HTML u otros recursos, como imágenes.
  • Si el servidor puede localizar los recursos, los envía a los navegadores.

El navegador web

Un navegador simplemente es una aplicación que se encarga de mostrar páginas web. El código HTML realizará el siguiente recorrido:

  • El código HTML parte del servidor, quien se encarga de enviar páginas web completas a los navegadores.
  • Los navegadores recuperan las páginas.
  • Los navegadores muestran las páginas HTML a los usuarios finales.

Elementos <!doctype> y <html>

En sus inicios, cuando el HTML llevaba poco tiempo (alrededor de 1991-1992), los doctypes servían como enlaces al conjunto de reglas que la página HTML debía seguir para que fuera considerado buen HTML. Un elemento doctype de aquella época podía parecerse a esto:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

En la actualidad se ignora y se considera un legado histórico que hay que incluir para que todo funcione correctamente. <!DOCTYPE html> es la secuencia de caracteres más corta que se acepta como elemento doctype válido, que indica estamos utilizando la última versión de HTML (HTML5).

En segundo lugar debemos utilizar obligatoriamente el elemento <html> (elemento raíz), que incluirá todo el contenido de la página.

En resumen, todos los documentos HTML deben comenzar con una declaración de tipo de documento seguida del código de la página web en sí entre las etiquetas <html> and </html>:

<!doctype html>
<html lang="es">
...
</html>

El elemento <head>

Este elemento actúa como contenedor para todos los parámetros que se quieran incluir en el documento HTML que no serán visibles a los visitantes de la página. Incluye cosas como palabras clave y la descripción de la página que se quieran mostrar en los resultados de búsqueda, así como la hoja de estilo para formatear nuestro contenido, declaraciones de codificación de caracteres y más. Por ejemplo:

<!doctype html>
<html lang="es">
  <head>
    <!-- Etiquetas obligatorias -->
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />

    <!-- Hoja de estilos (CSS) de Bootstrap -->
    <link rel="stylesheet" href="bootstrap.min.css" />
    ...
  </head>  
  ...
</html>

El elemento <meta>

Metadata

Es importante que las páginas web contengan una lista de elementos de metadatos para proporcionar a los motores de búsqueda (por ejemplo, Google, DuckDuckGo, Bing, etc.) información semántica como el autor, palabras clave, descripción, etc. Dado que este tipo de información no se muestra al usuario directamente por el navegador, los elementos de metadatos deben colocarse dentro del elemento <head>:

<head>
  <meta name="description" content="My página web" />
  <meta name="keywords" content="HTML, CSS, JavaScript" />
  <meta name="author" content="Fernando" />
</head>

Charset

Uno de los elementos <meta> más importantes es el que define la especificación del charset. Es muy habitual especificar que el documento HTML usará la codificación UTF-8, que incluye la gran mayoría de caracteres de todos los idiomas humanos escritos. De esta forma, el navegador mostrará correctamente cualquier contenido textual que pongamos en el documento, tales como caracteres á, é, ü, ñ, etc.:

<meta charset="utf-8" />

Viewport

HTML5 introdujo un método para permitir a los desarrolladores controlar ciertos parámetros del área visible de la página web (viewport), a través de la etiqueta <meta> . El viewport puede variar dependiendo del dispositivo, y puesto que hay diferencias entre un móvil y una pantalla de ordenador, es conveniente establecer el comportamiento adecuado para que nuestra página web se muestre correctamente en cualquier dispositivo. Podemos utilizar por ejemplo la siguiente línea de código en todas nuestras páginas web:

<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />

El elemento <title>

Este elemento establece el título de la página web, que es el título que aparece en la pestaña del navegador en la que se carga dicha página. El título también se utiliza para describir la página cuando se marca como favorita. Se puede definir de la siguiente forma:

<title>Mi primera página web</title>

El elemento <link>

El elemento <link> especifica que el documento actual se encuentra enlazado con un recurso externo. El uso más habitual de este elemento es el de aplicar hojas de estilos al documento.

Mediante el atributo <rel> especificaremos el tipo de relación del documento enlazado con el actual. El atributo debe ser una lista de tipos de enlaces separados por espacio. El uso más común para este atributo es especificar el enlace a una hoja de estilos externa: el atributo rel se establece con valor stylesheet, y el atributo href se establece con la URL de la hoja de estilos externa para dar formato a la página:

<link rel="stylesheet" href="estilos.css" />

El elemento <script>

El elemento <script> se utiliza para incluir código ejecutable o datos. Se usa principalmente para incluir o enlazar código JavaScript, aunque también se puede usar para incluir otros lenguajes de programación tales como  WebGL o incluso también datos en formato JSON. Utilizando el atributo <src> podemos especificar la url de un fichero externo (esto se puede usar como alternativa a incluir el código JavaScript directamente en nuestro documento). Por ejemplo, en el caso de querer utilizar código JavaScript en nuestra página web, procederíamos de la siguiente forma:

<!-- Fichero externo con código JavaScript -->
<script src="javascript.js"></script>
...
<!-- Definimos código JavaScript dentro del fichero HTML -->
<script>
function miFuncion() {
  document.getElementById("demo").innerHTML = "¡Hola JavaScript!";
}
</script>

El elemento <body>

El elemento <body> contiene todo el contenido que quieres mostrar a los usuarios cuando visitan tu página, ya sea texto, imágenes, vídeos, juegos, pistas de audio reproducibles o cualquier otra cosa. Se abre justo después de cerrar el elemento <head> y se debe cerrar justo antes de cerrar el elemento <html>. Por ejemplo, con el siguiente código, el navegador sólo mostrará en pantalla el texto «¡Hola mundo!»:

<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8" />
  <meta name="description" content="Mi primera página web" />
  <meta name="keywords" content="HTML, CSS, JavaScript" />
  <meta name="author" content="Fernando" />
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
  <title>Página de ejemplo</title>
</head>
<body>
  ¡Hola mundo!
</body>
</html>

Debemos matizar que dentro de una página web no podemos poner directamente los caracteres “<” y “>” porque el navegador los confundiría con los caracteres que se utilizan para la creación de etiquetas.

Para cierto tipo de caracteres, es necesario utilizar otra sintaxis que suele constar de un ampersand seguido de su nombre identificativo y un punto y coma. Por ejemplo:

&amp;&
&lt;<
&quot;
&gt;>
&copy;©
&divide;÷
&euro;

Ejercicio propuesto: Hola mundo

Crea una página web para mostrar el texto «¡Hola mundo, mi nombre es … !» dentro del elemento <body>. Además, deberás utilizar el elemento <head> para mostrar el texto «Saludos» como título en la pestaña del navegador. Para finalizar, añade y completa la página web con el resto de elementos <meta> (charset, viewport, author, description y keywords), y comprueba el resultado en el navegador.

Párrafos y secciones

Para que el contenido textual de una página web resulte más legible, es muy importante utilizar párrafos y secciones.

El texto de cada párrafo se debe colocar entre las etiquetas <p> y </p>, y para indicar el comienzo de cada sección de nuestro contenido, disponemos de encabezados de varios niveles: <h1>, <h2>, <h3>, <h4>, <h5> y <h6> (del más grande al más pequeño). Podemos utilizar estos elementos de la siguiente forma:

...
<h1>Encabezado de primer nivel</h1>
<p>Mi primer párrafo</p>
<p>Mi segundo párrafo</p>
<p>Mi tercer párrafo</p>

<h2>Encabezado de segundo nivel</h2>
<p>Mi cuarto párrafo</p>
<p>Mi quinto párrafo</p>
...

Ejercicio propuesto: Encabezados de diferente nivel

Utilizando las etiquetas HTML adecuadas, crea una página web que tenga seis encabezados con el texto «Hola». Comienza con el encabezado más importante (el más grande) y ves bajando hasta el de menor relevancia (el más pequeño).

Ejercicio propuesto: Párrafos

Crea una página web para mostrar diferentes párrafos utilizando las etiquetas <p> y </p>, todos ellos con diferentes líneas de cualquier texto. A continuación, añade el resto de elementos <meta> (charset, viewport, author, description, keywords y title), y comprueba el resultado utilizando tu navegador.

Ejercicio propuesto: Encabezados y párrafos

Crea una página web para mostrar varios encabezados de diferentes niveles, utilizando las etiquetas <h1> a <h6>. Añade además varios párrafos (encerrados entre las etiquetas <p> y </p> ) con cualquier texto, colocando un párrafo diferente después de cada encabezado. Para concluir, añade el resto de elementos <meta> (charset, viewport, author, description, keywords and title), y comprueba el resultado en tu navegador.

Ejercicio propuesto: Párrafos dentro de la misma sección

Utilizando las etiquetas correspondientes (<h1> y <p>), añade el código HTML necesario para crear una página web que muestre el siguiente texto con el formato correcto:

Alicante

Alicante (en valenciano y cooficialmente Alacant) es una ciudad y un municipio de España, capital de la provincia homónima, en la Comunidad Valenciana. Ciudad portuaria, está situada en la costa mediterránea. Con 337.482 habitantes (INE, 2020), es el segundo municipio más poblado de la comunidad autónoma y el undécimo del país.

Forma una conurbación de 468.782 habitantes con muchas de las localidades de la comarca del Campo de Alicante: San Vicente del Raspeig, San Juan de Alicante, Muchamiel y Campello. Estadísticamente se asocia también con el área metropolitana de Alicante-Elche, que cuenta con 757 085 habitantes. 

Ciudad eminentemente turística y de servicios, es uno de los destinos turísticos más importantes de España.

Ejercicio propuesto: Encabezados, niveles y párrafos

Utiliza las etiquetas correctas (<h1>, <h2>, <h3> y <p>) sobre el texto que aparece a continuación:

Valencia

Valencia (en valenciano y oficialmente, València) ​es un municipio y una ciudad de España, capital de la provincia homónima y de la Comunidad Valenciana. Con una población de 800.215 habitantes (2020), que sube a 1.581.057 habitantes (2020) si se incluye su espacio urbano.​ Es la tercera ciudad y área metropolitana más poblada de España, por detrás de Madrid y Barcelona.

Geografía

Localización

La ciudad de Valencia se encuentra en la costa mediterránea de la península ibérica, sobre la gran llanura aluvial de los ríos Júcar y Turia, justo en el centro del golfo de Valencia. La ciudad primitiva estaba ubicada a unos cuatro kilómetros del mar, en una isla fluvial del Turia. Los montes más cercanos a la ciudad son algunas de las últimas estribaciones del sistema Ibérico en la Comunidad Valenciana, como el Cabeçol de El Puig y la sierra Calderona, a unos 12 km y 25 km al norte de la ciudad respectivamente.

Clima

Valencia cuenta con un clima mediterráneo suave y ligeramente lluvioso durante los inviernos y caluroso y seco durante los veranos. De acuerdo con los criterios de la clasificación climática de Köppen, Valencia tiene un clima de transición entre los climas mediterráneo y semiárido cálido. La temperatura media anual es de 18.4 °C.

El elemento <br>

El elemento HTML line break <br> produce un salto de línea en el texto (retorno de carro). Es útil para escribir un poema o una dirección, donde la división de las líneas es muy relevante.

Debemos tener en cuenta que no se debe utilizar <br> para incrementar el espacio entre líneas de texto; para ello se debe utilizar la propiedad margin de CSS o el elemento <p>.

Como se puede observar en el siguiente ejemplo, se utiliza un elemento <br /> cada vez que queremos cambiar de línea. El texto que coloquemos después del elemento <br /> aparecerá al comienzo de la siguiente línea:

<p> 
  Mozilla Foundation<br />
  1981 Landings Drive<br />
  Building K<br />
  Mountain View, CA 94043-0801<br />
  USA
</p>

Ejercicio propuesto: Poemas

Crea una página web que contenga diversos poemas de diversos autores, como el que tienes a continuación. Debes utilizar <h1> y <h2> para los títulos, y párrafos y saltos de línea para el resto.

Nota: Deberías utilizar <h1> para el encabezado principal y <h2> para el resto de encabezados. Ten en cuenta también que no puedes utilizar <br /> para crear márgenes entre cada poema. Deberás usar párrafos que contengan cada poema, y saltos de línea para cada verso.
POEMAS

Gustavo Adolfo Bécquer

Del salón en el ángulo oscuro,
de su dueña tal vez olvidada,
silenciosa y cubierta de polvo,
veíase el arpa.

¡Cuánta nota dormía en sus cuerdas,
como el pájaro duerme en las ramas,
esperando la mano de nieve
que sabe arrancarlas!

¡Ay!, pensé; ¡cuántas veces el genio
así duerme en el fondo del alma,
y una voz como Lázaro espera
que le diga «Levántate y anda»!

...

Ejercicio propuesto: Direcciones

Crea una página web para mostrar diferentes direcciones, similares a la que tienes a continuación (debes usar encabezados de tipo <h1> y <h2> , párrafos y saltos de línea).

Nota: Deberías utilizar un encabezado <h1> para el título principal y <h2> para el resto de encabezados. Ten en cuenta también que no debes utilizar <br /> para crear márgenes entre cada dirección. Al igual que el ejercicio anterior, utiliza párrafos para cada dirección y saltos de línea para el resto.
DIRECCIONES

Mozilla

331 E. Evelyn Avenue
Mountain View, CA
94041
USA

...

El elemento <pre>

El elemento <pre> (o Texto HTML Preformateado) representa texto que mantiene la estructura que se utilizó en el editor. El texto en este elemento típicamente se muestra con una fuente fija o monoespaciada (no proporcional), exactamente como se mostraba en el archivo mientras se editaba. Los espacios dentro de este elemento también se muestran como se escribieran en el editor. Por ejemplo:

<pre>
 -------------------------------
 | Soy un experto en mi campo. |
 -------------------------------
         \   ^__^ 
          \  (oo)\_______
             (__)\       )\/\
                 ||----w |
                 ||     ||
</pre>

Ejercicio propuesto: ASCII art

Crea un documento para mostrar diversos dibujos utilizando caracteres, de forma similar al ejemplo.

Nota: Puedes encontrar diversos ejemplos en los siguientes enlaces: https://www.asciiart.eu/ , https://en.wikipedia.org/wiki/ASCII_art , https://asciiart.website/ , https://fsymbols.com/text-art/ , etc.
 
  -------------------------------
  | Soy un experto en mi campo. | 
  -------------------------------
        \   ^__^
         \  (oo)\_______ 
            (__)\       )\/\ 
                ||----w | 
                ||     ||


         .^.    .^.
        (   \  (   \
         \   }  \   }
        ( `  { ( `  {
         \   }  \   }
         ( `  \ ( `  \
          \   }  \   }
         ( `  { ( `  {
        .-\   |-^-. /
       (  .\ /     '.
        '/  O _______\
         | .-'#### ###
       ,-./  # / \ / \___
      { ,-     \@/_\@'   '.
      { `.       .'        }
       `._   /    )._     _"
          \   `--' __"---"
           `. /  ,'  \  \
   Ahas      /  /     |  )
            |  /      ; /
             \/ '-,,-'\/


$$$_____$$$$$$$_$$$$$$$_$$$_______$$$_$$$$$$$$$$
$$$____$$$____$$$____$$$_$$$_____$$$__$$$_______
$$$____$$$_____$_____$$$_$$$_____$$$__$$$_______
$$$_____$$$_________$$$___$$$___$$$___$$$$$$$$__
$$$______$$$_______$$$_____$$$_$$$____$$$_______
$$$_______$$$_____$$$______$$$_$$$____$$$_______
$$$$$$$$$___$$$$$$$_________$$$$$_____$$$$$$$$$$

...

Test

Comprueba tus conocimientos con este test sobre formato de texto y otros conceptos relacionados con esta unidad.

Cuentos para ser escuchados 2

La muy esperada continuación del exitoso libro Cuentos para ser escuchados

Acompaña a nuestros protagonistas en estos doce nuevos cuentos.

Emotivos relatos para potenciar la capacidad de superación

Cuentos infantiles educativos que tratan por un lado temas cotidianos tales como la sostenibilidad del medio en el que vivimos y los beneficios del trabajo en equipo, y por otro lado abordan temas sensibles tales como la pobreza, los complejos, la discapacidad y las enfermedades raras, para que los más pequeños crezcan acostumbrados a la diversidad y no les dé miedo lo diferente.

Conmovedoras historias que ensalzan el valor de la amistad y la familia

Nos ofrecen un punto de vista entrañable y espontáneo ante diversas situaciones inspiradas en hechos reales a las que se enfrentan nuestros protagonistas, utilizando un lenguaje adaptado a los niños que consigue emocionar a los adultos.

  • El discurso. Conoceremos a un niño con una actitud muy positiva, capaz de mejorar el estado de ánimo de quienes le rodean.
  • Con sus ojos y los míos. Un cuento dentro de otro cuento… Dos amigas inseparables con fantásticas habilidades… ¿Ficción o realidad?
  • La propuesta. Si todos viéramos el mundo con los ojos de un niño, no habrían problemas grandes ni soluciones imposibles.
  • ¿Y en vacaciones? El protagonista se pone triste cuando comienzan las vacaciones… ¿por qué?
  • Ocaso tecnológico. La evolución de las nuevas tecnologías podría llegar a conducirnos a un futuro incierto. Las nuevas generaciones deberán encontrar el equilibrio entre uso y abuso.
  • El coche solar. Un equipo de ingenieros debe aprender una lección que no está en sus libros de texto.
  • Villa sonrisas. Una pequeña con mucho desparpajo elegirá el nuevo nombre del pueblo en el que vive.
  • Secretos en reunión. Descubre por qué Julia y Andrés se han enfadado. Es importante conocer el punto de vista de quienes nos rodean.
  • ¿Has oído eso? Esta historia te pondrá los pelos de punta… ¿quieres conocer el final?
  • El uniforme. Una conmovedora historia dedicada a esos amigos incondicionales que siempre están a nuestro lado, pase lo que pase, sin pedir nunca nada a cambio.
  • Papá «pesao». Un cuento muy típico, y un papá muy pesado.
  • Los candidatos. Un fantástico reto que conducirá al protagonista a resolver diversos acertijos con ayuda de sus padres.

Otros libros del autor

Todos los beneficios son para Alianza española de familias de VHL

Comprando el libro además ponemos un granito de arena para ayudar a la asociación de enfermos de von Hippel-Lindau (alianzavhl.org). Por ello, no sólo pretendemos regalar ilusión a quien lea el libro, sino a todos aquellos enfermos y familiares que están esperando que les echemos una mano, ya que todos los beneficios de la venta del libro van destinados íntegramente a ellos.

Una app muy sencilla para guardar sitios interesantes con ubicaciones y fotografías con IONIC 5+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

Vamos a instalar la versión 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.

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

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 sólo 100 líneas de código

Veremos que al utilizar IONIC 5 + Angular, con sólo 100 líneas de código (sumando HTML y Typescript) 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 --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

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 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:

reorder: boolean;
list: any;
text: string;

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

  • 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 Cordova):

...
  navigator.camera.getPicture(function success(data) {
    list.unshift({ name:this.text, img:"data:image/jpeg;base64,"+data, url:url });
    save();     
  }, function error(msg) {
  }, { targetWidth:100, navigator.camera.DestinationType.DATA_URL });
...

El fichero «home.page.ts» completo

import { Component } from '@angular/core';
import { LoadingController } from '@ionic/angular';

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

export class HomePage {
  reorder: boolean;
  list: any;
  text: string;

  constructor(public loadingController:LoadingController) {
    this.reorder = false;
    this.text = "";
    this.list = localStorage.getItem('places-list');
    if (this.list!=='undefined' && this.list!==null) this.list = JSON.parse(this.list);
    else this.list = [];
  }

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

    let text = this.text, list = this.list, save = this.save;
    this.text = "";

    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) => {
          list.unshift({ name:text, img:data.target.result, url:url });
          loading.dismiss();
          save(list);
        }
        reader.readAsDataURL(event.target.files[0]);
      }
      else {
        (<any>navigator).camera.getPicture(function success(data) { 
          list.unshift({ name:text, img:"data:image/jpeg;base64,"+data, url:url });
          loading.dismiss();
          save(list);
        }, function error(msg) {
          loading.dismiss();
        }, { targetWidth:100, destinationType:(<any>navigator).camera.DestinationType.DATA_URL });
      }
    });
  }

  save(list) {
    localStorage.setItem('places-list', JSON.stringify(list));
    location.href = "index.html";
  }

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

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

Compilando la aplicación

El fichero config.xml (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 y acceder a la cámara, añadiremos los plugins correspondientes al fichero config.xml (más información aquí y aquí):

...
    <plugin name="cordova-plugin-geolocation" />
    <plugin name="cordova-plugin-camera" />
...

Debemos recordar que si compilamos la aplicación utilizando PhoneGap Build, el archivo config.xml se deberá guardar dentro de la carpeta www.

Por último, tengamos en cuenta 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.

El fichero index.html

Si utilizamos PhoneGap Build deberemos modificar también el fichero src/index.html para enlazar el fichero cordova.js (no tenemos que crearlo nosotros, sólo enlazarlo, ya que dicho archivo se creará automáticamente al compilar el código con PhoneGap Build):

...
<body>
  <app-root></app-root>
  <script type="text/javascript" src="cordova.js"></script>
</body>
...

El resultado

Puedes hacer clic aquí para probar la aplicación propuesta. Se puede observar que el mismo código que hemos estado desarrollando se puede subir a cualquier servidor y el resultado se puede visualizar perfectamente en el navegador como cualquier otra página web.

Una app muy sencilla para guardar sitios interesantes con ubicaciones y fotografías con IONIC 5+JavaScript

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 ejecutar la 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 5, con menos de 100 líneas de código JavaScript 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.

Además de poder introducir 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.

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 onclick="toggleReorder()">
  ...
</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"></ion-input>
</ion-item>
<ion-item id="file">
  <input type="file" accept="image/*" onchange="addItem(event)" />
  <ion-icon slot="end" name="camera" onclick="addItem()"></ion-icon>
</ion-item>  

Destacamos el uso del elemento <input type="file">, que automatiza la creación del campo de tipo fichero, y nos permitirá elegir fácilmente una imagen para asociarla al lugar que queramos dejar registrado.

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-sliding>
  <ion-item href="...">
    <ion-thumbnail slot="start"><img src="..." /></ion-thumbnail>
    ...
    <ion-reorder slot="end"></ion-reorder>
  </ion-item>
  <ion-item-options side="start">
    <ion-item-option color="danger" onclick="deleteItem(...)">
      <ion-icon slot="icon-only" name="trash"></ion-icon>
    </ion-item-option>
  </ion-item-options>
</ion-item-sliding>

Mostrando todos los elementos

Bastará con hacer un simple bucle para mostrar la lista completa de lugares:

getList().forEach((item, index) => {
  document.querySelector('ion-reorder-group').innerHTML += 
    `<ion-item-sliding>
        <ion-item href='` + item.url + `'>
          <ion-thumbnail><img src="` + item.img + `" /></ion-thumbnail>` + 
          item.name + `<ion-reorder></ion-reorder>
        </ion-item>
        <ion-item-options>
          <ion-item-option onclick="deleteItem(` + index + `)">
            <ion-icon name="trash"></ion-icon>
          </ion-item-option>
        </ion-item-options>
     </ion-item-sliding>`;
});

Sólo necesitamos recorrer la lista utilizando por ejemplo un bucle forEach, que además nos proporcionará cada elemento item del array, así como el índice index de cada uno de ellos.

El fichero «index.html» completo

<!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="cordova.js"></script>
  <script src="places.js"></script>
  <title>Places!</title>
</head>
<body onload="onLoad()">
  <ion-app>
    <ion-header>
      <ion-toolbar color="primary">
        <ion-title>Places!</ion-title>
        <ion-buttons slot="primary">
          <ion-button onclick="toggleReorder()">
            <ion-icon slot="icon-only" name="reorder-four"></ion-icon>
          </ion-button>
        </ion-buttons>
      </ion-toolbar>       
    </ion-header>
    <ion-content>
      <ion-list lines="full">
        <ion-item color="light">
          <ion-input placeholder="Enter description"></ion-input>
        </ion-item>
        <ion-item color="light" id="file">
          <input type="file" accept="image/*" onchange="addItem(event)" />
          <ion-icon slot="end" name="camera" onclick="addItem()"></ion-icon>
        </ion-item>    
        <ion-reorder-group disabled="false">
        </ion-reorder-group>
      </ion-list>
    </ion-content>
  </ion-app>
</body>
</html>

Código JavaScript

Accediendo a los elementos principales

En este ejercicio sólo necesitamos gestionar una lista de lugares, con lo que desarrollaremos funciones para leer, actualizar e imprimir dicha lista, utilizando para ello localStorage, como en ejercicios anteriores:

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

function saveList(list) {
    localStorage.setItem('places-list', JSON.stringify(list));
    printItems();
}

function printItems() {
    document.querySelector('ion-reorder-group').innerHTML = "";

    getList().forEach((item, index) => {
        document.querySelector('ion-reorder-group').innerHTML += ... ;
    });
}

Al iniciar la aplicación

Al iniciar la aplicación capturaremos el evento de IONIC que nos indicará cuándo debemos reordenar los elementos de la lista, y además, imprimiremos los lugares que tengamos guardados de una ejecución anterior:

function onLoad() {
    document.addEventListener('ionItemReorder', (event) => { moveItem(event.detail); });
    printItems();
}

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í):

function addItem(event) {
  const loading = document.createElement('ion-loading');
  ...
  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" accept="image/*" onchange="addItem(event)" />

...

let reader = new FileReader();
reader.onload = (data) => {
    saveItem({ name:text, img:data.target.result, url:url });
    event.target.value = '';
}
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 Cordova):

...
navigator.camera.getPicture(function success(data) {
  saveItem({ name:text, img:"data:image/jpeg;base64,"+data, url:url });       
}, function error(msg) {
  alert(msg);
}, { targetWidth:100, destinationType:Camera.DestinationType.DATA_URL });
...

El fichero «places.js» completo

function onLoad() {
    document.addEventListener('ionItemReorder', (event) => { moveItem(event.detail); });
    printItems();
}

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

function saveList(list) {
    localStorage.setItem('places-list', JSON.stringify(list));
    printItems();
}

function printItems() {
    document.querySelector('ion-reorder-group').innerHTML = "";

    getList().forEach((item, index) => {
        document.querySelector('ion-reorder-group').innerHTML +=
        `<ion-item-sliding>
            <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" onclick="deleteItem(`+index+`)">
                    <ion-icon slot="icon-only" name="trash"></ion-icon>
                </ion-item-option>
            </ion-item-options>
        </ion-item-sliding>`;
    });
}

function addItem(event = false) {
    const loading = document.createElement('ion-loading');
    loading.duration = 15000;
    document.querySelector('ion-app').appendChild(loading);
    loading.present();

    navigator.geolocation.getCurrentPosition(pos => {
        let text = document.querySelector('ion-input').value;
        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) => {
                saveItem({ name:text, img:data.target.result, url:url });
                event.target.value = '';
                loading.dismiss();
            }
            reader.readAsDataURL(event.target.files[0]);
        }
        else {
            loading.dismiss();
            navigator.camera.getPicture(function success(data) {
                saveItem({ name:text, img:"data:image/jpeg;base64,"+data, url:url });       
            }, function error(msg) {
                alert(msg);
            }, { targetWidth:100, destinationType:Camera.DestinationType.DATA_URL });
        }
    });
}

function saveItem(item) {
    let list = getList();
    list.unshift(item);
    saveList(list);    
}

function deleteItem(item) { 
    document.querySelector('ion-list').closeSlidingItems();
    let list = getList();
    list.splice(item, 1);
    saveList(list);
}

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

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

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 el mismo archivo zip que el código fuente de nuestro proyecto.

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í):

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

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

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

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.

El archivo «config.xml» completo

<?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.places"
    versionCode = "1"
    version     = "0.0.1" >

  <name>Places!</name>

  <description>
      A simple application to save important places.
  </description>

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

  <access origin="*" />
  <allow-navigation href="*" />
  <allow-intent href="*" />

  <plugin name="cordova-plugin-whitelist" />
  <plugin name="cordova-plugin-splashscreen" />
  <plugin name="cordova-plugin-vibration" />
  <plugin name="cordova-plugin-geolocation" />
  <plugin name="cordova-plugin-camera" />

  <icon src="icon.png" />
  <splash src="splash.png" />

  <preference name="phonegap-version" value="cli-9.0.0" />

</widget>

Generando el archivo APK

Para instalar la aplicación en nuestros dispositivos móviles, tenemos que compilar el código fuente, con lo que deberemos incluir los siguientes archivos en el fichero zip que subiremos a la página web de PhoneGap:

  • index.html
  • places.js
  • config.xml
  • icon.png
  • splash.png

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.

IONIC 5+Angular+PhoneGap Build

Una vez hayas probado tu aplicación en el navegador con el comando ionic serve, puedes compilar el código para subirlo a PhoneGap Build y obtener el fichero .apk para instalarlo en tus dispositivos móviles. Bastará con seguir los pasos que se indican a continuación.

En primer lugar debemos cambiar el archivo tsconfig.json para que el campo target tenga el valor es5 (más información en stackoverflow):

...
    "target": "es5"
...

El segundo paso consistirá en ejecutar el siguiente comando desde el directorio del proyecto (puedes encontrar más información aquí):

ionic build --prod -- --base-href .

Ese comando generará una carpeta www dentro de tu proyecto con todo el código web necesario. A continuación deberemos añadir allí el fichero config.xml, incluyendo el plugin cordova-plugin-ionic-webview (puedes obtener más información en la página de github de dicho plugin):

...
<plugin name="cordova-plugin-ionic-webview" />
...

Un ejemplo del fichero config.xml (que debemos incluir en la carpeta www) 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="*" />
    <allow-navigation href="*" />
    <allow-intent href="*" />

    <plugin name="cordova-plugin-whitelist" />
    <plugin name="cordova-plugin-ionic-webview" />
    <plugin name="cordova-plugin-geolocation" />
    <plugin name="cordova-plugin-camera" />

    <preference name="phonegap-version" value="cli-9.0.0" />
</widget>

Si deseamos utilizar algún plugin (la cámara, por ejemplo) también deberemos modificar el fichero src/index.html para enlazar el fichero cordova.js (no tenemos que crearlo nosotros, sólo enlazarlo, ya que dicho archivo se creará automáticamente al compilar el código con PhoneGap Build):

...
<body>
  <app-root></app-root>
  <script type="text/javascript" src="cordova.js"></script>
</body>
...

Como último paso sólo deberemos comprimir todos los archivos dentro de la carpeta www (index.html, config.xml, …) para generar el archivo zip que subiremos a PhoneGap Build.

Lista de tareas con IONIC 5+Angular

El objetivo: IONIC + Angular

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

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

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

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

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

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

La funcionalidad de la aplicación

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

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

La pantalla principal

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

La pantalla de detalles

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

Primeros pasos

Requisitos

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

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

node --version
npm --version

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

npm install -g ionic

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

Código base de la aplicación

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

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

Al ejecutar el comando, IONIC nos informará del proceso con algunos avisos y  si todo va bien, al final nos mostrará un mensaje similar al siguiente:

[INFO] Next Steps:
       
       - Go to your newly created project: cd ./tareas
       - Run ionic serve within the app directory to see your app
       - Build features and components: https://ion.link/scaffolding-docs
       - Get Ionic DevApp for easy device testing: https://ion.link/devapp

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

cd tareas

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

ionic generate page AddEditItem
ionic generate service list

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

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

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

Probando la aplicación en el navegador

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

ionic serve

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

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

Funciones auxiliares

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

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

El fichero «list.service.ts» completo

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

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

  list: any;

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

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

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

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

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

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

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

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

La pantalla principal

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

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

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

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

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

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

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

<ion-tab-bar #myTabs color="primary" [selectedTab]="tabs[0].label">
  <ion-tab-button *ngFor="let tab of tabs; let i=index" (click)="setTab(i)" [tab]="tab.label">
    <ion-label>{{tab.label}}</ion-label>
    <ion-icon [name]="tab.icon"></ion-icon>
  </ion-tab-button>
</ion-tab-bar>

Y para mostrar la lista de tareas:

<ion-list #myList lines="full">
  <ion-reorder-group [disabled]="!reorder" (ionItemReorder)="moveItem($event.detail)">
    <ion-item-sliding *ngFor="let item of tabs[tabIndex].list; let i=index">
      <ion-item [routerLink]="['/add-edit-item', { tab:tabIndex, item:i }]">
        <ion-label class="ion-text-wrap">
          <h2>{{item.task}}</h2>
          <p>{{item.date.substring(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" (click)="deleteItem(i)">
          <ion-icon slot="icon-only" name="trash"></ion-icon>
        </ion-item-option>
      </ion-item-options>
    </ion-item-sliding>
  </ion-reorder-group>
</ion-list>

Respecto a la versión con JavaScript, la mayor diferencia la observamos en las siguientes líneas:

<ion-item-sliding *ngFor="let item of tabs[tabIndex].list; let i=index">

...

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

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

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

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

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

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

El fichero «home.page.html» completo

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

<ion-content>
  <ion-list #myList lines="full">
    <ion-reorder-group [disabled]="!reorder" (ionItemReorder)="moveItem($event.detail)">
      <ion-item-sliding *ngFor="let item of tabs[tabIndex].list; let i=index" >
        <ion-item [routerLink]="['/add-edit-item', { tab:tabIndex, item:i }]"  >
          <ion-label class="ion-text-wrap">
            <h2>{{item.task}}</h2>
            <p>{{item.date.substring(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" (click)="deleteItem(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>

<ion-footer>
  <ion-tab-bar #myTabs color="primary" [selectedTab]="tabs[0].label">
    <ion-tab-button *ngFor="let tab of tabs; let i=index" (click)="setTab(i)" [tab]="tab.label">
      <ion-label>{{tab.label}}</ion-label>
      <ion-icon [name]="tab.icon"></ion-icon>
    </ion-tab-button>
  </ion-tab-bar>
</ion-footer>

El fichero «home.page.ts» completo

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

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

export class HomePage {
  @ViewChild('myTabs', {static: false}) tabRef: IonTabBar;
  @ViewChild('myList', {static: false}) listRef: IonList;
  tabs: any;
  tabIndex: number;
  reorder: boolean;

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

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

  setTab(tabIndex) {
    this.tabIndex = tabIndex;
    this.tabRef.selectedTab = this.tabs[this.tabIndex].label;
  } 

  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);
    indexes.complete();
  }
}

El formulario para añadir o editar tareas

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

export class AddEditItemPage {
  item: any;
  tabIndex: number;
  itemIndex: number;
  buttons: Array<string>;

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

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

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

    await alert.present();
  }

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

El resultado

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

Puedes hacer clic aquí para observar que también podemos acceder al código generado desde el navegador, como cualquier otra página web.

Menú lateral con opción de compartir con IONIC 5+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.

Galería de imágenes con IONIC 5

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.

Lista de tareas con IONIC 5+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 5

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-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-four"></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>

    <ion-content>
      <ion-tabs>
        <ion-tab tab="home">
          <ion-list lines="full">
            <ion-reorder-group></ion-reorder-group>
          </ion-list>
        </ion-tab>
        <ion-tab tab="school">
          <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>
    </ion-content>
  </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.