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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ionic start lugares blank --type=angular --cordova --no-git --no-link
ionic start lugares blank --type=angular --cordova --no-git --no-link
ionic start lugares blank --type=angular --cordova --no-git --no-link

Al finalizar el proceso, se habrá creado un directorio con el nombre

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
cd lugares
cd lugares
cd lugares

Vamos a instalar la versión más estable hasta el momento:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm install @ionic/angular@5.1.1
npm install @ionic/angular@5.1.1
npm install @ionic/angular@5.1.1

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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ionic serve
ionic serve
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<ion-header>
...
<ion-title>Places!</ion-title>
...
<ion-button (click)="reorder=!reorder">
...
</ion-header>
<ion-header> ... <ion-title>Places!</ion-title> ... <ion-button (click)="reorder=!reorder"> ... </ion-header>
<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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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>
<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>
<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
ngModel, que nos permitirá enlazar el valor del atributo
text
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
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
href del elemento
<ion-item></ion-item>
<ion-item></ion-item> (más información aquí).

Además, utilizaremos el elemento 

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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 [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 [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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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>
<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>
<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"
[disabled]="!reorder" para activar o desactivar la opción de reordenar los elementos. Mediante los corchetes enlazamos el valor del atributo
reorder
reorder de TypeScript para poder acceder a su valor directamente. El evento 
(ionItemReorder)
(ionItemReorder) nos indicará que se ha cambiado de sitio algún elemento, y por lo tanto ejecutaremos el método
move()
move(), cuya funcionalidad veremos más adelante.

Por último, sólo necesitamos realizar un bucle utilizando la directiva

ngFor
ngFor, que además nos proporcionará cada elemento
item
item del array que representa la lista, así como el índice
i
i de cada uno de ellos.

El fichero «home.page.html» completo

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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>
<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>
<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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@ViewChild('myList') listRef: List;
reorder: boolean;
list: any;
text: string;
@ViewChild('myList') listRef: List; reorder: boolean; list: any; text: string;
@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í):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
navigator.geolocation.getCurrentPosition(pos => {
let url = "https://maps.google.com/maps?&z=15&t=k&q="+pos.coords.latitude+" "+pos.coords.longitude;
...
});
navigator.geolocation.getCurrentPosition(pos => { let url = "https://maps.google.com/maps?&z=15&t=k&q="+pos.coords.latitude+" "+pos.coords.longitude; ... });
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í):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
async add(event) {
let loading = await this.loadingController.create();
loading.present();
...
loading.dismiss();
}
async add(event) { let loading = await this.loadingController.create(); loading.present(); ... loading.dismiss(); }
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í):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
...
<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]);
}
... <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]); }
...
<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):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
...
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();
});
...
... 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(); }); ...
...
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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();
}
}
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(); } }
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ionic cordova resources
ionic cordova resources
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í):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ionic cordova plugin add cordova-plugin-geolocation --save
ionic cordova plugin add cordova-plugin-geolocation --save
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í):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ionic cordova plugin add cordova-plugin-camera --save
npm install --save @ionic-native/camera@beta
ionic cordova plugin add cordova-plugin-camera --save npm install --save @ionic-native/camera@beta
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
...
import { Camera } from '@ionic-native/camera/ngx';
...
@NgModule({
...
providers: [
...
Camera,
...
],
...
})
export class AppModule {}
... import { Camera } from '@ionic-native/camera/ngx'; ... @NgModule({ ... providers: [ ... Camera, ... ], ... }) export class AppModule {}
...
import { Camera } from '@ionic-native/camera/ngx';
...
@NgModule({
  ...
  providers: [
    ...
    Camera,
    ...
  ],
  ...
})
export class AppModule {}

El archivo «app.module.ts» completo

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 {}
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 {}
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ionic cordova build android --prod
ionic cordova build android --prod
ionic cordova build android --prod

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ionic cordova run android --prod
ionic cordova run android --prod
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ionic start lugares blank --type=angular --no-git --no-link
ionic start lugares blank --type=angular --no-git --no-link
ionic start lugares blank --type=angular --no-git --no-link

Al finalizar el proceso, se habrá creado un directorio con el nombre

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
cd lugares
cd lugares
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ionic serve
ionic serve
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<ion-header>
...
<ion-title>Places!</ion-title>
...
<ion-button (click)="reorder=!reorder">
...
</ion-header>
<ion-header> ... <ion-title>Places!</ion-title> ... <ion-button (click)="reorder=!reorder"> ... </ion-header>
<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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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>
<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>
<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
ngModel, que nos permitirá enlazar el valor del atributo
text
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
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
href del elemento
<ion-item></ion-item>
<ion-item></ion-item> (más información aquí).

