Integrating Firebase into an Ionic app with AngularFire2 part 2

February 24, 2017, 12:23 pm Categories:

Categories

** UPDATED TO IONIC 3 **

In Part 1 of this tutorial we set up a Firebase application, created a database from a pre-populated JSON file, generated an Ionic project and subsequently configured our application root module.

In this concluding part of the tutorial we'll implement the necessary logic, styling and templating to allow an Ionic application for iOS to communicate with Firebase and display data retrieved from there.

Home run

What better place to start than with the HomePage component?

Open the moveez/src/pages/home/home.ts file and change the existing code to match the following instead:

import { Component } from '@angular/core';
import { NavController, ModalController, Platform } from 'ionic-angular';
import { AngularFire, FirebaseListObservable } from 'angularfire2';
import 'rxjs/add/operator/map'; 
import * as firebase from 'firebase';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

   public movies    : FirebaseListObservable<any[]>;


   constructor(public navCtrl    : NavController,
               private angFire   : AngularFire,
               private modalCtrl : ModalController,
               private platform  : Platform) 
   {
      
   }



   ionViewDidLoad()
   {
      this.platform.ready()
      .then(() => 
      { 
         this.movies = this.angFire.database.list('/films');         
      });
   }



   addRecord()
   {
      let modal = this.modalCtrl.create('Modals');
      modal.present();
   }



   editMovie(movie)
   {
      let params = { movie: movie, isEdited: true },
          modal  = this.modalCtrl.create('Modals', params);
      
      modal.present();
   }



   deleteMovie(movie : any)
   {
      this.movies.remove(movie);
   }

}

Here we've started by importing the following modules:

  • AngularFire and FirebaseListObservable from angularFire2
  • All firebase packages

We then configure a movie property which will be used to manage our interaction with the remote Firebase database and return that data for use in the page template.

One important point to note here is that we assign a type of FirebaseListObservable to the movies property. Doing so allows this property to "listen" for changes made to the Firebase database and, as a result, retrieve the amended data for subsequent display in our application.

This observable is essentially the "glue" that binds changes to the Firebase database with our Ionic application.

We then initialise a handful of properties within our class constructor, assigning them to specific modules so we can access their methods as and where necessary later on in the class (the initialised angFire and modalCtrl properties in particular are quite important for our purposes).

The ionViewDidLoad method is then used to trigger the interaction between Ionic2 and Firebase through the angularFire2 database.list method. This retrieves all of the records in our films database:

ionViewDidLoad()
{
   this.platform.ready()
   .then(() => 
   { 
      this.movies = this.angFire.database.list('/films');         
   });
}

