How to upload and display image file in PWA/Angular project using Firebase-Cloud-Storage and AngularFire.

Ankit Maheshwari
9 min readDec 21, 2019

--

You’ll learn to upload and download your files to Firebase Cloud Storage from your PWA or Angular App.

Firebase Cloud Storage is a server-less way to upload and download binary files from the browser. As of today, it’s officially supported by AngularFire. AngularFire allows your app to easily and securely manage a Cloud Storage bucket without a line of server code.

Cloud Storage with AngularFire

AngularFire is the official library that connects Angular to Firebase.
AngularFireincludes modules for the Realtime Database, Firebase Authentication, Cloud Firestore, and Cloud Storage.

#1 Adding Firebase & AngularFire to your project.

It’s easy to add AngularFire to your project. Install Firebase and AngularFire from npm. Follow the link for steps — Click here 👆

Okay, I assume you have followed each step from above link↑ to Add Firebase & AngularFire to your project.

Please comment in the comment box below if anything does not works for you! I’ll be happy to Help :)

#2 Configure AngularFireStorage module.

We need to register our app module with AngularFireStorageModule . Let's finish this by importing the corresponding AngularFire modules in our app.module.ts file:

import { AngularFireModule } from '@angular/fire';
import { AngularFireStorageModule } from '@angular/fire/storage';
...
imports: [
BrowserModule,
AngularFireModule.initializeApp(environment.firebase),
...
AngularFireStorageModule
],
providers: [
...
AuthService
],
...

Next, Injecting the AngularFireStorage service

Once the AngularFireStorageModule is registered we can inject the AngularFireStorage service in our app.component.ts or home.page.ts or any other component file.

Feel free to add this code in any component file.

Adding this code in our component: ( home.page.ts file)

import { Component } from '@angular/core';
import { AngularFireStorage } from '@angular/fire/storage';

@Component({
selector: 'app-home',
template: 'home.page.html'
})
export class HomePage {
constructor(private afStorage: AngularFireStorage) { }
}

Now we’re ready to manage our files from Firebase Cloud Storage without a server.

#3 How we upload file.

The primary way of uploading files on the web is through the
<input type="file"> tag.

<label for="file">File:</label>
<input type="file" (change)="upload($event)" accept=".png,.jpg" />

This tag fires a (change) event when the user selects a file. It even allows us to restrict the user to upload .png and .jpg formats with the accept attribute. (beware that this is only a client restriction, we'll cover server restrictions later).
It's easy to handle a (change) event using Angular-Event-bindings and send the file to Cloud Storage.

Open up your app.component.ts or home.page.ts file and declare these variables:

...
export class HomePage {
...
ref: AngularFireStorageReference;
task: AngularFireUploadTask;
...
}

In your app.component.ts or home.page.ts file and add this upload function using Arrow Function:

...
export class HomePage {
ref: AngularFireStorageReference;
task: AngularFireUploadTask;

...

// function to upload file
upload = (event) => {
// create a random id
const randomId = Math.random().toString(36).substring(2);
// create a reference to the storage bucket location
this.ref = this.afStorage.ref('/images/' + randomId);
// the put method creates an AngularFireUploadTask
// and kicks off the upload
this.task = this.ref.put(event.target.files[0]);
}
...
}

This upload function creates an AngularFireStorageReference with a randomly generated Id. This reference controls a path in your Cloud Storage bucket. Creating a reference allows to delete the file saved at that location. Calling .put() on a reference with a Blob beings the upload to Cloud Storage.

The task: AngularFireUploadTask is allow us to monitor the upload progress.

Displaying the upload progress using Reactive upload method.

The web platform provides an easy and accessible way of displaying the progress of any given task through the aptly named <progress> element.

<progress max="100" [value]="(uploadProgress | async)"></progress>

The progress element’s value attribute is easy to control with Angular's property binding. AngularFire provides an upload observable that we can pipe in the new value as it changes.

In your app.component.ts or home.page.ts file - Import map then declare variable uploadProgress then update your upload function (add uploadProgress)

import { map } from 'rxjs/operators';
...

export class HomePage {
ref: AngularFireStorageReference;
task: AngularFireUploadTask;
uploadProgress: Observable<number>;
...

// function to upload file
upload = (event) => {
// create a random id
const randomId = Math.random().toString(36).substring(2);
// create a reference to the storage bucket location
this.ref = this.afStorage.ref('/images/' + randomId);
// the put method creates an AngularFireUploadTask
// and kicks off the upload
this.task = this.ref.put(event.target.files[0]);

// AngularFireUploadTask provides observable
// to get uploadProgress value
this.uploadProgress = this.task.snapshotChanges()
.pipe(map(s => (s.bytesTransferred / s.totalBytes) * 100));
}
...
}

this.task.snapshotChanges() : The .snapshotChanges() method on an AngularFireUploadTask returns an object with helpful metadata about the upload progress.

this.uploadProgress : This uploadProgress will have properties such as the totalBytesTransferred, totalBytes in the upload, any metadata provided, the state of the upload, and the downloadURL of the file once uploaded. Using this metadata we can update the UI to show the progress.

Most of the time our UI display the upload percentage and download url. Since this a common task, we made this a bit easier with two helpful methods: .percentageChanges() and .downloadURL().

Again in your app.component.ts or home.page.ts file - Import finalize then declare variable downloadURL then update your upload function:

import { map, finalize } from 'rxjs/operators';
...

export class HomePage {
ref: AngularFireStorageReference;
task: AngularFireUploadTask;
uploadProgress: Observable<number>;
downloadURL: string;
...

// function to upload file
upload = (event) => {
// create a random id
const randomId = Math.random().toString(36).substring(2);
// create a reference to the storage bucket location
this.ref = this.afStorage.ref('/images/' + randomId);
// the put method creates an AngularFireUploadTask
// and kicks off the upload
this.task = this.ref.put(event.target.files[0]);

// AngularFireUploadTask provides observable
// to get uploadProgress value
// this.uploadProgress = this.task.snapshotChanges()
// .pipe(map(s => (s.bytesTransferred / s.totalBytes) * 100));

// observe upload progress
this.uploadProgress = this.task.percentageChanges();
// get notified when the download URL is available
this.task.snapshotChanges().pipe(
finalize(() => this.downloadURL = this.ref.getDownloadURL())
)
.subscribe();
}
...
}

Inside upload function we're using the .percentageChanges() observable, instead of calculating upload progress. The .getDownloadURL() observable emits the download URL string once the upload is completed. This simplifies binding uploadProgress to our UI.

Open up your app.component.html or home.page.html file and add this code:

<label for="file">File:</label>
<input type="file" (change)="upload($event)" accept=".png,.jpg" />

<progress max="100" [value]="(uploadProgress | async)"></progress>
<div class="result" *ngIf="downloadURL | async; let uploadSrc">
<a [href]="uploadSrc">You just uploaded this file!</a>
</div>

<div class="container" *ngIf="downloadURL | async; let url">
<img [src]="url" alt="Image from AngularFireStorage">
</div>

By using observable methods we don’t need to worry about complex mapping or multiple .subscribe() calls in our component code. Just bind them to the async pipe and our users will watch their file upload progress.

But what if your user changes their mind? What if they want to pause their upload because they’re no longer on a WiFi network? Will they be able to resume when they want? What if they want to cancel the upload all together?
For that we need to use Control uploads…

#4 Controlling uploads/downloads.

A user should be able to pause, resume, or cancel an upload progress. If or when their upload completes they should also be able to delete it.

An AngularFireUploadTask contains the following appropriately named methods: .pause(), .resume() , and .cancel() . In the sample above, the component stores the task as an instance property. This allows us to call it from our template.

Open up your app.component.html or home.page.html file and update your code:

<label for="file">File:</label>
<input type="file" (change)="upload($event)" accept=".png,.jpg" />

<progress max="100" [value]="(uploadProgress | async)"></progress>
<div class="result" *ngIf="downloadURL | async; let uploadSrc">
<a [href]="uploadSrc">You just uploaded this file!</a>
</div>

<section class="control-bar">
<button (click)="task.pause()">Pause</button>
<button (click)="task.resume()">Resume</button>
<button (click)="task.cancel()">Cancel</button>
</section>

<div class="container" *ngIf="downloadURL | async; let url">
<img [src]="url" alt="Image from AngularFireStorage">
</div>

Ideally we’ll want to update our UI so the user knows the state of their upload. The .snapshotChanges() observable emits this information through the state property.

Open up your app.component.ts or home.page.ts file and declare variable uploadState then update by adding this.uploadState at the bottom of upload function:

export class HomePage {
...
uploadState: Observable<string>;
...

// function to upload file
upload = (event) => {
...
this.uploadState = this.task.snapshotChanges().pipe(map(s => s.state));
}
...
}

When the user taps “Pause”, the 'paused' state emits from the observable. When the user taps "Resume", the 'running' state emits. And of course, when the "Pause" button is tapped, the 'paused' state emits.

Open up your app.component.html or home.page.html file and add modify your control-bar code:

<label for="file">File:</label>
<input type="file" (change)="upload($event)" accept=".png,.jpg" />

<progress max="100" [value]="(uploadProgress | async)"></progress>
<div class="result" *ngIf="downloadURL | async; let uploadSrc">
<a [href]="uploadSrc">You just uploaded this file!</a>
</div>

<section class="control-bar" *ngIf="uploadState | async; let state">
<button (click)="task.pause()" [disabled]="state === 'paused'">Pause</button>
<button (click)="task.cancel()" [disabled]="!(state === 'paused' || state === 'running')">Cancel</button>
<button (click)="task.resume()" [disabled]="state === 'running'">Resume</button>
</section>

<div class="container" *ngIf="downloadURL | async; let url">
<img [src]="url" alt="Image from AngularFireStorage">
</div>

The template above will disable each button depending on the state. A user can cancel a 'running' upload, and they can click "Resume" only when the state is 'paused' .

We have simplified our template by binding the UI to the upload state because we don’t have to manage stateful properties on our component like: isPaused, isUploading, and isFinished. The state emits from the observable as we have bind the expression in our template.

Simple downloads

Downloading files from Firebase Cloud Storage is really very simple with AngularFireStorage. The .getDownloadURL() method on an AngularFireStorageReference returns an observable of a download URL.

Open up your component file and add code to download file:
(You can also separate your template code from component file and put that in your template file.)

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

@Component({
selector: 'app-root',
template: `
<div class="container" *ngIf="downloadURL | async; let url">
<img [src]="url" alt="Image from AngularFireStorage">
</div>`
})
export class AppComponent implements OnInit {
...

constructor(private afStorage: AngularFireStorage) { }

ngOnInit(event) {
// this code returns the download url of image
this.downloadURL = this.afStorage.ref('/images/my_file_name.png').getDownloadURL();
}
}

Combining the .getDownloadURL() method with the *ngIf directive and using async pipe which allow us to display the image once it's downloaded.

It’s very easy now to upload and download files, but.. but.. but.. we need to secure them from unauthorized access.

#5 Secure your files.

Firebase Cloud Storage comes with a server security rule set. These security rules ensures the proper file formats are uploaded and only the right users have access to the right files.

The <input type="file" access=".png,.jpg"> tag restricts file uploads to .png and .jpg. However, this is only a client side restriction. It has nothing to do with what Cloud Storage will accept. To secure our storage bucket we need to write rules that only allow images to be uploaded at the given /images path.

Open up your Firebase Storage Rules from Firebase console and add these rules:
(Scroll down and See video below to understand how)

service firebase.storage {
match /b/{bucket}/o {
match /images {
// Cascade read to any image type at any path
match /{allImages=**} {
allow read;
}
// Allow write files to the path "images/*", subject to the constraints:
// 1) File is less than 5MB
// 2) Content type is an image
match /{imageId} {
allow write: if request.resource.size < 5 * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
}
}
}
}

The rules above will allow anyone to read from the /images path, but only upload a file if it has an 'image/.*' content type (.png, .jpg, .jpeg, etc...) and should less than 5MB.

See this video to add Rules in Firebase Cloud Storage 👇👇

Open up your Firebase Storage Rules from Firebase console and add these rules.

💁‍♂️ See this in Action👇👇 (How it works…)

Done! 🤩 It’s that simple to upload and display image file using AngularFire.

See you later👋👋

Feel free to comment down in the comment box… If I missed anything or anything is incorrect or anything does not works for you :)

Stay connected for more articles:
https://medium.com/@AnkitMaheshwariIn

If you wouldn’t mind giving it some claps 👏 👏 since it helped, I’d greatly appreciate it :) Help others find the article, so it can help them!

Always be clapping…

--

--