Uploading images to Firebase Storage with Ionic - Part 2

March 6, 2017, 5:42 pm Categories:

Categories

** UPDATED TO IONIC 3 **

In Part 1 of this tutorial we set up the Firebase environment, prepopulating the database with our imported JSON data and generated the necessary services/pages for the application.

In this concluding part of the tutorial we'll implement the necessary logic for our services and pages to create a CRUD application to help manage interacting with the Firebase database and Storage services.

Image is everything

As we're going to be making heavy use of the Firebase Storage service it probably makes sense to start with our Image service.

Open the moveez/src/providers/image.ts file and make the following changes:

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import { Camera, CameraOptions } from '@ionic-native/camera';


@Injectable()
export class ImageProvider {

   public cameraImage : String

   constructor(public http     : Http,
               private _CAMERA : Camera) 
   {    	
   }


   selectImage() : Promise<any>
   {
      return new Promise(resolve => 
      {
         let cameraOptions : CameraOptions = {
             sourceType         : this._CAMERA.PictureSourceType.PHOTOLIBRARY,
             destinationType    : this._CAMERA.DestinationType.DATA_URL,      
             quality            : 100,
             targetWidth        : 320,
             targetHeight       : 240,
             encodingType       : this._CAMERA.EncodingType.JPEG,      
             correctOrientation : true
         };

         this._CAMERA.getPicture(cameraOptions)
         .then((data) =>
         {
            this.cameraImage 	= "data:image/jpeg;base64," + data;
            resolve(this.cameraImage);	    		
         }); 


      }); 		
   }

}

This is a fairly simple service that makes use of the Apache Cordova Camera plugin - that we import with the following statement:

import { Camera, CameraOptions } from '@ionic-native/camera';

We then create a single method - selectImage - which uses a Promise to return an image selected from the device photolibrary as a base64 data URI.

You'll notice the image format is set to a predefined height and width, along with the image quality. These values might feel a little restrictive to some and, should we want to, we could always change these/make them a little more flexible by supplying alternative values in the form of parameters to be injected through the selectImage method's parentheses.

For the purposes of this tutorial though we'll keep it simple!

That pretty much sums up all that we need for the Image service so let's move onto taking care of preloading items for our application.

Preloading

No matter how optimised an application's codebase might be we cannot guarantee the length of time that a network request will take to complete. As a result it's good practice to inform your users that an action is taking place and waiting to be completed.

This usually happens in the form of loading GIF's or preloaders (hands up anyone old enough to have cut their teeth developing with Flash MX - ahh...golden memories!)

To implement such functionality we'll use the Ionic Loading Component.

Open the moveez/src/providers/preloader.ts file and make the following changes:

import { Injectable } from '@angular/core';
import { LoadingController } from 'ionic-angular';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

/*
  Generated class for the Preloader provider.

  See https://angular.io/docs/ts/latest/guide/dependency-injection.html
  for more info on providers and Angular DI.
*/
@Injectable()
export class PreloaderProvider {

   private loading : any;

   constructor( public http        : Http,
                public loadingCtrl : LoadingController) 
   {
   }



   displayPreloader() : void
   {
      this.loading = this.loadingCtrl.create({
         content: 'Please wait...'
      });

      this.loading.present();
   }



   hidePreloader() : void
   {
      this.loading.dismiss();
   }

}

Fairly simple stuff here; just one method to display a preloader and another method to hide an already activated preloader.

Thanks to Ionic's pre-built Loading Component we simply utilise the methods provided by this class and then wrap them within our own custom methods for the service.

Easy-peesy!

Managing data

