Con este tutorial puedes conocer cómo crear una aplicación para Ionic que almacene información en una base de datos "en la nube", de manera que los datos estén siempre disponibles desde cualquier dispositivo con conexión a Internet. Concretamente se va a utilizar el servicio Cloud Firestore (Firestore Database), ofrecido por Google dentro de Firebase.

Se va a realizar una aplicación sencilla, que permita almacenar y gestionar una lista de tareas.

Los primeros pasos que se describen a continuación en este tutorial (sólo hasta la inserción de los datos) puedes verlos también en el siguiente vídeo:

Configuración de la base de datos en Google Cloud Firestore

Accede a la consola de Google Cloud Firestore desde la dirección: console.firebase.google.com y crea un nuevo proyecto.

Captura de pantalla 2020 11 25 a las 11.13.10 8a340

Asigna un nombre al proyecto (ten en cuenta que no podrás usar acentos ni algunos caracteres especiales).

Captura de pantalla 2020 11 25 a las 11.14.11 b96a2

En el siguiente paso puedes dejar activado el servicio de Google Analytics o desactivarlo, ya que no va a ser necesario en este tutorial.

Captura de pantalla 2020 11 25 a las 11.18.47 50c22

Una vez finalizada la creación del proyecto, accede a la sección Firestore Database del menú izquierdo y usa el botón Crear base de datos.

Captura de pantalla 2021 10 21 a las 20.40.22 1fcd5

Al iniciarse la creación de la base de datos, se solicita en primer lugar la regla de seguridad. Activa inicialmente la opción de modo de prueba para facilitar la lectura y escritura en la base de datos sin controlar de momento qué usuarios pueden hacerlo (posteriormente se debería establecer un control más exhaustivo).

Captura de pantalla 2020 11 25 a las 11.02.11 d84df

Por defecto se establecen 30 días para el modo de prueba, por lo que si necesitas más tiempo deberás editar posteriormente la fecha que aparece en las reglas.

En la siguiente pantalla deberás establecer la región desde la que usarás la base de datos, por lo que selecciona la zona europea, si es el caso.

Captura de pantalla 2020 11 25 a las 11.08.48 a7999

En la pestaña Datos podrás ir viendo el contenido de la base de datos en cada momento.

Captura de pantalla 2019 02 08 a las 13.45.11 b837c

Creación del nuevo proyecto Ionic

Ejecuta las siguientes sentencias en el terminal para crear un nuevo proyecto en blanco de Ionic, e instalar las dependencias necesarias para poder trabajar con los servicio de Firebase de Google. En este tutorial se va a usar la librería fire de Angular (@angular/fire) que a su vez requiere la librería firebase. Puedes encontrar más información sobre la librería @angular/fire en: https://www.npmjs.com/package/@angular/fire.

ionic start ejemplo-firestore blank
cd ejemplo-firestore 
npm install firebase @angular/fire

Configurar el acceso a la base de datos en el proyecto

Entra en la sección Project Overview (o Descripción general) y haz clic en el botón </> para obtener la información necesaria para configurar el servicio en una aplicación web o también para Ionic.

Captura de pantalla 2019 02 08 a las 13.51.44 e04fb 3540c

Es necesario de nuevo indicar un nombre (apodo) para tu aplicación.

Captura de pantalla 2020 11 25 a las 11.22.28 b25c9

Y en el siguiente paso aparecerán unos datos de configuración similares a los siguientes, los cuales deberás tener en cuenta cuando edites el archivo que se indica a continuación:

Captura de pantalla 2020 11 25 a las 11.22.47 05574

environments/environment.ts

Copia los valores de los atributos apiKey, authDomain, ...  dentro del archivo environment.ts (lo encontrarás dentro del proyecto Ionic, en la carpeta src/environments) como se muestra en el siguiente apartado.

export const environment = {
  production: false,
  firebaseConfig: {
    apiKey: "????????????",
    authDomain: "????????????",
    databaseURL: "????????????",
    projectId: "????????????",
    storageBucket: "????????????",
    messagingSenderId: "????????????",
appId: "????????????" }
};

app.module.ts

Edita el archivo src/app/home/app-module.ts añadiéndole las líneas subrayadas en amarillo. Con estas líneas conseguirás importar el módulo de Firebase en el proyecto para poder usar posteriormente en el proyecto llamadas a funciones que utilicen este servicio. Además se importa el archivo environment que tiene las credenciales de acceso como has visto antes.

Observa que en esta línea de código
AngularFireModule.initializeApp(environment.firebaseConfig)
se inicializa el servicio de Firebase, se pasa como parámetro la variable firebaseConfig que se ha declarado anteriormente en el archivo environment con las credenciales.

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

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';

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