Además, utilizaremos el elemento 

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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 [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 [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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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>
<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>
<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"
[disabled]="!reorder" para activar o desactivar la opción de reordenar los elementos. Mediante los corchetes enlazamos el valor del atributo
reorder
reorder de TypeScript para poder acceder a su valor directamente. El evento 
(ionItemReorder)
(ionItemReorder) nos indicará que se ha cambiado de sitio algún elemento, y por lo tanto ejecutaremos el método
move()
move(), cuya funcionalidad veremos más adelante.

Por último, sólo necesitamos realizar un bucle utilizando la directiva

ngFor
ngFor, que además nos proporcionará cada elemento
item
item del array que representa la lista, así como el índice
i
i de cada uno de ellos.

El fichero «home.page.html» completo

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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>
<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>
<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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
reorder: boolean;
list: any;
text: string;
reorder: boolean; list: any; text: string;
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í):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
navigator.geolocation.getCurrentPosition(pos => {
let url = "https://maps.google.com/maps?&z=15&t=k&q="+pos.coords.latitude+" "+pos.coords.longitude;
...
});
navigator.geolocation.getCurrentPosition(pos => { let url = "https://maps.google.com/maps?&z=15&t=k&q="+pos.coords.latitude+" "+pos.coords.longitude; ... });
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í):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
async add(event) {
let loading = await this.loadingController.create();
loading.present();
...
loading.dismiss();
}
async add(event) { let loading = await this.loadingController.create(); loading.present(); ... loading.dismiss(); }
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í):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
...
<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]);
}
... <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]); }
...
<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):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
...
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 });
...
... 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 }); ...
...
  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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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();
}
}
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(); } }
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í):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
...
<plugin name="cordova-plugin-geolocation" />
<plugin name="cordova-plugin-camera" />
...
... <plugin name="cordova-plugin-geolocation" /> <plugin name="cordova-plugin-camera" /> ...
...
    <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):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
...
<body>
<app-root></app-root>
<script type="text/javascript" src="cordova.js"></script>
</body>
...
... <body> <app-root></app-root> <script type="text/javascript" src="cordova.js"></script> </body> ...
...
<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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<ion-header>
...
<ion-title>Places!</ion-title>
...
<ion-button onclick="toggleReorder()">
...
</ion-header>
<ion-header> ... <ion-title>Places!</ion-title> ... <ion-button onclick="toggleReorder()"> ... </ion-header>
<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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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>
<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>
<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
href del elemento
<ion-item></ion-item>
<ion-item></ion-item> (más información aquí).

Además, utilizaremos el elemento 

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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>
<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>
<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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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>`;
});
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>`; });
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
forEach, que además nos proporcionará cada elemento
item
item del array, así como el índice
index
index de cada uno de ellos.

El fichero «index.html» completo

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<!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/core@5.1.1/dist/ionic/ionic.esm.js"></script>
<script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/core@5.1.1/dist/ionic/ionic.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core@5.1.1/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>
<!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/core@5.1.1/dist/ionic/ionic.esm.js"></script> <script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/core@5.1.1/dist/ionic/ionic.js"></script> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core@5.1.1/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>
<!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/core@5.1.1/dist/ionic/ionic.esm.js"></script>
  <script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/core@5.1.1/dist/ionic/ionic.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core@5.1.1/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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 += ... ;
});
}
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 += ... ; }); }
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function onLoad() {
document.addEventListener('ionItemReorder', (event) => { moveItem(event.detail); });
printItems();
}
function onLoad() { document.addEventListener('ionItemReorder', (event) => { moveItem(event.detail); }); printItems(); }
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í):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
navigator.geolocation.getCurrentPosition(pos => {
let url = "https://maps.google.com/maps?&z=15&t=k&q="+pos.coords.latitude+" "+pos.coords.longitude;
...
});
navigator.geolocation.getCurrentPosition(pos => { let url = "https://maps.google.com/maps?&z=15&t=k&q="+pos.coords.latitude+" "+pos.coords.longitude; ... });
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í):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function addItem(event) {
const loading = document.createElement('ion-loading');
...
loading.present();
...
loading.dismiss();
}
function addItem(event) { const loading = document.createElement('ion-loading'); ... loading.present(); ... loading.dismiss(); }
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í):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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]);
<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]);
<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):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
...
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 });
...
... 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 }); ...
...
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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;
}
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; }
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í):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<plugin name="cordova-plugin-geolocation" />
<plugin name="cordova-plugin-geolocation" />
<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í):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<plugin name="cordova-plugin-camera" />
<plugin name="cordova-plugin-camera" />
<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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?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="fernando@iessanvicente.com">
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>
<?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="fernando@iessanvicente.com"> 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>
<?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="fernando@iessanvicente.com">
      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):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
...
"target": "es5"
...
... "target": "es5" ...
...
    "target": "es5"
...

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ionic build --prod -- --base-href .
ionic build --prod -- --base-href .
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):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
...
<plugin name="cordova-plugin-ionic-webview" />
...
... <plugin name="cordova-plugin-ionic-webview" /> ...
...
<plugin name="cordova-plugin-ionic-webview" />
...

Un ejemplo del fichero config.xml (que debemos incluir en la carpeta www) podría ser el siguiente:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?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="fernando@iessanvicente.com">
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>
<?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="fernando@iessanvicente.com"> 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>
<?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="fernando@iessanvicente.com">
        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):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
...
<body>
<app-root></app-root>
<script type="text/javascript" src="cordova.js"></script>
</body>
...
... <body> <app-root></app-root> <script type="text/javascript" src="cordova.js"></script> </body> ...
...
<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.