Our final service - the database.ts class - will help manage all database interactions and uploads to Firebase Storage (technically we should put that into its own service for better modularity but I'll leave that as an exercise for the reader to undertake).

Open the moveez/src/providers/database.ts file and make the following changes:

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import { Observable } from "rxjs/Observable";
import * as firebase from 'firebase';


@Injectable()
export class DatabaseProvider {

   constructor(public http: Http) 
   {
   }



   renderMovies() : Observable<any>
   {
                
      return new Observable(observer => 
      {
         let films : any = [];
         firebase.database().ref('films').orderByKey().once('value', (items : any) =>
         {
            items.forEach((item) => 
            {
               films.push({
	              id        : item.key,
	              actors    : item.val().actors,
	              date      : item.val().date,
	              duration  : item.val().duration,
	              genres    : item.val().genres,
	              image     : item.val().image,
	              rating    : item.val().rating,
	              summary   : item.val().summary,
	              title     : item.val().title
	           });
            });   
   
            observer.next(films);
            observer.complete();
         },
         (error) => 
         {
            console.log("Observer error: ", error);
            console.dir(error);
            observer.error(error)
         });
   
      });
   }



   deleteMovie(id) : Promise<any>
   {
      return new Promise((resolve) =>
      {
         let ref = firebase.database().ref('films').child(id);
         ref.remove();
         resolve(true);
      });
   }



   addToDatabase(movieObj) : Promise<any>
   { 
      return new Promise((resolve) =>
      {
         let addRef = firebase.database().ref('films');
         addRef.push(movieObj);
         resolve(true);
      });
   }



   updateDatabase(id, moviesObj) : Promise<any>
   {
      return new Promise((resolve) =>
      {
         var updateRef = firebase.database().ref('films').child(id);
	      updateRef.update(moviesObj);
         resolve(true);
      });
   }



   uploadImage(imageString) : Promise<any>
   {
      let image       : string  = 'movie-' + new Date().getTime() + '.jpg',
          storageRef  : any,
          parseUpload : any;
    
      return new Promise((resolve, reject) => 
      {
         storageRef       = firebase.storage().ref('posters/' + image);    
         parseUpload      = storageRef.putString(imageString, 'data_url');
    
         parseUpload.on('state_changed', (_snapshot) => 
         {
            // We could log the progress here IF necessary
            // console.log('snapshot progess ' + _snapshot);
         }, 
         (_err) => 
         {
            reject(_err);
         }, 
         (success) => 
         {
            resolve(parseUpload.snapshot);
         });
      });
   }


}

Here we make use of the Firebase Node package and Observables (both of which are imported towards the top of the script) to interact with the Firebase database and Storage services.

The Database service contains the following methods:

  • renderMovies
  • deleteMovie
  • addToDatabase
  • updateDatabase
  • uploadImage

The names should be fairly self explanatory and descriptive as to the purpose of each method but let's spend some time exploring each one in turn.

The renderMovies method uses an observable to return data retrieved from the films database using the Firebase Web API.

Each returned record is then iterated through using a forEach loop and pushed, as an object, into an array which, once completed, is passed into the Observable next method (which allows the array to be 'observed' by our script and subsequently subscribed to):

renderMovies() : Observable<any>
{
   return new Observable(observer => 
   {
      let films : any = [];
      firebase.database().ref('films').orderByKey().once('value', (items : any) =>
      {
         items.forEach((item) => 
         {
            films.push({
               id        : item.key,
               actors    : item.val().actors,
               date      : item.val().date,
               duration  : item.val().duration,
               genres    : item.val().genres,
               image     : item.val().image,
               rating    : item.val().rating,
               summary   : item.val().summary,
               title     : item.val().title
	    });
         });   
   
         observer.next(films);
         observer.complete();
      },
      (error) => 
      {
         console.log("Observer error: ", error);
         console.dir(error);
         observer.error(error);
      });
   
   });
}

Our deleteMovie method is passed the id of a particular movie which we use the Firebase Web API to locate within the database (using the child() method) and subsequently remove.

The result of this operation is returned using a Promise:

deleteMovie(id) : Promise<any>
{
   return new Promise((resolve) =>
   {
      let ref = firebase.database().ref('films').child(id);
      ref.remove();
      resolve(true);
   });
}

The addToDatabase method simply accepts a JavaScript object of keys/values that corresponds to the data we want to insert into the films database.

This is then 'pushed' into the films database using the Firebase push method - the result of which is returned back to the calling script courtesy of a Promise:

addToDatabase(movieObj) : Promise<any>
{ 
   return new Promise((resolve) =>
   {
      let addRef = firebase.database().ref('films');
      addRef.push(movieObj);
      resolve(true);
   });
}

The updateDatabase method accepts 2 parameters:

  • id (the id of the movie to be updated)
  • moviesObj (the key/value object of data that corresponds to the record to be updated)

The child method of the Firebase Web API is used to locate the matching entry in the films database upon which the update method is used to publish the modified data for that particular entry.

As before a promise is used to return the result of the operation:

updateDatabase(id, moviesObj) : Promise<any>
{
   return new Promise((resolve) =>
   {
      var updateRef = firebase.database().ref('films').child(id);
      updateRef.update(moviesObj);
      resolve(true);
   });
}

Finally the uploadImage method.

This accepts a single parameter - an image in the form of a base64 Data URL (which, for those of you who've been paying attention, is initially generated using the selectImage method of the Image service).

We create a name for our image using a combination of a prefix titled movie followed by a timestamp. This helps ensure that each entry we generate is unique with NO chance of existing entries being overwritten - always a welcome result I find!

We then upload the image, using the putString method, to the Firebase Storage service which, if successful, generates a snapshot object which we return using a Promise:

uploadImage(imageString) : Promise<any>
{
   let image       : string  = 'movie-' + new Date().getTime() + '.jpg',
       storageRef  : any,
       parseUpload : any;
    
   return new Promise((resolve, reject) => 
   {
      storageRef       = firebase.storage().ref('posters/' + image);    
      parseUpload      = storageRef.putString(imageString, 'data_url');
  
      parseUpload.on('state_changed', (_snapshot) => 
      {
         // We could log the progress here IF necessary
         //console.log('snapshot progess ' + _snapshot);
      }, 
      (_err) => 
      {
         reject(_err);
      }, 
      (success) => 
      {
         resolve(parseUpload.snapshot);
      });
   });
}

With that we've concluded the coding for our services so now let's move onto covering the application components.

Returning home

Our HomePage component will render the retrieved movies from firebase (along with their associated images located in Firebase Storage) to the view template.

Each movie entry will be displayed within an <ion-card>component which, in addition to display information about the movie, will also contain the following 2 button options:

  • Edit Movie
  • Delete Movie

Let's make a start by adding the necessary logic for the moveez/src/pages/home/home.ts file:

import { Component } from '@angular/core';
import { NavController, Platform, ModalController } from 'ionic-angular';
import { ImageProvider } from '../../providers/image/image';
import { PreloaderProvider } from '../../providers/preloader/preloader';
import { DatabaseProvider } from '../../providers/database/database';
import * as firebase from 'firebase';
import { Http } from '@angular/http';
import 'rxjs/Rx';


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

   private auth     : any;
   public movies    : any;
   private email    : string = 'YOUR-EMAIL-ADDRESS';
   private pass     : string = 'PASSWORD-FOR-YOUR-EMAIL-ADDRESS';
   

   constructor( public navCtrl       : NavController,
                private platform     : Platform,
                private modalCtrl    : ModalController,
                private _IMG         : ImageProvider,
                private _LOADER      : PreloaderProvider,
                private _DB          : DatabaseProvider) 
   {
   }


   ionViewDidEnter()
   {
      this._LOADER.displayPreloader();
      this.platform.ready()
      .then(() => 
      {
         firebase.auth().signInWithEmailAndPassword(this.email, this.pass)
         .then((credentials) => 
         {
            this.loadAndParseMovies(); 
         })
         .catch((err : Error) =>
         {
            console.log(err.message);
         });
      });
   }


   loadAndParseMovies()
   {  
      this.movies = this._DB.renderMovies();
      this._LOADER.hidePreloader();       
   }


   addRecord()
   {
      let modal = this.modalCtrl.create('Modals');
      modal.onDidDismiss((data) => 
      {
         if(data)
         {
            this.loadAndParseMovies();
         }
      });
      modal.present();
   }


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

      modal.onDidDismiss((data) => 
      {
         if(data)
         {
            this.loadAndParseMovies();
         }
      });
      modal.present();
   }



   deleteMovie(movie)
   {
      this._DB.deleteMovie(movie.id)
      .then((data) =>
      {
         this.loadAndParseMovies();
      });
   }


}

