During latest months we’ve explored deeply how Ionic4/Angular and Firebase can help to develop high scalable (web) Apps. This post is part of the ongoing Ionic4/Angular with Firebase serie, where we cover common use cases. Here is the full serie:

/!\ This post was updated on Feb 10, 2020 and tested with these packages:

@angular/cli@8.3.25
cordova@9.0.0 
@ionic/angular@4.11.10
@angular/fire@5.4.2
firebase@7.8.1

Update Notes: Updated code snippets for Angular v8

Find an issue? Drop a comment I'll fix it ASAP

TLTR

Quick resume for people in a hurry:

Observable data service

import { Injectable } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument, DocumentReference } from '@angular/fire/firestore';

import * as firebase from 'firebase/app';
import 'firebase/firestore';

import { Observable } from 'rxjs';
import { Item } from './item.model';

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

  private itemsCollection: AngularFirestoreCollection<Item>;
  private collectionPath = 'items';

  constructor(
    private afs: AngularFirestore
  ) {
    this.itemsCollection = afs.collection<Item>('items', ref => ref.orderBy('publishedAt', 'desc'));
  }
  
  get items$: Observable<Item[]> {
    return this.itemsCollection.valueChanges({idField: 'id'});
  }
  ...

Create => Push

  public push(item: any): Promise<DocumentReference> {
    const timestamp = this.timestamp;
    return this.itemsCollection.add({
      ...item,
      createdAt: firebase.firestore.FieldValue.serverTimestamp()
    });
  }

Read

List

Public data stream this.items$ = this.itemsCollection.valueChanges({idField: 'id'});

Object

  public getById(id: string): Observable<any> {
    return this.itemsCollection.doc(id).valueChanges();
  }

Update => Update or Set

  /**
   * AngularFirestore provides methods for setting and updating
   * - set(data: T) - Destructively updates a document's data.
   * - update(data: T) - Non-destructively updates a document's data.
   */
   
  public set(id: string, item: Item): Promise<void> {
    return this.itemsCollection.doc(id).set(item);
  }

  public update(id: string, item: Item): Promise<void> {
    return this.itemsCollection.doc(id).update(item);
  }

Delete => Remove

  public remove(id: string): Promise<void> {
    return this.itemsCollection.doc(id).delete();
  }

Repository & demo

Demo app is deployed on crud-angularfirestore-ionic4.web.app

All source code can be found on GitHub: https://github.com/meumobi/mmb-demos.crud-angularfirestore-ionic4

What you’ll build

We are going to create a news App, implementing CRUD actions (Create, Read, Update, Delete).

We’ll use Ionic for UI with Angular, Firestore as Cloud db and AngularFirestore, the official Angular library for Firebase.

Our main concern is to apply good/recommended practices:

What you’ll need

We need to have Node.js and Git installed in order to install both Ionic and Cordova. Follow the Android and iOS platform guides to install required tools for development.

And of course you’ll also need a Firebase account.

Methodology

Each main section below corresponds to a visible milestone of the project, where you can validate work on progress running App.

  1. Create a project
  2. Run & deploy the application
  3. Create pages and routing
  4. Data modeling
  5. Add Firebase on project
  6. Observable data service

By this way you can pickup what is interesting for you and/or run tutorial on several days always keeping a stable state of project, avoid big bang ;-)

Create a project

Prerequisites

Install both Ionic and Cordova.

$ npm install cordova @ionic/cli typescript @angular/cli -g
...

$ npm ls -g cordova @ionic/cli npm typescript @angular/cli --depth 0
├── @angular/cli@8.3.25 
├── @ionic/cli@6.0.1 
├── cordova@9.0.0 
├── npm@6.13.6 
├── phonegap@9.0.0
└── typescript@3.7.3 

Create a new Ionic v4 application

Create a New Ionic/Angular Application with

$ ionic start mmb-demos.crud-angularfirestore-ionic4 blank --type=angular --cordova --package-id=com.meumobi.crud-angularfire-ionic4
$ cd mmb-demo.crud-angularfirestore-ionic4

That means:

  • ionic start creates the app.
  • meu-starter.crud-angularfire.ionic-v4 is the name we gave it.
  • blank tells the Ionic CLI the template you want to start with. You can list available templates using ionic start –list
  • --type=<angular> type of project to start (e.g. angular, react, ionic-angular, ionic1)
  • --cordova include Cordova integration (default config.xml, iOS and Android resources, like icon and splash screen)
  • --package-id=<com.meumobi.crud-angularfire.ionic-v4> specify the bundle ID/application ID for your app (reverse-DNS notation)