import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { environment } from '../environments/environment';

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

Interface para declarar el tipo de objeto a almacenar en la base de datos

Ejecuta una sentencia similar a la siguiente desde el Terminal para que se genere un archivo donde declararás la estructura de los datos que vas a utilizar. Cambia el nombre "Tarea" por el tipo de objeto que vayas a utilizar en tu proyecto.

ionic generate interface Tarea

Esa sentencia habrá creado el siguiente archivo dentro de la carpeta src/app del proyecto:

Captura de pantalla 2021 10 21 a las 21.03.12 2f218

tarea.ts

Este es el archivo que generará la sentencia anterior. Indica en él la estructura de los datos que vas a usar en tu aplicación para almacenar información en Firebase.

export interface Tarea {
    titulo: string;
    descripcion: string;
} 

Clase para métodos de acceso a la base de datos

Para organizar mejor el código vamos a crear una nueva clase dentro de los servicios del proyecto. Se le va a asignar como nombre "firestore", por lo que con la siguiente orden de la línea de comandos creará el archivo firestore.service.ts dentro de la carpeta app del proyecto.

ionic generate service firestore

Observa que esa sentencia crea los archivos firestore.service.tsfirestore.service.spec.ts.

Captura de pantalla 2021 10 21 a las 21.10.21 2e9cf

En este archivo se van a ir añadiendo el código los distintos métodos que van a permitir realizar las acciones básicas sobre la base de datos (inserción, consulta, borrado y modificación de datos) que tenemos en Firebase, como se irá viendo a continuación. 

Insertar registros

firestore.service.ts

Como se ha comentado anteriormente, en este archivo (firestore.service.ts) se van a ir declarando los distintos métodos que realicen las operaciones con la base de datos. Comenzamos con el método al que se la ha asignado el nombre insertar, que se encargará de añadir a la base de datos los datos que se indiquen por parámetro.

Este método recibirá 2 parámetros que corresponderán a la colección donde se almacenarán (similar a una tabla de SQL) y los datos en sí.

public insertar(coleccion, datos)

La inserción de datos en Cloud Firestore se realiza invocando al método add() sobre la colección que obtiene la llamada a angularFirestore.collection(), siendo angularFirestore un objeto de AngularFirestore que se obtiene desde el constructor.

this.angularFirestore.collection(coleccion).add(datos)

Así debe quedar el código del archivo:

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

import { AngularFirestore } from '@angular/fire/firestore';

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

  constructor(private angularFirestore: AngularFirestore) { 
} public insertar(coleccion, datos) { return this.angularFirestore.collection(coleccion).add(datos); }   }

home.page.ts

En código TypeScript de la página home se va a crear una propiedad de la clase donde se va a almacenar un objeto del tipo de dato que se ha declarado en el interface. En este ejemplo será de la clase Tarea. y la propiedad va a recibir el nombre tareaEditando, ya que va a almacenar la tarea que se esté insertando o editando por el usuario en un momento determinado.

tareaEditando: Tarea;

Esa propiedad se crea como un objeto vacío en el constructor de la clase:

this.tareaEditando = {} as Tarea;

Como parámetro del método constructor se debe inyectar el servicio (firestore) que hemos creado anteriormente para que puedan ser usados en esta clase los métodos (insertar, consultar, etc) que se vayan creando en el archivo firestore.service.ts.

constructor(private firestoreService: FirestoreService)

También se va a preparar un método llamado clicBotonInsertar que se ejecutará cuando el usuario pulse un botón, que se encargará de tomar los datos que haya en la propiedad tareaEditando para insertarlos en la base de datos, llamando al método insertar que hemos hecho antes en el servicio que hemos llamado FirestoreService.

this.firestoreService.insertar("tareas", this.tareaEditando)

Observa que tras la llamada a insertar se usa el método then() de JavaScript para ejecutar un bloque de código una vez que finalice el código anterior (en este caso la inserción en la base de datos) que se hace de manera asíncrona. Aquí se va a mostrar un mensaje por consola para señalar que se ha realizado la acción correctamente y se vuelve a dejar vacío el objeto tareaEditando, de manera que quede preparado para seguir añadiendo nuevos datos.

console.log('Tarea creada correctamente!');
this.tareaEditando= {} as Tarea;

También se hace uso de la opción error del método then() para ejecutar código si se ha producido algún error. En este ejemplo se va a mostrar el error en la consola.

(error) => {
    console.error(error);
}

