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:
- Master-detail on Ionic4
- Login flow on Ionic4
- Login flow with Firebase custom auth
- master-detail native App deeplink
- CRUD APP with Ionic 4, Firestore and AngularFire 5.2+
- Add Web Push Support on Ionic PWA with Firebase Cloud Messaging and AngularFire
/!\ 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:
-
Lazy load pages and deep-linking
-
Shared and Core modules
- Observable data service
- Keep a consistent timestamp via back-end server
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.
- Create a project
- Run & deploy the application
- Create pages and routing
- Data modeling
- Add Firebase on project
- 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:
- Angular.io: Overall structural guidelines
- How to define a highly scalable folder structure for your Angular project
./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:
- deploy Ionic apps to iOS simulators and devices using Cordova
- deploy Ionic apps to Android simulators and devices using Cordova
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.
- Cory Rylan: Angular Observable Data Services
- Angular university: How to build Angular apps using Observable Data Services
$ 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
- AngularFirebase: Reactive CRUD App With Angular and Firebase Tutorial
- Angular Templates.io: Angular CRUD with Firebase
- Jave Bratt: Building a CRUD Ionic application with Firestore
- Josh Morony: Implementing a Master Detail Pattern in Ionic 4 with Angular Routing
- Simon Grimm: How to Build An Ionic 4 App with Firebase and AngularFire 5
- Angular 7 Firebase 5 CRUD Operations with Reactive Forms