The command ionic start will initialize a git repository and run npm install to get all the packages into node_modules.

File structure

There’s no perfect solution or unique rule to define file structure, it’s important to consider how and where your App will grow to adapt the file structure to your project. I highly recommend to read some posts to be aware of basic recommendations:

./src
  /app
    /pages
      item-list/
      item-detail/
      item-form/
    app-routing.module.ts
    app.module.ts
    /components
      item-headline
    shared
      item.ts
      item-mock.ts
      item-mock-service.ts
      item.service.ts|spec.ts
      index.ts
...

Interesting points to observe:

  • we use a shared module for item model and servive, because these classes will be shared across pages
  • we use a shared component item-headline, it will be convenient to use it on item-list and item-detail to not repeat ui, although it’s not common on ‘real world’.
  • items service is “mocked”, it’s easier to test our App, consuming mock entries.

Run & deploy the application

Run your app on web browser

You can test the App running ionic serve cmd:

$ ionic serve

Ionic lab to test iOS and Android rendering

To test iOS and Android views I recommend using @ionic/lab package

$ npm i --save-dev @ionic/lab

and run

$ ionic serve --lab

Deploy native Apps on device

It’s not the purpose of this tutorial, so I will not develop this topic but if you need check links below for more details:

Create pages and routing

Pages

Then let’s go! We start creating pages to implement CRUD, item-list, item-form and item-detail.

$ ng g page pages/item-list
CREATE src/app/pages/item-list/item-list.module.ts (554 bytes)
CREATE src/app/pages/item-list/item-list.page.scss (0 bytes)
CREATE src/app/pages/item-list/item-list.page.html (128 bytes)
CREATE src/app/pages/item-list/item-list.page.spec.ts (706 bytes)
CREATE src/app/pages/item-list/item-list.page.ts (267 bytes)
UPDATE src/app/app-routing.module.ts (565 bytes)
$ ng g page pages/item-detail
CREATE src/app/pages/item-detail/item-detail.module.ts (564 bytes)
CREATE src/app/pages/item-detail/item-detail.page.scss (0 bytes)
CREATE src/app/pages/item-detail/item-detail.page.html (130 bytes)
CREATE src/app/pages/item-detail/item-detail.page.spec.ts (720 bytes)
CREATE src/app/pages/item-detail/item-detail.page.ts (275 bytes)
UPDATE src/app/app-routing.module.ts (669 bytes)
$ ng g page pages/item-form
CREATE src/app/pages/item-form/item-form.module.ts (554 bytes)
CREATE src/app/pages/item-form/item-form.page.scss (0 bytes)
CREATE src/app/pages/item-form/item-form.page.html (128 bytes)
CREATE src/app/pages/item-form/item-form.page.spec.ts (706 bytes)
CREATE src/app/pages/item-form/item-form.page.ts (267 bytes)
UPDATE src/app/app-routing.module.ts (765 bytes)

We can reuse item-form to create and edit item depending if an id already exists or not.

Routing

Schematics from @ionic/angular-toolkit auto add new routes on src/app/app-routing.module.ts but we need to make some changes:

  • set item-list as default page (instead of home)
  • use new syntax introduced in Angular 8 for loadChildren (as loadChildren:string is now deprecated).
  • add item-add path, use ItemEdit component to create item
  • add id as param on item-edit and item-detail

Open and edit src/app/app-routing.module.ts to add updates routes as following.

...


const routes: Routes = [
  { path: '', redirectTo: 'item-list', pathMatch: 'full' },
  { path: 'home', loadChildren: () => import('./home/home.module').then( m => m.HomePageModule)},
  { path: 'item-list', loadChildren: () => import('./pages/item-list/item-list.module').then( m => m.ItemListPageModule) },
  { path: 'item-detail/:id', loadChildren: () => import('./pages/item-detail/item-detail.module').then( m => m.ItemDetailPageModule) },
  { path: 'item-edit/:id', loadChildren: () => import('./pages/item-form/item-form.module').then( m => m.ItemFormPageModule) },
  { path: 'item-create', loadChildren: () => import('./pages/item-form/item-form.module').then( m => m.ItemFormPageModule) },
];

...

At this stage you can add links on pages to test navigation or access directly to lazy-loaded pages running ionic serve and typing url on address bar of your browser.

  • ’/’ for item-list
  • ‘/item-create’
  • ‘/item-detail/123’ for item-detail
  • ‘/item-update/123’ for item-update