Por tanto, el código completo debe quedar como el siguiente:

import { Component } from '@angular/core';
import { FirestoreService } from '../firestore.service';
import { Tarea } from '../tarea';
@Component({ selector: 'app-home', templateUrl: 'home.page.html', styleUrls: ['home.page.scss'], }) export class HomePage { tareaEditando: Tarea; constructor(private firestoreService: FirestoreService) { // Crear una tarea vacía this.tareaEditando = {} as Tarea; } clicBotonInsertar() { this.firestoreService.insertar("tareas", this.tareaEditando).then(() => { console.log('Tarea creada correctamente!'); this.tareaEditando= {} as Tarea; }, (error) => { console.error(error); }); } }

home.page.html

Para que el usuario pueda introducir los datos se va a preparar un formulario con etiquetas de Ionic. Las etiquetas ion-input van a hacer un databinding con cada una de las propiedades del objeto tareaEditando que hemos declarado antes en la clase TypeScript. De esa manera conseguimos que los datos que escriba el usuario en esos campos de texto se almacenen en el objeto tareaEditando.

[(ngModel)]="tareaEditando.titulo"

[(ngModel)]="tareaEditando.descripcion"

Además se añadirá un botón para que se ejecute el método clicBotonInsertar() que hemos desarollado antes en el archivo de TypeScript.

<ion-button (click)="clicBotonInsertar()">

El código completo puede ser como el siguiente:

<ion-header>
  <ion-toolbar>
    <ion-title>
      Ejemplo Firestore
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-item>
    <ion-label>Título</ion-label>
    <ion-input [(ngModel)]="tareaEditando.titulo"></ion-input>
  </ion-item>
  <ion-item>
    <ion-label>Descripción</ion-label>
    <ion-input [(ngModel)]="tareaEditando.descripcion"></ion-input>
  </ion-item>
  <ion-button (click)="clicBotonInsertar()">Añadir tarea</ion-button>
</ion-content>

Así quedará la vista al ejecutar la aplicación:

Captura de pantalla 2019 02 15 a las 12.53.55 57b80

Prueba a insertar algunos datos y comprueba que se almacenan en la Colección tareas de Firestore. Observa que se crea un documento para cada registro que se ha ido añadiendo, al que se le asocia un ID aleatorio formado de letras y números:

Captura de pantalla 2019 02 15 a las 12.56.14 29d92

Obtener lista de registros

De manera similar al caso anterior de inserción de registros se hará el resto de acciones sobre la base de datos. Se creará un método en la clase de servicio de firestore.service.ts, se implementará en el archivo TypeScript el código necesario para tratar la información y llamar al método oportuno del servicio, y se modificará el archivo HTML para acceder a los datos almacenados en el código TypeScript o se llamará a los métodos necesarios como respuesta a acciones del usuario. 

Observa que se ha usado un código de subrayado de colores para facilitar la interpretación de las llamadas que se realizan entre los distintos archivos.

firestore.service.ts

  public consultar(coleccion) {
    return this.angularFirestore.collection(coleccion).snapshotChanges();
  }

home.page.ts

Los datos (tareas en este ejemplo) obtenidos de la consulta se almacenan como un Array en la variable arrayColeccionTareas. Cada elemento de dicho Array será un objeto que contiene 2 propiedades que se han denominado id y data. En la propiedad id se va a almacenar cada uno de los ID aleatorios que crea Firebase para cada documento. La propiedad data será un objeto de la clase Tarea, y contendrá toda la información de cada documento (tarea) obtenido en la consulta.

  arrayColeccionTareas: any = [{
    id: "",
    data: {} as Tarea
   }];

  constructor(private firestoreService: FirestoreService) {
    this.obtenerListaTareas();
  }

  obtenerListaTareas(){
    this.firestoreService.consultar("tareas").subscribe((resultadoConsultaTareas) => {
      this.arrayColeccionTareas = [];
      resultadoConsultaTareas.forEach((datosTarea: any) => {
        this.arrayColeccionTareas.push({
          id: datosTarea.payload.doc.id,
          data: datosTarea.payload.doc.data()
        });
      })
    });
  }

home.page.html

  <h1>LISTA DE TAREAS</h1>
  <ion-list>
    <ion-item *ngFor="let documentTarea of arrayColeccionTareas">
      <ion-grid>
        <ion-row>
          <h2>{{documentTarea.data.titulo}}</h2>
        </ion-row>
        <ion-row>
          <p>{{documentTarea.data.descripcion}}</p>
        </ion-row>
      </ion-grid>
    </ion-item>
  </ion-list>

Borrado de datos

firestore.service.ts

El borrado de registros (documentos) de Firestore requiere conocer el ID del documento a eliminar, por lo que este método va a pedir como parámetros el ID del documento y la colección donde se encuentra dicho documento.

