Search Lessons, Code Snippets, and Videos
search by algolia
X
#native_cta# #native_desc# Sponsored by #native_company#

Firebase Storage With AngularFire - DropZone File Uploader

Episode 82 written by Jeff Delaney
full courses and content on fireship.io
>

In this lesson, I will show you how to take advantage of the brand new AngularFireStorageMoudule to build a dropzone file uploader from scratch. The look and feel is inspired by DropZone.js, but it does not actually use this library whatsoever - we’re building our own Angular version from scratch. In addition to uploading files to Firebase Storage, it can also monitor the upload task progress and provides UI buttons to pause, cancel, or resume the upload.

Grab the full source code for my angularfire2 storage demo.

A demo of the storage module in AngularFire2

Initial Setup

First things first, you need to have a firebase account and project, then follow the official setup guide for AngularFire2. Your app module should look something like this.

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

import { AngularFirestoreModule } from 'angularfire2/firestore';
import { AngularFireStorageModule } from 'angularfire2/storage';


@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
AngularFireModule.initializeApp(environment.firebase),
AngularFirestoreModule,
AngularFireStorageModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Firestore is not required, but I want to show you how to save the storage file path in the database so it can be accessed after the initial upload.

Drop Zone Directive

Generate a custom directive to handle drag/drop activity.

ng g directive dropZone

Custom Drop Event and FileList

A dropzone is nothing more than an <div> that listens for the drop event. In our case, it emits the dropped FileList off to AngularFire to do all the uploading magic.

In addition to the drop, the directive will also listen to dragover and dragleave to emit a custom hovered event used to toggle CSS classes. When a user drags over the drop zone, it will add a solid border to the div to indicate that the file is ready to be dropped.

import { Directive, HostListener, HostBinding, Output, EventEmitter } from '@angular/core';

@Directive({
selector: '[dropZone]'
})
export class DropZoneDirective {

@Output() dropped = new EventEmitter<FileList>();
@Output() hovered = new EventEmitter<boolean>();

constructor() { }

@HostListener('drop', ['$event'])
onDrop($event) {
$event.preventDefault();
this.dropped.emit($event.dataTransfer.files);
this.hovered.emit(false);
}

@HostListener('dragover', ['$event'])
onDragOver($event) {
$event.preventDefault();
this.hovered.emit(true);
}

@HostListener('dragleave', ['$event'])
onDragLeave($event) {
$event.preventDefault();
this.hovered.emit(false);
}

}

The File Upload Component

The FileUploadComponent will handle the the actual file upload task.

ng g file-upload

file-upload.component.ts

The AngularFireUploadTask object is the crux of the component. Here are some important points to keep in mind:

  • Calling storage.upload(path, file) creates a task that will start the upload immediately, no need to subscribe.
  • You know an upload is complete when bytesTransferred equal totalBytes.
  • You monitor progress by subscribing to percentageChanges or snapshotChanges on the task.
import { Component, OnInit } from '@angular/core';
import { AngularFireStorage, AngularFireUploadTask } from 'angularfire2/storage';
import { Observable } from 'rxjs/Observable';

@Component({
selector: 'file-upload',
templateUrl: './file-upload.component.html',
styleUrls: ['./file-upload.component.scss']
})
export class FileUploadComponent {

// Main task
task: AngularFireUploadTask;

// Progress monitoring
percentage: Observable<number>;

snapshot: Observable<any>;

// Download URL
downloadURL: Observable<string>;

// State for dropzone CSS toggling
isHovering: boolean;

constructor(private storage: AngularFireStorage, private db: AngularFirestore) { }


toggleHover(event: boolean) {
this.isHovering = event;
}


startUpload(event: FileList) {
// The File object
const file = event.item(0)

// Client-side validation example
if (file.type.split('/')[0] !== 'image') {
console.error('unsupported file type :( ')
return;
}

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

// Totally optional metadata
const customMetadata = { app: 'My AngularFire-powered PWA!' };

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

// Progress monitoring
this.percentage = this.task.percentageChanges();
this.snapshot = this.task.snapshotChanges()

// The file's download URL
this.downloadURL = this.task.downloadURL();
}

// Determines if the upload task is active
isActive(snapshot) {
return snapshot.state === 'running' && snapshot.bytesTransferred < snapshot.totalBytes
}

}