The first thing to note with the home.ts script is that, within the ionViewDidEnter method, we are using Firebase authentication to supply a hardcoded e-mail address and password to log into the Firebase service (remember how we set this up in part 1 of the tutorial?)

We could have added a log-in form component to supply the log-in credentials but, for the purpose of this tutorial a hard coded e-mail address and password will do just fine - just be sure to add the exact e-mail address and password you used when setting up the authentication within Firebase! :)

Once the log-in has been successfully authorised the loadAndParseMovies method is then called which utilises the Database service renderMovies method to retrieve our movie records from Firebase.

The remaining methods should be fairly self-explanatory so let's move onto adding the necessary HTML for our view to the moveez/src/pages/home/home.html file:

<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>
            <div *ngIf="movie.image">
               <img [src]="movie.image">
            </div>
            <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>

There's not really a great deal to dwell on with the above as, if you've read previous tutorials from this site (and/or been working with Ionic for a while now), the code should be fairly self-explanatory.

The only aspect to draw attention to is the use of the async pipe in the ngFor directive which allows for asynchronous data, whether returned by an Observable or a Promise, to update the UI:

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

Modal

The final component we are going to explore is that of the Modal page component which handles the display and logic for the form where adding or editing a movie entry for the Firebase database takes place.

Let's start with the moveez/src/pages/modals/modals.ts script:

