Ionic 5 Image Upload in Firebase with Progress Percentage Bar Tutorial

In this Ionic 5/4 tutorial, we will integrate Firebase services and see how to upload images in Ionic application with a progress bar indicator on the Firebase database.

As we all know Firebase provides an awesome package of cloud services making the life of developers very easy. There is a wide range of tools available for next-level app development like Authentication, Testing, Analytics, Storage, Database, etc.

In this post, we will create an Ionic Application with Image Upload feature with the Progress bar. It will also list already saved of Images. This application will focus on the two most important services of Firebase

 

 

How the Ionic App will Look

Required Firebase Services

Firestore Database

Cloud Firestore by Firebase is a NoSQL database that is very fast and reliable. In Firestore data is saved in the form of Collection of Documents which can be a hierarchy of Collection Document up to any level. In database will store Image Path, Name, and Size of uploaded image.

 

Firestorage Disk

Storage service can save files of any type under a provided Bucket name. In storage, we will save the actual Image uploaded by the user from the application.

 

Let’s start building!

 

 

Create an Ionic Application

First, create a new Ionic application with a blank template using the latest Ionic CLI. Make sure to update Ionic CLI by running the following command.

# Update Ionic CLI 
$ npm install -g @ionic/cli

# Create new application 
$ ionic start ionic-firebase-image-upload-app blank --type=angular

#Move inside the application directory
$ cd ionic-firebase-image-upload-app

Install AngularFire2 in Application

AngularFire2 is the official Angular library for Firebase. Using if we can easily connect with Firebase services with the Ionic application.

Run following NPM command to install AngularFire2

$ npm install firebase @angular/fire --save

 

Import AngularFire2 in AppModule

Now open the app.module.ts file to import the required Firebase services.

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

import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { AngularFireStorageModule } from '@angular/fire/storage';

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