We then define the following methods to allow database records to be added, edited or deleted:

  • addRecord (which opens the Modals page component in a Modal window)
  • editMovie (which also opens the Modals page component in a Modal window but supplies a movie entry as an additional parameter - allowing that movie entry to be editable in the Modal window [we'll see how this works a little later on in this tutorial])
  • deleteMovie (which accepts a movie entry as a parameter that is then supplied to Firebase through AngularFire2's remove method - causing it to be deleted from the database)
addRecord()
{
   let modal = this.modalCtrl.create('Modals');
   modal.present();
}


editMovie(movie)
{
   let params = { movie: movie, isEdited: true },
       modal  = this.modalCtrl.create('Modals', params);
      
   modal.present();
}


deleteMovie(movie : any)
{
   this.movies.remove(movie);
}

There shouldn't be anything too challenging with the above so now let's move onto the HTML for our HomePage component.

In the moveez/src/pages/home/home.html file make the following changes:

<ion-header>
   <ion-navbar>
      <ion-title>
         Moveez
      </ion-title>

      <ion-buttons end>
         <button 
            ion-button 
            icon-only 
            (click)="addRecord()">
            <ion-icon name="add"></ion-icon>
         </button>
      </ion-buttons>
   </ion-navbar>
</ion-header>


<ion-content padding>
   <ion-list>
      <ion-card 
         class="movie" 
         text-wrap 
         *ngFor="let movie of movies | async">
         <ion-item>
            <h2>{{ movie.title }}</h2>    
            <small>{{ movie.date }} ({{ movie.duration }})</small>
            <p>{{ movie.summary }}</p>
            

            <section class="multiples">
               <h3>Actors/Actresses</h3>
	       <ion-chip 
                *ngFor="let actor of movie.actors" 
                padding-left 
                padding-right 
                margin-right>{{ actor.name }}</ion-chip>
            </section>


            <section class="multiples">
               <h3>Genres</h3>
               <ion-chip 
                  *ngFor="let genre of movie.genres" 
                  padding-left 
                  padding-right 
                  margin-right>{{ genre.name }}</ion-chip>
            </section>
		
         </ion-item>


         <div class="manage-record" padding>
            <button 
               ion-button 
               text-center 
               color="primary" 
               (click)="editMovie(movie)">Edit</button>

            <button 
               ion-button 
               text-center 
               color="danger" 
               (click)="deleteMovie(movie)">Delete</button>
         </div>
      </ion-card>
   </ion-list>

</ion-content>

Here we simply structure the HTML in the following order:

  • Introduce a button in the NavBar to call the addRecord method from the HomePage class
  • Use a list to display each movie retrieved through the top-level ngFor directive in a card component
  • Retrieve each actor/actress associated with a film using a secondary level ngFor directive and render the output to the screen using an <ion-chip> component
  • Retrieve each genre associated with a film using a secondary level ngFor directive and render the output to the screen using an <ion-chip> component
  • At the bottom of each film entry place Edit and Delete buttons (with click events that call their respective methods from the HomePage class) to manage that movie

One important feature of the above HTML is the following snippet:

<ion-card 
   class="movie" 
   text-wrap 
   *ngFor="let movie of movies | async">

Note the use of the async pipe in the ngFor directive?

This is used to subscribe to an observable (remember that FirebaseListObservable type we assigned to the movies property?), returns the latest emitted value from the observable and then updates the <ion-card> component with all detected changes.

You'll notice, when the app is published, how the UI is updated in real-time relating to records being added, amended or removed. You can thank the async pipe for this handy content refresh.

Read more about the async pipe here.

Now for the final adjustment to the HomePage component: the following minor style additions to the moveez/src/pages/home/home.scss file:

page-home {


   .movie {
      h2 {
         font-weight: bold;
         font-size: 2.0rem;
      }

      small {
         font-size: 1.4rem;
      }

      p {
         padding: 1.5em 0 0 0;
      }
   }
   
   .multiples {
      margin: 2em 0 0 0;


      h3 {
         font-weight: bold;
         padding: 0 0 0.5em 0;
      }
   }

   .manage-record {
      button {
      	width: 40%;
      }
   }
}

This simply provides some typography tweaks to the <ion-card> component and ensure that the widths for the buttons allow those to sit side by side within the <ion-card> component.

Nothing too drastic or exciting, just some nice aesthetic tweaks!

Now let's move onto crafting the logic and necessary HTML for the ModalsPage component.

Modal

The Modals page component will function in either of the following ways:

  • Provide a blank form to the user when creating a new movie entry
  • Provide a pre-filled form to the user with the selected movie data when editing an existing movie entry

To accomplish this we'll use a combination of the Angular FormBuilder API, AngularFire2 modules and Ionic form UI components to create a user-friendly, validated form for providing movie data that will be saved to our Firebase database.

Let's start implementing the logic for this component by opening the moveez/src/pages/modals/modals.ts file and making the following changes:

import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { IonicPage, NavController, ViewController, NavParams } from 'ionic-angular';
import { AngularFire, FirebaseListObservable } from 'angularfire2';

@IonicPage()
@Component({
  selector: 'page-modals',
  templateUrl: 'modals.html'
})
export class Modals {	   

   public form          : any;
   public movies        : FirebaseListObservable<any[]>;
   public movieName     : any     = '';
   public movieGenres   : any     = [];
   public movieDuration : any     = '';
   public movieSummary  : any     = '';
   public movieActors   : any     = [];
   public movieYear     : any     = '';
   public movieRating   : any     = '';
   public movieId       : string  = '';
   public isEditable    : boolean = false;

   
   constructor(
      public navCtrl        : NavController,
      public params         : NavParams,
      private _FB 	        : FormBuilder,
      private _FIRE         : AngularFire,
      public viewCtrl       : ViewController
   ) 
   {
      this.form 	    = _FB.group({
         'summary' 	    : ['', Validators.minLength(10)],
         'year' 	    : ['', Validators.maxLength(4)],
         'name'         : ['', Validators.required],
         'duration'	    : ['', Validators.required],
         'rating'	    : ['', Validators.required],
         'genres' 	    : ['', Validators.required],
         'actors' 	    : ['', Validators.required]
      });	

      this.movies = this._FIRE.database.list('/films');    


      if(params.get('isEdited'))
      {
          let movie 		= params.get('movie'),
              k;

          this.movieName        = movie.title;
          this.movieDuration	= movie.duration;
          this.movieSummary     = movie.summary;
          this.movieRating   	= movie.rating;
          this.movieYear    	= movie.year;
          this.movieId          = movie.$key;

          
          for(k in movie.genres)
          {
             this.movieGenres.push(movie.genres[k].name);
          }

          
          for(k in movie.actors)
          {
             this.movieActors.push(movie.actors[k].name);
          }

          this.isEditable      = true;
      }
   }



   saveMovie(val)
   {      
      let title	    : string	= this.form.controls["name"].value,
          summary   : string 	= this.form.controls["summary"].value,
          rating    : number	= this.form.controls["rating"].value,
          genres    : any       = this.form.controls["genres"].value,
          actors    : any	    = this.form.controls["actors"].value,
          duration  : string	= this.form.controls["duration"].value,
          year      : string	= this.form.controls["year"].value,
          types     : any       = [],
  	      people    : any       = [],
  	      k         : any;


    for(k in genres)
    {
       types.push({
          "name" : genres[k]
       });
    }


    for(k in actors)
    {
       people.push({
          "name" : actors[k]
       });
    }


   if(this.isEditable)
   {
      this.movies.update(this.movieId, {
         title    : title,
         summary  : summary,
         rating   : rating,
         duration : duration,
         genres   : types,
         actors   : people,
         year     : year
      });
   }
   else 
   {
      this.movies.push({
         title    : title,
         summary  : summary,
         rating   : rating,
         duration : duration,
         genres   : types,
         actors   : people,
         year     : year
      });
   }
     
   this.closeModal();
   }



   closeModal() 
   {
      this.viewCtrl.dismiss();
   }


}

There's a fair bit going on here so let's take a few minutes to break the above script down and fully digest what's happening.

As always with our TypeScript files we begin by importing the necessary modules before defining a range of properties prior to our class constructor:

public form          : any;
public movies        : FirebaseListObservable<any[]>;
public movieName     : any     = '';
public movieGenres   : any     = [];
public movieDuration : any     = '';
public movieSummary  : any     = '';
public movieActors   : any     = [];
public movieYear     : any     = '';
public movieRating   : any     = '';
public movieId       : string  = '';
public isEditable    : boolean = false;

We'll use most of these properties as models to manage editing the different fields for a selected movie entry in the HTML form of this component.

The isEditable property allows the class to determine whether the form is used to create a new record or edit an existing one. The value for this property will be set based on whether a movie entry has been passed as a navigation parameter to the Modals page component from the HomePage component (we'll see how this works a little later on).

We subsequently initialise certain properties in our class constructor which allow us to use methods from specific modules, namely the FormBuilder and AngularFire modules:

constructor(
   public navCtrl        : NavController,
   public params         : NavParams,
   private _FB 	         : FormBuilder,
   private _IMG          : Images,
   private _FIRE         : AngularFire,
   public viewCtrl       : ViewController
) 

Within the constructor itself we then define the validation rules to use for the form in the component HTML template:

this.form 		= _FB.group({
   'summary' 	: ['', Validators.minLength(10)],
   'year' 		: ['', Validators.maxLength(4)],
   'name' 		: ['', Validators.required],
   'duration'	: ['', Validators.required],
   'rating'		: ['', Validators.required],
   'genres' 	: ['', Validators.required],
   'actors' 	: ['', Validators.required]
});

This should be fairly self-explanatory but if you're not familiar with using the FormBuilder API I suggest you read my previous tutorial on that subject.

We subsequently assign our FirebaseListObservable to 'listen' for changes that occur to the data within the films database in Firebase:

this.movies = this._FIRE.database.list('/films');

This will come into play later on in the script when we create and update our movie records.

Finally, within the class constructor, we set up the logic for how the class should behave when a record is deemed editable:

if(params.get('isEdited'))
{
   let movie 		    = params.get('movie'),
       k;

   this.movieName	    = movie.title;
   this.movieDuration	= movie.duration;
   this.movieSummary    = movie.summary;
   this.movieRating   	= movie.rating;
   this.movieYear    	= movie.year;
   this.movieId         = movie.$key;

          
   for(k in movie.genres)
   {
      this.movieGenres.push(movie.genres[k].name);
   }

          
   for(k in movie.actors)
   {
      this.movieActors.push(movie.actors[k].name);
   }

   this.isEditable      = true;
}

Here we use the isEdited conditional check to determine whether data was passed through the NavParams object to the ModalsPage component. If it was we then assign the data from the supplied movie object to the respective properties we defined towards the top of the class.

These properties will then act as one-way models binding the data to their respective fields in the component form HTML. These values, updated or not by the user, will be subsequently retrieved using FormControl objects through the saveMovie method.

Finally, within the conditional logic statement we set the isEditable boolean property to true to indicate to the saveMovie method that we are actually editing an existing movie entry and not creating a new entry instead.

Penultimately the saveMovie method, as the name implies, saves our movie entries regardless of whether they have been newly created or updated from existing entries:

saveMovie(val)
{
   let title	  : string	= this.form.controls["name"].value,
       summary 	  : string 	= this.form.controls["summary"].value,
       rating  	  : number	= this.form.controls["rating"].value,
       genres  	  : any	    = this.form.controls["genres"].value,
       actors  	  : any	    = this.form.controls["actors"].value,
       duration   : string	= this.form.controls["duration"].value,
       year    	  : string	= this.form.controls["year"].value,
       types      : any     = [],
       people     : any     = [],
       k          : any;


   for(k in genres)
   {
      types.push({
         "name" : genres[k]
      });
   }


   for(k in actors)
   {
      people.push({
         "name" : actors[k]
      });
   }


   if(this.isEditable)
   {
      this.movies.update(this.movieId, {
         title    : title,
         summary  : summary,
         rating   : rating,
         duration : duration,
         genres   : types,
         actors   : people,
         year     : year
      });
   }
   else 
   {
      this.movies.push({
         title    : title,
         summary  : summary,
         rating   : rating,
         duration : duration,
         genres   : types,
         actors   : people,
         year     : year
      });
   }
   this.closeModal();
}

This method starts by using individual FormControl objects to retrieve the value for each respective form field/component located in the HTML template. These are each assigned to block-level properties before being used as values in JSON objects which are saved to the films database.

You'll notice that, depending on whether the movie has been detected as an existing record or a new entry (based on the value of the isEditable property), these JSON objects are saved through either of the following AngularFire2 methods:

  • update
  • push

If we are editing an existing database entry we use the update method, having supplied the necessary id to match the record to that found in the database:

this.movies.update(this.movieId, {
   title    : title,
   summary  : summary,
   rating   : rating,
   duration : duration,
   genres   : types,
   actors   : people,
   year     : year
});

Otherwise, if it's a brand new entry we rely on push instead:

this.movies.push({
   title    : title,
   summary  : summary,
   rating   : rating,
   duration : duration,
   genres   : types,
   actors   : people,
   year     : year
});

Finally, after the database logic has been added we call the closeModal method which, as the name implies, is used to close the modal window and allow the user to see the updated records in the HomePage component.

That concludes the logic for our class so now all we need to implement is the actual HTML for our Modals page component.

Coding the Modal template

The form HTML should be fairly self-explanatory (particularly if you read my previous blog entry on using the FormBuilder API) so I'm not going to spend a great deal of time explaining this.

Simply open the moveez/src/pages/modals/modals.html file and replace the existing content with the following instead:

<ion-header>
   <ion-toolbar>
      <ion-title>
         {{ title }}
      </ion-title>
      <ion-buttons start>
         <button ion-button color="primary" (click)="closeModal()">
            <span showWhen="ios">Cancel</span>
            <ion-icon name="md-close" showWhen="android,windows"></ion-icon>
         </button>
      </ion-buttons>
   </ion-toolbar>
</ion-header>
<ion-content>
   <form [formGroup]="form" (ngSubmit)="saveMovie(form.value)">

      <ion-item-divider color="light">
         <div *ngIf="!isEditable">
            Add a new movie
         </div>

         <div *ngIf="isEditable">
            Amend this movie
         </div>
      </ion-item-divider>


      <ion-item>
         <ion-label stacked>Movie name:</ion-label>
         <ion-input
            type="text"
            formControlName="name" 
            [(ngModel)]="movieName"></ion-input>
      </ion-item>



      <ion-item>
         <ion-label stacked>Movie length:</ion-label>
         <ion-input
            type="text"
            formControlName="duration" 
            [(ngModel)]="movieDuration"></ion-input>
      </ion-item>



      <ion-item>
         <ion-label stacked>Genre:</ion-label>
         <ion-select 
            formControlName="genres" 
            multiple="true" 
            [(ngModel)]="movieGenres">
             <ion-option value="Action">Action</ion-option>
             <ion-option value="Comedy">Comedy</ion-option>
             <ion-option value="Documentary">Documentary</ion-option>
             <ion-option value="Historical">Historical</ion-option>
             <ion-option value="Romance">Romance</ion-option>
             <ion-option value="Science Fiction">Science Fiction</ion-option>
             <ion-option value="Thriller">Thriller</ion-option>
             <ion-option value="Zombie">Zombie</ion-option>
             <ion-option value="War">War</ion-option>
         </ion-select>
      </ion-item>



      <ion-item>
         <ion-label stacked>Actors/actresses:</ion-label>
         <ion-select 
            formControlName="actors" 
            multiple="true" 
            [(ngModel)]="movieActors">
             <ion-option value="Keanu Reeves">Keanu Reeves</ion-option>
             <ion-option value="Ian McShane">Ian McShane</ion-option>
             <ion-option value="Adrianne Palicki">Adrianne Palicki</ion-option>
             <ion-option value="Woody Harrelson">Woody Harrelson</ion-option>
             <ion-option value="Willem Dafoe">Willem Dafoe</ion-option>
             <ion-option value="John Leguizamo">John Leguizamo</ion-option>
             <ion-option value="Michael Nyqvist">Michael Nyqvist</ion-option>
             <ion-option value="Bridget Moynahan">Bridget Moynahan</ion-option>
             <ion-option value="Alfie Allen">Alfie Allen</ion-option>
             <ion-option value="Russell Crowe">Russell Crowe</ion-option>
             <ion-option value="Oliver Reed">Oliver Reed</ion-option>
             <ion-option value="Joaquin Phoenix">Joaquin Phoenix</ion-option>
             <ion-option value="Connie Nielsen">Connie Nielsen</ion-option>
             <ion-option value="Ralf Moeller">Ralf Moeller</ion-option>
             <ion-option value="Tom Hanks">Tom Hanks</ion-option>
             <ion-option value="Leonardo Dicaprio">Leonardo Dicaprio</ion-option>
             <ion-option value="Christopher Walken">Christopher Walken</ion-option>
             <ion-option value="Mike Myers">Mike Myers</ion-option>
             <ion-option value="Heather Graham">Heather Graham</ion-option>
             <ion-option value="Verne Troyer">Verne Troyer</ion-option>
             <ion-option value="Robert Wagner">Robert Wagner</ion-option>
             <ion-option value="Rob Lowe">Rob Lowe</ion-option>
             <ion-option value="Mindy Sterling">Mindy Sterling</ion-option>
         </ion-select>
      </ion-item>



      <ion-item>
         <ion-label stacked>Summary:</ion-label>
         <ion-textarea 
			rows="6"
            formControlName="summary" 
            [(ngModel)]="movieSummary"></ion-textarea>
      </ion-item>



      <ion-item>
         <ion-label stacked>Film rating:</ion-label>
         <ion-select 
            formControlName="rating" 
            [(ngModel)]="movieRating">
             <ion-option value="PG">PG</ion-option>
             <ion-option value="12">12</ion-option>
             <ion-option value="12A">12A</ion-option>
             <ion-option value="15">15</ion-option>
             <ion-option value="18">18</ion-option>
             <ion-option value="U">U</ion-option>
             <ion-option value="R18">R18</ion-option>
         </ion-select>
      </ion-item>



      <ion-item>
         <ion-label stacked>Year released:</ion-label>
         <ion-input
            type="text"
            formControlName="year" 
            [(ngModel)]="movieYear"></ion-input>         
      </ion-item>


      <ion-item>
         <input 
            type="hidden" 
            name="movieId" >

         <button 
           ion-button 
           block 
           color="primary" 
           text-center
           [disabled]="!form.valid">
            <div *ngIf="!isEditable">
               Add a new movie
            </div>

            <div *ngIf="isEditable">
               Update this movie
            </div>
         </button>
      </ion-item>

   </form>
</ion-content>

You'll notice the addition of FormControlName attributes and ngModel bindings to each input field in the above form as well as ngIf directives to change the text value for the <ion-item> divider as well as the submit button based on whether or not this is a record being added or amended.

One glaring issue with the above form is the presence of the options for the actors and genres fields being hard coded into those form UI components. This is, of course, far from ideal and presents serious limitations with being able to add/edit movie entries accurately.

The correct solution here would be to dynamically add new entries to these fields but I'll leave this as an exercise for the viewer to undertake instead ;).

Managing the Modals components module

One final step before we can build and run the application is to ensure that all references to IonicModule are changed to IonicPageModule within the Modals component module - moveez/src/pages/modals/modals.modules.ts like so:

import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { Modals } from './modals';

@NgModule({
  declarations: [
    Modals,
  ],
  imports: [
    IonicPageModule.forChild(Modals),
  ],
  exports: [
    Modals
  ]
})
export class ModalsModule {}

Hopefully this step will no longer be needed in forthcoming iterations of the Ionic Framework but for now we need to double check that ALL component module files have this change made otherwise the Ionic application won't be able to be built properly.

Now all that's left is to actually build our project and publish this to a connected device.

Build and run the moveez application

In the ionic CLI, and at the root of the moveez project directory, issue the following command:

ionic build ios --prod

Don't forget to add that --prod flag as this will instruct the Ionic compiler to use Ahead-of-Time compilation to speed up the launch times and rendering of the application on a handheld device (see this blog entry for further details).

You can then deploy the published application to your iOS device using Xcode or the following Ionic CLI command:

ionic run ios --prod

Which, if all has gone well, should allow you to see the application being launched with data rendering to the screen and allowing you to interact with the modal forms like so:

Ionic application using AngularFire2 to retrieve Firebase data

If so, congratulations - you've successfully integrated Firebase content into an Ionic application for iOS!

In summary

This concludes our exploration into using AngularFire2 to interact with and post to and retrieve data from a Firebase database.

As you can see it's a fairly simply yet powerful way of integrating Firebase content into our Ionic applications, particularly through the use of FirebaseListObservables and the Angular async pipe (which allows our application UI to be refreshed in real-time based on changes to the Firebase data).

However AngularFire2 isn't without limitations; namely an inability to upload documents to Firebase Storage and also being unable to perform queries on a denormalised database structure (I.e. similar to a relational query in an SQL oriented database system).

This, unfortunately, has the effect of rendering AngularFire2 a VERY limited choice for consideration in real-world projects.

In subsequent tutorials I'll demonstrate how we can use Firebase's REST API to accomplish both of these tasks instead.

If you enjoyed what you've read here then please sign up to my mailing list and, if you haven't done so already, take a look at my e-book: Mastering Ionic for information about working with alternative data storage methods in Ionic.

Tags

Categories

Post a comment

All comments are welcome and the rules are simple - be nice and do NOT engage in trolling, spamming, abusiveness or illegal behaviour. If you fail to observe these rules you will be permanently banned from being able to comment.

Top