import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { IonicPage, NavController, ViewController, NavParams } from 'ionic-angular';
import { ImageProvider } from '../../providers/image/image';
import { PreloaderProvider } from '../../providers/preloader/preloader';
import { DatabaseProvider } from '../../providers/database/database';
import * as firebase from 'firebase';

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

   public form             : any;
   public filmImage  	   : any;
   public movies           : any;
   public movieName        : any     = '';
   public movieImage       : 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 _IMG          : ImageProvider,
      public viewCtrl       : ViewController,
      private _LOADER       : PreloaderProvider,
      private _DB           : DatabaseProvider
   ) 
   {
      this.form 		= _FB.group({
         'summary' 		: ['', Validators.minLength(10)],
         'year' 		: ['', Validators.maxLength(4)],
         'name' 		: ['', Validators.required],
         'duration'		: ['', Validators.required],
         'image'		: ['', Validators.required],
         'rating'		: ['', Validators.required],
         'genres' 		: ['', Validators.required],
         'actors' 		: ['', Validators.required]
      });	

      this.movies = firebase.database().ref('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.movieImage       = movie.image;
          this.filmImage        = movie.image;
          this.movieId          = movie.id;

          
          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)
   {
      this._LOADER.displayPreloader();

      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,
  		  image     : string        = this.filmImage,
  		  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)
      {
         
         if(image !== this.movieImage)
         {
            this._DB.uploadImage(image)
            .then((snapshot : any) => 
            {
               let uploadedImage : any = snapshot.downloadURL;
                  
               this._DB.updateDatabase(this.movieId, 
               {
	              title    : title,
	              summary  : summary,
	              rating   : rating,
	              duration : duration,
	              image    : uploadedImage,
	              genres   : types,
	              actors   : people,
	              year     : year
	           })
               .then((data) =>
               {
                  this._LOADER.hidePreloader();
               });	
   
            });
         }
         else
         {               
           
           this._DB.updateDatabase(this.movieId, 
           {
	          title    : title,
	          summary  : summary,
	          rating   : rating,
	          duration : duration,
	          genres   : types,
	          actors   : people,
	          year     : year
	       })
           .then((data) =>
           {
              this._LOADER.hidePreloader();
           });
	     }

      }
      else 
      {            
         this._DB.uploadImage(image)
         .then((snapshot : any) => 
         {
            let uploadedImage : any = snapshot.downloadURL;
   
            this._DB.addToDatabase({
	           title    : title,
	           image    : uploadedImage,
	           summary  : summary,
	           rating   : rating,
	           duration : duration,
	           genres   : types,
	           actors   : people,
	           year     : year
	        })
            .then((data) =>
            {
               this._LOADER.hidePreloader();
            });	  
         });
	        
      }
      this.closeModal(true);
   }



   closeModal(val = null) 
   {
      this.viewCtrl.dismiss(val);
   }



   selectImage()
   {
      this._IMG.selectImage()
      .then((data) =>
      {
         this.filmImage = data;
      });
   }


}

There's quite a bit going on here so let's take a little while to break this down and digest what's happening at each stage.

We start by importing the necessary modules and then declaring properties, most of which will be used as models for editing an existing movie entry in the modalPage form:

public form             : any;
public filmImage  	    : any;
public movies           : any;
public movieName        : any     = '';
public movieImage       : 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;

The class constructor function is used to declare form validation rules using the FormBuilder object and also determine if the component is being called with any supplied navigation parameters. If there are parameters present we then set the data for the modal form fields based on the data received through the navParams object:

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

   this.movies = firebase.database().ref('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.movieImage     = movie.image;
       this.filmImage      = movie.image;
       this.movieId        = movie.id;

       
       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;
   }
}