Data modeling

Model

We’ll use type specifier to get a typed result object.

Inspired by RSS specification we’ll manage items with following fields:

  • title: string
  • description: string
  • createdAt: Timestamp; // Epoch timestamp
  • modifiedAt: Timestamp; // Epoch timestamp
  • link: string
  • imageUrl: string
$ ng generate class shared/item --type model --skipTests
CREATE src/app/shared/item.model.ts (37 bytes)

Open and edit src/app/shared/item.model.ts as below:

import { Timestamp } from '@firebase/firestore-types';

export class Item {
  id?: string;
  title: string;
  description: string;
  createdAt: Timestamp;
  modifiedAt: Timestamp;
  link: string;
  imageUrl: string = null;
}

Interesting points to observe:

  • To prevent inconsistencies due to user’s local system, we’ll use firebase.firestore.FieldValue.serverTimestamp() as back-end server timestamp.
  • the id field is optional because it is filled by angularfirebase.

    An ‘idField’ option can be used with collection.valueChanges() to include the document ID on the emitted data payload. On previous versions we use to duplicate the key of document within the document itself as id, definitely not a good practice, could avoid it now!

This feature is available since Angularfire v5.2 (20190531) thanks to Jeff Delaney, the author of the PR.

Add Firebase on project

Create a Firebase project

Before you can add Firebase to your JavaScript app, you need to create a Firebase project to connect to your app.

Register your app

After you have a Firebase project, you can add your web app to it.

Install dependencies

@angular/fire is the official Angular library for Firebase, we’ll install both packages:

$ npm install firebase @angular/fire --save

Setup Environment Config

To initialize Firebase in your app, you need to provide your app’s Firebase project configuration. Copy it on src/environments/environment.ts

export const environment = {
  production: false,
  firebaseConfig: {
    apiKey: 'AIzaSyA5Xvv-O_G531RILC50FlRBSWr-HVzlEJA',
    authDomain: 'meu-starter.firebaseapp.com',
    databaseURL: 'https://meu-starter.firebaseio.com',
    projectId: 'meu-starter',
    storageBucket: 'meu-starter.appspot.com',
    messagingSenderId: '581248963506',
    appId: '1:581248963506:web:b207e18491151d7bee4aab'
  }
};

Connect Firebase to Angular

Add required modules, AngularFireModule and AngularFirestoreModule, on your app.module. Also add our environment config.

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 { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

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.enablePersistence(),
  ],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Observable data service

Definition

Observable data services or stores are a simple and intuitive pattern that allows tapping into the power of functional reactive programming in Angular without introducing too many of new concepts. An observable data service is an Angular injectable service that can be used to provide data to multiple parts of the application. This pattern can ensure data is coming from one place in our application and that every component receives the latest version of that data through our data streams.

$ ng generate service shared/item --skipTests

AngularFirestore provides methods to manipulate documents setting, updating, and deleting document data:

  • set(data: T) - Destructively updates a document’s data.
  • update(data: T) - Non-destructively updates a document’s data.
  • delete() - Deletes an entire document. Does not delete any nested collections.
import { Injectable } from '@angular/core';
import {
  AngularFirestore,
  AngularFirestoreCollection,
  AngularFirestoreDocument,
  DocumentReference
} from '@angular/fire/firestore';

import * as firebase from 'firebase/app';
import 'firebase/firestore';

import { Observable } from 'rxjs';
import { Item } from './item.model';

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

  private itemsCollection: AngularFirestoreCollection<Item>;
  items$: Observable<Item[]>;

  constructor(
    private afs: AngularFirestore
  ) {
    this.itemsCollection = afs.collection<Item>('items', ref => ref.orderBy('publishedAt', 'desc'));
    this.items$ = this.itemsCollection.valueChanges({idField: 'id'});
  }
/**
 * Inspired by https://angularfirebase.com/lessons/firestore-advanced-usage-angularfire/#3-CRUD-Operations-with-Server-Timestamps
 */
  get timestamp() {
    return firebase.firestore.FieldValue.serverTimestamp();
  }

  push(item: any): Promise<DocumentReference> {
    const timestamp = this.timestamp;
    return this.itemsCollection.add({
      ...item,
      createdAt: timestamp,
      modifiedAt: timestamp,
      publishedAt: timestamp,
    });
  }
}

Furthermore


Victor Dias

Sharing mobile Experiences

Follow me