@NgModule({
  declarations: [
    AppComponent
  ],
  entryComponents: [],
  imports: [
    BrowserModule, 
    IonicModule.forRoot(), 
    AppRoutingModule,
    AngularFireModule.initializeApp(environment.firebase, 'my-app-name'), // imports firebase/app needed for everything
    AngularFirestoreModule, // imports firebase/firestore, only needed for database features
    AngularFireStorageModule // imports firebase/storage only needed for storage features
  ],
  providers: [
    StatusBar,
    SplashScreen,
    //FileSizeFormatPipe,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Here we are using two services provided by Firebase, the AngularFireStorageModule which is used to store multimedia files and other files on a disk, and AngularFirestoreModule to save image file information in the NoSQL database.

If you check above we are initializing the AngularFireModule with our Firebase application credential which we will keep in the Angular environment file. Let’s quickly check the steps to create a Firebase project and get credentials to connect our Ionic application with the Firebase project.

 

Create a Firebase Project & Enable Services

To create a project click on “+ Add Project” after logging in your Google account. Then fill project information then click “Create Project“.

Database Setup

After successful project creation, click on “Database” on the left sidebar to “Create database”.

Select the “Start in test mode” option as currently, we are not using Authentication, so this option will let us read/write. But make sure to change this in a production application.

NOTE: If you get credential without creating Database then you will get error “ERROR Error: No Storage Bucket defined in Firebase Options.

 

Storage Setup

Now we need to change Rule in storage as well otherwise you will get the following error “Firebase Storage: User does not have permission to access ‘freakyStorage/1563691783666_ABC.png’.

Click on “Storage” on the left sidebar, hit “Get Started” >> “Next” >> “Done“. Now click on the “Rules” tab then change

allow read, write: if request.auth != null;

to

allow read, write: if request.auth == null;

Then click “Publish”.

 

 

Get Firebase Project Credential & Configure in Ionic

Where to find Credentials in Firebase?

To get credential, click on “Project Overview” then click on “Web” icon then fill some details then “Register app”

Next, you will see credentials

 

 

Save Firebase Credentials in Angular App Environment File

Copy the values then paste in “~environments/environment.ts” file in the application root.

//environment.ts
export const environment = {
  production: false,
  firebase: {
    apiKey: "AIzaSyDPihZfC8Ixuet47cfoPg22RnY6df7pQ-4",
    authDomain: "test-ff7b4.firebaseapp.com",
    databaseURL: "https://test-ff7b4.firebaseio.com",
    projectId: "test-ff7b4",
    storageBucket: "test-ff7b4.appspot.com",
    messagingSenderId: "892903466231",
    appId: "1:892903466231:web:b629ab121e5c608c"
  }
};

 

Implement Image Upload

Finally, let’s start the implementation of the Image upload feature in our Ionic application. As our application is having a blank template so it already has a Home component.

Update Home Component Class

In the home.component.ts file place the following code.

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

import { AngularFireStorage, AngularFireUploadTask } from '@angular/fire/storage';
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { finalize, tap } from 'rxjs/operators';

export interface MyData {
  name: string;
  filepath: string;
  size: number;
}

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {

  // Upload Task 
  task: AngularFireUploadTask;

  // Progress in percentage
  percentage: Observable<number>;

  // Snapshot of uploading file
  snapshot: Observable<any>;

  // Uploaded File URL
  UploadedFileURL: Observable<string>;

  //Uploaded Image List
  images: Observable<MyData[]>;

  //File details  
  fileName:string;
  fileSize:number;

  //Status check 
  isUploading:boolean;
  isUploaded:boolean;

  private imageCollection: AngularFirestoreCollection<MyData>;
  constructor(private storage: AngularFireStorage, private database: AngularFirestore) {
    this.isUploading = false;
    this.isUploaded = false;
    //Set collection where our documents/ images info will save
    this.imageCollection = database.collection<MyData>('freakyImages');
    this.images = this.imageCollection.valueChanges();
  }


  uploadFile(event: FileList) {
    

    // The File object
    const file = event.item(0)

    // Validation for Images Only
    if (file.type.split('/')[0] !== 'image') { 
     console.error('unsupported file type :( ')
     return;
    }

    this.isUploading = true;
    this.isUploaded = false;


    this.fileName = file.name;

    // The storage path
    const path = `freakyStorage/${new Date().getTime()}_${file.name}`;

    // Totally optional metadata
    const customMetadata = { app: 'Freaky Image Upload Demo' };

    //File reference
    const fileRef = this.storage.ref(path);

    // The main task
    this.task = this.storage.upload(path, file, { customMetadata });

    // Get file progress percentage
    this.percentage = this.task.percentageChanges();
    this.snapshot = this.task.snapshotChanges().pipe(
      
      finalize(() => {
        // Get uploaded file storage path
        this.UploadedFileURL = fileRef.getDownloadURL();
        
        this.UploadedFileURL.subscribe(resp=>{
          this.addImagetoDB({
            name: file.name,
            filepath: resp,
            size: this.fileSize
          });
          this.isUploading = false;
          this.isUploaded = true;
        },error=>{
          console.error(error);
        })
      }),
      tap(snap => {
          this.fileSize = snap.totalBytes;
      })
    )
  }

  addImagetoDB(image: MyData) {
    //Create an ID for document
    const id = this.database.createId();

    //Set document id with value in database
    this.imageCollection.doc(id).set(image).then(resp => {
      console.log(resp);
    }).catch(error => {
      console.log("error " + error);
    });
  }


}

In the Home component class, import Storage and Firestore packages. Connect to FireStore collection and subscribe to the valueChanges() method to get a snapshot of an updated list of images from Database. "freakyImages" is the name of collection which will be created if not exist.

The uploadFile() method will first validate file type then call this.storage.upload(path, file, { customMetadata });  to start uploading the file. The percentageChanges() returns percentage status of file uploading.

Subscribe to snapshotChanges() to get file upload progress and finalize() having addImagetoDB() method which saves the details to the database.

 

Update Home Template

For the home component template, we have three sections.

Choose Image

First one shows Image upload control

<ion-card class="ion-text-center" *ngIf="!isUploading && !isUploaded">
    <ion-card-header>
      <ion-card-title>Choose Images to Upload</ion-card-title>
    </ion-card-header>
    <ion-card-content>
      <ion-button color="success" shape="round" size="large">
        <span>Select Image</span>
        <input id="uploadBtn" type="file" class="upload" (change)="uploadFile($event.target.files)" />
      </ion-button>
    </ion-card-content>
  </ion-card>

 

Image Upload Progress with Pause, Resume, Cancel buttons

The second shows the progress with percentage bar and total file size and the number of data transferred.

<ion-card class="ion-text-center" *ngIf="isUploading && !isUploaded">
    <ion-card-header>
      <ion-card-title>Selected File:<b>{{ fileName }}</b></ion-card-title>
    </ion-card-header>

    <ion-card-content>
      <div *ngIf="percentage | async as pct">
        Progress: {{ pct | number }}%
        <ion-progress-bar value="{{ pct / 100 }}"></ion-progress-bar>
      </div>
      <div *ngIf="snapshot | async as snap">
        File Size: {{ snap.totalBytes | fileSizePipe }} Transfered:
        {{ snap.bytesTransferred | fileSizePipe }}
        <div *ngIf="snapshot && snap.bytesTransferred != snap.totalBytes">
          <ion-button color="warning" size="small" (click)="task.pause()" class="button is-warning">Pause</ion-button>
          <ion-button size="small" (click)="task.resume()" class="button is-info">Resume</ion-button>
          <ion-button color="danger" size="small" (click)="task.cancel()" class="button is-danger">Cancel</ion-button>
        </div>
      </div>
    </ion-card-content>
  </ion-card>

 

Uploaded Image Card with Download Button

The third section shows the uploaded image with the download file link.

<ion-card class="ion-text-center" *ngIf="!isUploading && isUploaded">
    <ion-card-header>
      <ion-card-title>
        <b>{{ fileName }}</b> Uploaded!
      </ion-card-title>
    </ion-card-header>

    <ion-card-content>
      <div *ngIf="UploadedFileURL | async as url">
        <img [src]="url" />
        <a [href]="url" target="_blank" rel="noopener">Download</a>
      </div>
      File Size: {{ fileSize | fileSizePipe }}
      <ion-button expand="full" color="success" (click)="isUploading = isUploaded = false">Upload More</ion-button>
    </ion-card-content>
  </ion-card>

 

 

Complete home.page.html will have the following HTML content.

<ion-header>
  <ion-toolbar color="tertiary">
    <ion-title>
      Firestore Image Upload
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
  
  <ion-card class="ion-text-center" *ngIf="!isUploading && !isUploaded">
    <ion-card-header>
      <ion-card-title>Choose Images to Upload</ion-card-title>
    </ion-card-header>
    <ion-card-content>
      <ion-button color="success" shape="round" size="large">
        <span>Select Image</span>
        <input id="uploadBtn" type="file" class="upload" (change)="uploadFile($event.target.files)" />
      </ion-button>
    </ion-card-content>
  </ion-card>

  <ion-card class="ion-text-center" *ngIf="isUploading && !isUploaded">
    <ion-card-header>
      <ion-card-title>Selected File:<b>{{ fileName }}</b></ion-card-title>
    </ion-card-header>

    <ion-card-content>
      <div *ngIf="percentage | async as pct">
        Progress: {{ pct | number }}%
        <ion-progress-bar value="{{ pct / 100 }}"></ion-progress-bar>
      </div>
      <div *ngIf="snapshot | async as snap">
        File Size: {{ snap.totalBytes | fileSizePipe }} Transfered:
        {{ snap.bytesTransferred | fileSizePipe }}
        <div *ngIf="snapshot && snap.bytesTransferred != snap.totalBytes">
          <ion-button color="warning" size="small" (click)="task.pause()" class="button is-warning">Pause</ion-button>
          <ion-button size="small" (click)="task.resume()" class="button is-info">Resume</ion-button>
          <ion-button color="danger" size="small" (click)="task.cancel()" class="button is-danger">Cancel</ion-button>
        </div>
      </div>
    </ion-card-content>
  </ion-card>
  
  <ion-card class="ion-text-center" *ngIf="!isUploading && isUploaded">
    <ion-card-header>
      <ion-card-title>
        <b>{{ fileName }}</b> Uploaded!
      </ion-card-title>
    </ion-card-header>

    <ion-card-content>
      <div *ngIf="UploadedFileURL | async as url">
        <img [src]="url" />
        <a [href]="url" target="_blank" rel="noopener">Download</a>
      </div>
      File Size: {{ fileSize | fileSizePipe }}
      <ion-button expand="full" color="success" (click)="isUploading = isUploaded = false">Upload More</ion-button>
    </ion-card-content>
  </ion-card>

  <h2 class="ion-text-center">Uploaded Freaky Images</h2>

  <ion-card color="light" class="ion-text-center" *ngFor="let item of images | async">
    <ion-card-header>
      <ion-card-title>
        {{ item.name }}
      </ion-card-title>
    </ion-card-header>

    <ion-card-content>
      <img [src]="item.filepath" />
      <a [href]="item.filepath" target="_blank" rel="noopener">Download</a>
    </ion-card-content>
  </ion-card>
</ion-content>

 

Image Size Pipe Filter

In HTML there is "fileSizePipe" used to convert bytes of file size into a readable format. Add this pipe file file-size-format.pipe.ts in the home component folder

//file-size-format.pipe.ts
import {Pipe, PipeTransform} from '@angular/core';

const FILE_SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const FILE_SIZE_UNITS_LONG = ['Bytes', 'Kilobytes', 'Megabytes', 'Gigabytes', 'Pettabytes', 'Exabytes', 'Zettabytes', 'Yottabytes'];

@Pipe({
  name: 'fileSizePipe'
})
export class FileSizeFormatPipe implements PipeTransform {
  static forRoot() {
    throw new Error("Method not implemented.");
  }
  transform(sizeInBytes: number, longForm: boolean): string {
    const units = longForm
      ? FILE_SIZE_UNITS_LONG
      : FILE_SIZE_UNITS;
    let power = Math.round(Math.log(sizeInBytes)/Math.log(1024));
  	power = Math.min(power, units.length - 1);
  	const size = sizeInBytes / Math.pow(1024, power); // size in new units
  	const formattedSize = Math.round(size * 100) / 100; // keep up to 2 decimals
  	const unit = units[power];
  	return `${formattedSize} ${unit}`;
  }
}

Thanks to this Plunker

Also, import this Pipe in home.module.ts file

//home.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { HomePage } from './home.page';

import { FileSizeFormatPipe } from './file-size-format.pipe';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild([
      {
        path: '',
        component: HomePage
      }
    ])
  ],
  declarations: [HomePage,FileSizeFormatPipe]
})
export class HomePageModule {}

 

Run Application

That’s it… now try running the app in the browser

$ ng serve --open

 

Source Code

Find the source code of this tutorial in my GitHub repository here.

Conclusion

In Ionic Application above we created a fully functional demo with Image Uploader control showing progress and snapshot of database images. Firebase provides many tools that can be easily incorporated in Ionic application to create high-end features filled with Google awesomeness.

 

 

 

3 thoughts on “Ionic 5 Image Upload in Firebase with Progress Percentage Bar Tutorial

  • Thanks for writing this up. A couple of notes: The tutorial is missing the stylesheet markup to improve the design. Also the last command would normally be ionic serve 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *