fbpx

I am always looking for ways to improve efficiency and reduce the amount of code I have to write. I am currently working on a project with the Ionic framework and one of the problems I had to solve was to upload an image to Firebase Cloud storage. I thought, the aspect of uploading an image, is clearly independent from the rest of the app, so why not isolate it from the rest of the code? Since, I was working with Angular, this “looked like a job for a directive”. Uploading an image to cloud storage is functionality that would be common across several apps. Therefore if done with an Angular directive, it can be reused across other apps. In this post, I will talk about how to achieve this as well as share the code via my Github repo mdt_ionic_tutorials.

Problem

I want to upload an image to a specific path in Firebase cloud storage and I also want to know when the asynchronous uploading process is complete.

Firebase, Storage & AngularFire

I have been using the AngularFire library and it does make things more convenient when working with Angular/Firebase. I won’t be talking about how to upload a file to Firebase storage with AngularFireStorage. There are already some really good tutorials out there for this so you can just refer to those. For starters, you can have a read of the official docs here. That should tell you everything you need to know about uploading files to FireStorage with AngularFire.

Directives

I have been working with directives since AngularJS i.e. Angular 1.x onwards and they were a little simpler in earlier versions. From Angular 2 onwards, Google added a bit of depth to it, by further separation of concerns. As such there are now 3 types of directives in Angular

  1. Attribute directives: They are only used as attributes of HTML elements i.e. they change the appearance or behaviour of the element but not the structure. A simple example would be the ng-class directive i.e. something you can use to change the colour of an element
  2. Structural directive: This is the next level, these directives effect the structure of the HTML element e.g. the ng-if directive. When do we use ng-if? to show or hide an html element therefore we are changing the html structure via ng-if
  3. Components: These directives have a greater effect, they come into a page with their own html (template). You know how we have HTML tags like <p> <a> <div> etc, once we define a component directive, we create our own unique tag. To achieve the aforementioned image-upload functionality, I built the image upload component using a component directive. You will see the code for it as you read on

Communicating with components

Ok, let’s take a step back and think about the problem we are trying to solve?

  • We want to upload an image to Firebase storage
    • Naturally the image must be saved to some path?
  • Next we want the component to be reusable across apps
    • Now, to be able to reuse it means, we cannot hard code that path into the component?
  • Uploading an image is obviously an async process,
    • How do we know once our component has finished uploading the image?

So now that we have thought through that, we have some interesting questions. Mainly, we need to know how we can communicate with our component. The answer to the above questions lies in the @Input and @Output properties.

Like the blog? Subscribe for updates

@Input & @Output

The @Input and the @Output decorator are… fun to use. To understand them, search for “decorator pattern” on the internet. Even if you don’t dive deep into it, it doesn’t hurt to know a little more about this heavily applied design pattern. Or if you have worked with Spring Framework with Java, then you shouldn’t have any problems understanding these.

Our objective is: not hard code the Firebase storage path where these images are uploaded and the @Input decorator will help us achieve that. It’s a class level property that’s bound to a DOM property in the template.

Next up, we need to let the component’s parent class e.g. image-upload.page.ts that we are done uploading the file. The @Output decorator combined with the EventEmitter will help us with that.

I will talk about the @Input and @Output decorators a little more when I show the code for the image upload component. If you need to know more about these then have a read of the official docs.

The Code

I was working on an Ionic app while building this functionality, so to get started with component based directive, go to your project’s directory on console and run this command,

ionic g component name

which in my case was,

ionic g component components/firebase-image-upload

You will see that it will generate 4 files for the component,

  • .spec.ts test file
  • .scss file for our css
  • .html template file
  • .ts i.e. the class file

In this post, we will only be looking at the template file and the class file for the component, starting with, firebase-image-upload.component.html

<ion-card>
<ion-card-header>
    <ion-card-title>
    Upload Image
    </ion-card-title>
</ion-card-header>
<ion-card-content>
    <div *ngIf="uploadProgress | async as pct">
    Progress: {{ round(pct | number) }}%
    <ion-progress-bar value="{{ pct / 100 }}"></ion-progress-bar>
    </div>
    <ion-item>
    <ion-spinner [hidden]="!isUploading" name="lines"></ion-spinner>
    <div class="btn btn-primary">
        <input type="file"
            id="file"
            (change)="handleFileInput($event.target.files)">
        <ion-button (click)="upload()"> Upload </ion-button>
    </div>
    </ion-item>
</ion-card-content>
</ion-card>

Like the blog? Subscribe for updates

Followed by the firebase-image-upload.component.ts

import { Component, OnInit, EventEmitter,Input, Output } from '@angular/core';
import { AngularFireStorage, AngularFireUploadTask } from '@angular/fire/storage';
import { AlertController,LoadingController } from '@ionic/angular';
import { Observable } from 'rxjs';
import { finalize, tap } from 'rxjs/operators';
import { AuthService } from  '../../services/auth.service';

@Component({
selector: 'app-firebase-image-upload',
templateUrl: './firebase-image-upload.component.html',
styleUrls: ['./firebase-image-upload.component.scss'],
})
export class FirebaseImageUploadComponent implements OnInit {

@Input() path: string; 
@Output() outcome = new EventEmitter<any>(true);

task: AngularFireUploadTask;
uploadProgress: Observable<any>;
round = Math.round;
fileToUpload: File;
UploadedFireURL: Observable<string>;
filesize: number; 
isUploading = false;

constructor(private alertCtrl: AlertController,
            private fireStorage: AngularFireStorage) { }

ngOnInit() {
    console.log(this.path);
}

handleFileInput(files: FileList) {
    this.fileToUpload = files.item(0);
}
/*I am using it here, but in your production code, try to avoid
    storing and relying on class level variables. This gives
    the code state and you would have problems later on when 
    your user base grows. Then you may have scaling problems */
upload() {
    let filename = this.fileToUpload.name;
    const fullPath = `${this.path}/${new Date().getTime()}_${filename}`;

    const fileref = this.fireStorage.ref(fullPath);

    const customMetadata = { app: 'Upload demo' };
    // Totally optional metadata
    this.task = this.fireStorage.upload(fullPath, this.fileToUpload, { customMetadata });
    this.isUploading = true;
    this.task.catch(res => {
    this.isUploading = false;
    console.log("Error uploading the file");
    });
    this.uploadProgress = this.task.percentageChanges();
    this.uploadProgress.subscribe( percentage => {
    console.log(percentage);
    }, err => {
    this.isUploading = false;
    });
    this.task.snapshotChanges().pipe(
    finalize(() => {
        this.UploadedFireURL = fileref.getDownloadURL();
        this.UploadedFireURL.subscribe( urlStr => {
        //created an object for sake of clarity
        const uploadOutcome = {
            hasUploaded: true,
            uploadUrl: urlStr
        };
        this.outcome.emit(uploadOutcome);
        this.isUploading = false;
        this.uploadDone();
        this.uploadProgress = null;
        });
    }),
    tap(snap => {
        this.filesize = snap.totalBytes;
    })
    ).subscribe( res => {
    console.log(res);
    })
}
async uploadDone() {
    const alert = await this.alertCtrl.create({
    header: "👍",
    message: "Image uploaded 😊",
    buttons: ['Ok']
    });
    await alert.present();
}
}

The use of @Input & @Output

Notice how we use the @Input directive, we use it to specify a path i.e. a directory where we need to save the images we upload. We will specify the “path” when we use (declare) the component in our parent page.

Regarding the @Output property, we use it with the EventEmitter to emit and even with the upload state and the upload url once we have finished uploading.

TODO: I haven’t got it here, but maybe you can write the code to emit an event in case an upload failed?

Finally, how do we use this in our parent class? Here’s the code for image-upload.page.html

<ion-header>
    <ion-toolbar>
        <ion-title>Image upload</ion-title>
    </ion-toolbar>
</ion-header>

<ion-content padding>
    <h1> Upload images to firebase storage</h1>
    <p>
        This page demonstrates achieving this functionality via the use 
        of reusbale Angular components.
    </p>
    <app-firebase-image-upload
    path="Component"
    (outcome)="uploadFinished($event)"> </app-firebase-image-upload>
</ion-content>

and the corresponding image-upload.page.ts class

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

@Component({
selector: 'app-image-upload',
templateUrl: './image-upload.page.html',
styleUrls: ['./image-upload.page.scss'],
})
export class ImageUploadPage implements OnInit {

constructor() { }

ngOnInit() {
}
uploadFinished(outcomeObj) {
    if(outcomeObj.hasUploaded) {
        console.log("Image successfully uploaded with url...");
        console.log(outcomeObj.uploadUrl);
    }
}

Remember, before we can use this in our page, we need to import this into our page module

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Routes, RouterModule } from '@angular/router';

import { IonicModule } from '@ionic/angular';

import { ImageUploadPage } from './image-upload.page';
import { FirebaseImageUploadComponent } from '../../components/firebase-image-upload/firebase-image-upload.component';

const routes: Routes = [
{
    path: '',
    component: ImageUploadPage
}
];

@NgModule({
imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild(routes)
],
declarations: [ImageUploadPage, FirebaseImageUploadComponent]
})
export class ImageUploadPageModule {}

As you can see above, we declare our component app-firebase-image-upload in our template file specify the images need to be saved in “Component”.

<app-firebase-image-upload
path="Component"
(outcome)="uploadFinished($event)"> </app-firebase-image-upload>

Once, we are done uploading, using EventEmitter with @Output we emit an event to which we attach an object with the uploadState and uploadedFileUrl. Here’s the code from the firebase-image-upload.component.ts class.

const uploadOutcome = {
hasUploaded: true,
uploadUrl: urlStr
};
this.outcome.emit(uploadOutcome);

In our parent page, we bind the outcome property of our component to the uploadFinished method. This is so the parent class can know the state of the upload as well as the upload url.

uploadFinished(outcomeObj) {
    if(outcomeObj.hasUploaded) {
        console.log("Image successfully uploaded with url...");
        console.log(outcomeObj.uploadUrl);
    }
}

You can get access to all this code on my repository mdt_ionic_tutorials on Github.

p.s. I know, the name of the component is a bit too long but I have done so for sake of clarity.

Like the blog? Subscribe for updates

Conclusion

Directives in Angular are great and I quite like how they decomposed directives into 3 different types from Angular 2 onwards. In this post, we saw how to create a reusable directive to upload an image to Firebase Cloud Storage. This is a great way to build functionality that we can reuse it across other apps.

Remember, whenever you are trying to build any new functionality, give some thought to how exactly does it fit into your solution. Once you get a habit of it, you will come up with ways to write the same code over and over but rather focus your attention more on solving other interesting problems.

There we go, I finally managed to find the time to write a new post and my first one in the new year! Happy new Year!!!

As usual, if you find any of my posts useful support us by  buying or even trying one of our products and leave us a review on the app store.

‎My Day To-Do - Smart Task List
‎My Day To-Do - Smart Task List
‎My Day To-Do Lite - Task list
‎My Day To-Do Lite - Task list
‎Snap! I was there
‎Snap! I was there
Developer: Bhuman Soni
Price: Free
Numbers Game: Calculation Master
Numbers Game: Calculation Master
‎Simple 'N' Easy: Todos & food
‎Simple 'N' Easy: Todos & food
‎Captain's Personal Log
‎Captain's Personal Log
Developer: Bhuman Soni
Price: $4.99
My Simple Notes
My Simple Notes
Developer: Bhuman Soni
Price: Free
‎My Simple Notes - Dictate
‎My Simple Notes - Dictate
Developer: Bhuman Soni
Price: $2.99

0 Comments

Leave a Reply

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