The saveMovie method then handles whether a form is adding or updating a movie entry to the Firebase database based on the value of the isEditable property.

If the movie is being updated we then perform a further check to determine if the existing image is being replaced or not.

If an image is being supplied, whether an entry is being created or updated, we call the uploadImage method of the Database service to retrieve the Firebase Storage download URL:

saveMovie(val)
{
   this._LOADER.displayPreloader();


   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,
	  image     : string        = this.filmImage,
	  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)
   {
      
      if(image !== this.movieImage)
      {
         this._DB.uploadImage(image)
         .then((snapshot : any) => 
         {
            let uploadedImage : any = snapshot.downloadURL;
               
            this._DB.updateDatabase(this.movieId, 
            {
               title    : title,
               summary  : summary,
               rating   : rating,
               duration : duration,
               image    : uploadedImage,
               genres   : types,
               actors   : people,
               year     : year
            })
            .then((data) =>
            {
               this._LOADER.hidePreloader();
            });	

         });
      }
      else
      {               
        
        this._DB.updateDatabase(this.movieId, 
        {
           title    : title,
           summary  : summary,
           rating   : rating,
           duration : duration,
           genres   : types,
           actors   : people,
           year     : year
        })
        .then((data) =>
        {
           this._LOADER.hidePreloader();
        });
     }

   }
   else 
   {            
      this._DB.uploadImage(image)
      .then((snapshot : any) => 
      {
         let uploadedImage : any = snapshot.downloadURL;

         this._DB.addToDatabase({
            title    : title,
            image    : uploadedImage,
            summary  : summary,
            rating   : rating,
            duration : duration,
            genres   : types,
            actors   : people,
            year     : year
         })
         .then((data) =>
         {
            this._LOADER.hidePreloader();
         });	  
      });
       
   }
   this.closeModal(true);
}

Finally the following methods:

  • closeModal (implement closing of the modal window)
  • selectImage (select the desired movie image from the device photolibrary)
closeModal(val = null) 
{
   this.viewCtrl.dismiss(val);
}



selectImage()
{
   this._IMG.selectImage()
   .then((data) =>
   {
      this.filmImage = data;
   });
}

Modal Form

Let's now add the necessary HTML for the Modal page component form.

Open the moveez/src/pages/modals/modals.html file and add the following code:

<ion-header>
   <ion-toolbar>
      <ion-title>
         {{ title }}
      </ion-title>
      <ion-buttons start>
         <button 
            ion-button 
            (click)="closeModal()">
           <span 
              ion-text 
              color="primary" 
              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>
         <span 
            ion-text 
            color="danger" 
            block 
            text-center 
            padding-top
            padding-bottom
            (click)="selectImage()">Select an image</span>
            <input 
               type="hidden" 
               name="image" 
               formControlName="image" 
               [(ngModel)]="filmImage">
            <img [src]="filmImage">
   	  </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 
           padding-top 
           padding-bottom 
           [disabled]="!form.valid">
            <div *ngIf="!isEditable">
               Add a new movie
            </div>

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

   </form>
</ion-content>

Don't be surprised if the above HTML looks familiar as it's pretty much a carbon copy of the form we used in the AngularFire2 tutorial.

You'll notice the existence of FormControlName attributes and ngModel bindings to each input field in the above form.

The FormControlNameattributes help to ensure validation rules can be set while the ngModel bindings allow values for that field to be added from an existing movie entry.

As with the previous tutorial one glaring issue with the above HTML are the options for the actors and genres form elements being hard coded into those components. This is obviously 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 Modal 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:

Moveez application deployed on iOS

If so - give yourself a round of applause - you can now manage Firebase database entries and add images to Firebase Storage all from the comfort of your Ionic application!

In summary

This concludes our exploration into managing both Firebase data and images in Firebase Storage using only the Firebase Web API (along with some help from the Apache Cordova Camera plugin and various Ionic UI components).

There's definitely room for improvement with the above application though.

For example, we could implement the data for actors/actresses and genres in a much more content manageable manner as well as implement a method to delete images from Firebase Storage.

These I will leave to the reader to explore implementing, should they wish to do so.

Hopefully you've found this tutorial useful and can take away various concepts and approaches to working with the Firebase Web API in your Ionic projects.

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