file-upload.component.html

The user has two UI options for uploading a file. (1) They can drop a file into the dropzone div or (2) click the choose file button to bring up the file select screen on their device. In both cases, we will listen for the event to emit a FileList object, then fire startUpload handler.

<div class="dropzone" 
dropZone
(hovered)="toggleHover($event)"
(dropped)="startUpload($event)"
[class.hovering]="isHovering">



<h3>AngularFire Drop Zone</h3>

<div class="file">
<label class="file-label">


<input class="file-input" type="file" (change)="startUpload($event.target.files)">


<span class="file-cta">
<span class="file-icon">
<i class="fa fa-upload"></i>
</span>
<span class="file-label">
or choose a file…
</span>
</span>
</label>
</div>
</div>

Just below our DropZone, we give the user some feedback about the upload progress and a few buttons to pause, cancel, or resume the current task. When the user clicks pause, the progress bar should stop in place.

Pause or resume an upload to Firebase storage and monitor it with a progress bar

<div *ngIf="percentage | async as pct">

<progress class="progress is-info"
[value]="pct"
max="100">
</progress>

{{ pct | number }}%

</div>


<div *ngIf="snapshot | async as snap">
{{ snap.bytesTransferred | fileSize }} of {{ snap.totalBytes | fileSize }}

<div *ngIf="downloadURL | async as url">
<h3>Results!</h3>
<img [src]="url"><br>
<a [href]="url" target="_blank" rel="noopener">Download Me!</a>
</div>

<button (click)="task.pause()" class="button is-warning" [disabled]="!isActive(snap)">Pause</button>
<button (click)="task.cancel()" class="button is-danger" [disabled]="!isActive(snap)">Cancel</button>
<button (click)="task.resume()" class="button is-info" [disabled]="!(snap?.state === 'paused')">Resume</button>

</div>

file-upload.component.scss

The CSS is very simple, just a flexbox with everything centered and an extra class for the hovering state. The progress bar can be animated with just simple CSS transition - otherwise it will look jerky as it uploads the file in chunks. FYI, I am also using Bulma for the base CSS, so you will need that to fully replicate the look of this drop zone and buttons.

Make sure to add a CSS transition animation to the progress bar to smooth out the steps.

.dropzone { 
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 300px;
border: 2px dashed #f16624;
border-radius: 5px;
background: white;
margin: 10px 0;

&.hovering {
border: 2px solid #f16624;
color: #dadada !important;
}
}

progress::-webkit-progress-value {
transition: width 0.1s ease;
}

Bonus 1 - Save File Information in the Firestore Database

Connect Firebase storage uploads to the Firestore NoSQL database.

You’re probably wondering how a user might access image data after the initial upload. We can save the path to the file in Firestore for easy access at a later time with just a few modifications to the file-upload.component.ts code.

First, inject AngularFirestore in the constructor. Second, pipe in the tap operator to the snapshot Observable to fire an update to database when the task is complete.

import { AngularFirestore } from 'angularfire2/firestore';
import { tap } from 'rxjs/operators';

export class FileUploadComponent {

// ...omitted

constructor(private storage: AngularFireStorage, private db: AngularFirestore) { }

// ...omitted

this.snapshot = this.task.snapshotChanges().pipe(
tap(snap => {
if (snap.bytesTransferred === snap.totalBytes) {
// Update firestore on completion
this.db.collection('photos').add( { path, size: snap.totalBytes })
}
})
)

}

Bonus 2 - File Size Pipe

In the component HTML, we used the fileSize pipe to convert raw bytes to strings that look like 23KB, 0.23MB, etc. This code is almost identical to the Custom Pipes code provided by Rangle.io.

ng g pipe fileSize
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: 'fileSize'
})
export class FileSizePipe implements PipeTransform {

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 size ? `${formattedSize} ${unit}` : '0';
}

}

The End

Overall, I am very happy to see the Firebase storage support in AngularFire. It fits nicely with the existing API and makes file upload features incredibly simple to implement in Angular.