 public borrar(coleccion, documentId) {
return this.angularFirestore.collection(coleccion).doc(documentId).delete();
}

home.page.ts

Se crea la propiedad idTareaSelec para almacenar el ID del documento que se desea eliminar. El valor contenido en ella se pasará como parámetro a la llamada al método borrar() anterior. El método selecTarea() se encarga de almacenar en idTareaSelec el ID del documento que se recibe como parámetro con el nombre tareaSelec.

  idTareaSelec: string;

  selecTarea(tareaSelec) {
    console.log("Tarea seleccionada: ");
    console.log(tareaSelec);
    this.idTareaSelec = tareaSelec.id;
    this.tareaEditando.titulo = tareaSelec.data.titulo;
    this.tareaEditando.descripcion = tareaSelec.data.descripcion;
  }

  clicBotonBorrar() {
    this.firestoreService.borrar("tareas", this.idTareaSelec).then(() => {
      // Actualizar la lista completa
      this.obtenerListaTareas();
      // Limpiar datos de pantalla
      this.tareaEditando = {} as Tarea;
    })
  }

home.page.html

Añadir a cada item de la lista la llamada al método selecTarea cuando se seleccione un determinado item. Se recorre el array arrayColeccionTareas, cargando en la variable documentTarea cada uno de los elementos de ese array. Esa variable documentTarea es la que se pasa como parámetro en la llamada a selecTarea() que se ha implementado antes para almacenar el ID y los datos de la tarea seleccionada por el usuario.

  <ion-list>
    <ion-item *ngFor="let documentTarea of arrayColeccionTareas" (click)="selecTarea(documentTarea)">
      ...
    </ion-item>
  </ion-list>

En este ejemplo se ha añadido un botón para eliminar el item que se encuentre seleccionado: 

  <ion-button (click)="clicBotonBorrar()">Borrar tarea</ion-button>

Modificación de datos

firestore.service.ts

public actualizar(coleccion, documentId, datos) {
return this.angularFirestore.collection(coleccion).doc(documentId).set(datos);
}

home.page.ts

  clicBotonModificar() {
    this.firestoreService.actualizar("tareas", this.idTareaSelec, this.tareaEditando).then(() => {
      // Actualizar la lista completa
      this.obtenerListaTareas();
      // Limpiar datos de pantalla
      this.tareaEditando = {} as Tarea;
    })
  }

home.page.html

El código de este botón debe incluirse junto con la llamada al método selecTarea() que ya se ha mostrado anteriormente al seleccionar un item de la lista.

<ion-button (click)="clicBotonModificar()">Modificar tarea</ion-button>

Consulta de datos a partir de un ID

firestore.service.ts

public consultarPorId(coleccion, documentId) {
  return this.angularFirestore.collection(coleccion).doc(documentId).snapshotChanges();
}

xxxx.page.ts

Lo habitual puede ser que la consulta de los datos de un determinado documento por su ID se haga en un página distinta, por lo que no especifica aquí el nombre de la página donde colocar este código.

Los datos (un documento de FireStore) que se obtengan tras realizar la consulta se deben almacenar en alguna variable con una estructura que permita diferenciar el ID y los datos (DATA) obtenidos. En este ejemplo se va a crear la variable document con los campos id y data (de tipo Tarea):

document: any = {
  id: "",
  data: {} as Tarea
};

En el lugar del código que corresponda (donde se desee realizar la consulta por ID), se incluirán las siguientes líneas que realizan la consulta buscando el ID que se encuentre almacenado en la variable idConsultar (no se incluye en este ejemplo su declaración y asignación de valor, ya que depende del modo de uso que se haga de este tipo de consulta).

this.firestoreService.consultarPorId("tareas", idConsultar).subscribe((resultado) => {
  // Preguntar si se hay encontrado un document con ese ID
  if(resultado.payload.data() != null) {
    this.document.id = resultado.payload.id
    this.document.data = resultado.payload.data();
    // Como ejemplo, mostrar el título de la tarea en consola
    console.log(this.document.data.titulo);
  } else {
    // No se ha encontrado un document con ese ID. Vaciar los datos que hubiera
    this.document.data = {} as Tarea;
  } 
});

xxxx.page.html

Para mostrar algún dato contenido en la consulta se deberá incluir algo como esto (muestra el título de la tarea consultada):

<p> Título: {{ document.data.titulo }} </p>