Build A PWA App With Real-time Data and Offline Support

Build A PWA App With Real-time Data and Offline Support

This article will guide you through the entire setup of a simple todo app that can be deployed on mobile devices or served as a PWA. The main features of the app will be realtime data sync across client apps and offline support. We’ll use Ionic to build the app and Firebase’s new database engine: Cloud Firestore.

TL;DR

There’s a repository of a complete app available at https://github.com/front/frontmag-todo-app. You can also check the demo at https://frontmag-todos.firebaseapp.com/

Just be sure to check the bonus at the end of this article.

Let’s do it!

Go to Firebase site, sign up if you already haven't and create a new project. Then go to Database and on Cloud Firestore Beta card click “Get started”

Create project at Firebase console

And that’s all you have to do in the Firebase console.

Install Ionic if you already haven’t

npm install -g ionic

You’re ready to create your app!

ionic start todo-app blank

Using Ionic CLI you can quickly create a service provider the will handle communication with Firebase (or the AngularFire2 module to be more precise)

ionic g provider Todos

To handle Firebase and Cloud Firestore flow, let's use AngularFire2 and Firebase modules

npm install angularfire2 firebase

Create a environment settings file at /src/environment.ts

export const firebaseConfig = {

 apiKey: 'AIzaSyACjzK5Edn9mwTe3pY35oy9TWWLZjhykt4',

 authDomain: 'frontmag-todos.firebaseapp.com',

 databaseURL: 'https://frontmag-todos.firebaseio.com',

 projectId: 'frontmag-todos',

 storageBucket: 'frontmag-todos.appspot.com',

 messagingSenderId: '478702080463'

};

You can use the above settings but probably you would like to replace it with your own Firebase project settings.

Add AngularFire2 and Firebase imports at /src/app.module.ts and also the settings

import { AngularFireModule } from 'angularfire2';

import { AngularFirestoreModule } from 'angularfire2/firestore';

import { firebaseConfig } from '../environment';

 

imports: [

   BrowserModule,

   AngularFireModule.initializeApp(firebaseConfig),

   AngularFirestoreModule.enablePersistence(),

   IonicModule.forRoot(MyApp)

 ],

Check the complete source code at https://github.com/front/frontmag-todo-app/blob/master/src/app/app.module.ts

Notice the AngularFirestoreModule.enablePersistence(), this is what triggers the offline data features of our app. Easy, huh? More information at https://firebase.google.com/docs/firestore/manage-data/enable-offline

You’re ready to add some methods to our Todos service provider at /src/todos/todos.ts

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

import { AngularFirestore } from 'angularfire2/firestore';

import firebase from 'firebase';



/*

 Generated class for the TodosProvider provider.



 See https://angular.io/guide/dependency-injection for more info on providers

 and Angular DI.

*/

@Injectable()

export class TodosProvider {



 constructor(public db: AngularFirestore) {

   console.log('Hello TodosProvider Provider');

 }



 list() {

   return this.db.collection('/todos', ref => ref.orderBy('complete').orderBy('text')).valueChanges();

 }



 add(text) {

   const id = this.db.createId();



   return this.db.collection('todos').doc(id).set({

     id: id,

     text: text,

     complete: false,

     createdAt: firebase.firestore.FieldValue.serverTimestamp(),

     updatedAt: firebase.firestore.FieldValue.serverTimestamp()

   });

 }



 complete(todo) {

   return this.db.collection('todos').doc(todo.id).update({

     complete: todo.complete,

     updatedAt: firebase.firestore.FieldValue.serverTimestamp()

   });

 }



 delete(todo) {

   return this.db.collection('todos').doc(todo.id).delete();

 }

}

Now that we have all the backend stuff in place, let’s move to the frontend and build the UI to manage our todos.

At /src/pages/home/home.ts:

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

import { NavController } from 'ionic-angular';

import { TodosProvider } from '../../providers/todos/todos';

import { Observable } from 'rxjs/Observable';



@Component({

 selector: 'page-home',

 templateUrl: 'home.html',

 providers: [TodosProvider]

})

export class HomePage {



 public todos: Observable<any[]>;

 public todo: String = '';



 constructor(public navCtrl: NavController, public todosProvider: TodosProvider) {

   this.todos = this.todosProvider.list();

 }



 addTodo() {

   if (!this.todo) {

     return;

   }



   this.todosProvider.add(this.todo);

   this.todo = '';

 }



 completeTodo(todo) {

   this.todosProvider.complete(todo);

 }



 deleteTodo(todo) {

   this.todosProvider.delete(todo);

 }

}

At /src/pages/home/home.html

<ion-header>

 <ion-navbar>

   <ion-title>

     Todos

   </ion-title>

 </ion-navbar>

</ion-header>



<ion-content padding>

 <ion-list>



   <ion-item>

     <ion-label>To do <ion-icon name="arrow-round-forward"></ion-icon></ion-label>

     <ion-input type="text" [(ngModel)]="todo"></ion-input>

     <button (click)="addTodo($event)" ion-button outline item-end>Add</button>

   </ion-item>



 </ion-list>



 <ion-list>

   <ion-item-sliding *ngFor="let todo of todos | async">



     <ion-item ngClass="{{todo.complete?'done':''}}">

       <ion-checkbox [(ngModel)]="todo.complete" (ionChange)="completeTodo(todo)"></ion-checkbox>

       <ion-label>{{ todo.text }}</ion-label>

     </ion-item>



     <ion-item-options side="right">

       <button ion-button icon-only (click)="deleteTodo(todo)">

         <ion-icon name="remove-circle"></ion-icon>

       </button>

     </ion-item-options>

   

   </ion-item-sliding>

 </ion-list>



 <p ng-if="todos" text-center>Slide an item to left to show more options</p>

</ion-content>

And finally some style for the completed todos, at /scr/pages/home/home.scss

page-home {

 .item {

   &.done {

     text-decoration: line-through;

   }

 }

}

And that’s it! You’ve got yourself a simple todo app ready to run. To preview it just

ionic serve

Hmm … you’re running with your own Firebase project settings and it doesn’t work, does it?

Cloud Firestore index error

That’s because Cloud Firestore needs to create an index in order to be able to sort by complete status and text fields. Just follow the provided link to create the index or add it manually on your Firebase console.

PWA?

The app isn’t quite a PWA yet, you need to enable the service worker by un-commenting the following block at /src/index.html

<script>

   if ('serviceWorker' in navigator) {

     navigator.serviceWorker.register('service-worker.js')

       .then(() => console.log('service worker installed'))

       .catch(err => console.error('Error', err));

   }

 </script>

Congratulations! You have built a real-time data with offline support app in a few simple steps. You can use Firebase to host your PWA in, also, a few simple steps. Check it out here. Or just use Ionic to deploy to iOS, Android or Windows devices.

Bonus - Serverless Cron

For our demo project database at Firebase, the completed todos older than a week get deleted automatically. And we're doing this without any server. Well, at least, any server maintained by us ;)

Cloud functions, do your "clouding"

Firebase provides Cloud Functions, a service that allows you to automatically run backend code through triggers or requests.

In our case, we use a request function triggered by EasyCron.

Check the /functions folder on the repository.