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

Drag and Drop File Uploads to Firebase Storage

Episode 14 written by Jeff Delaney
full courses for pro members


Good news! Firebase Storage is now supported natively in AngularFire2. Checkout the AngularFire Storage Dropzone lesson for the latest and greatest information.

In this lesson, I am expanding on the my previous file upload lesson with a drag and drop directive from scratch. There are several JS libraries, such as dropzone, that provide this functionality, but it’s not always easy to implement with backend storage. So let’s just do it with pure Angular/TypeScript code. The goal is to allow users to drop files into a div in Angular that will automatically trigger the upload process to Firebase storage.

ng g service upload
ng g component upload-form component
ng g directive file-drop

Building the Service

First, we need a basic Upload class that defines the specific attributes we want to set before pushing the file to the storage and/or the database.

upload.ts

export class Upload {

$key: string;
file:File;
name:string;
url:string;
progress:number;
createdAt: Date = new Date();

constructor(file:File) {
this.file = file;
}
}

I’m not going to go into detail here because this is covered in a separate file upload lesson, check it out for more detail on the upload service code. Basically, we import the Firebase SDK, make a reference to the storage location, then interact with the promise as it uploads the files. Here is the minimum code necessary in the service to use the drag and drop feature.

upload.service.ts

import { Injectable } from '@angular/core';
import { Upload } from './upload';
import { AngularFireDatabase } from 'angularfire2/database';
import * as firebase from 'firebase';


@Injectable()
export class UploadService {

constructor(private db: AngularFireDatabase) { }

pushUpload(upload: Upload) {
let storageRef = firebase.storage().ref();
let uploadTask = storageRef.child(`uploads/${upload.file.name}`).put(upload.file);

uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED,
(snapshot) => {
// upload in progress
upload.progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100
},
(error) => {
// upload failed
console.log(error)
},
() => {
// upload success
upload.url = uploadTask.snapshot.downloadURL
upload.name = upload.file.name
this.saveFileData(upload)
}
);
}



// Writes the file details to the realtime db
private saveFileData(upload: Upload) {
this.db.list(`uploads`).push(upload);
}

}

File Drop Attribute Directive

drag and drop file uploads angular with firebase storage

We are going to use an Angular attribute directive for file uploads, instead of a component. The drag and drop field does not need a template, so it’s a perfect situation for a custom directive.

The purpose of the drop files directive is to receive the raw files as a JavaScript FileList, then emit them to the parent component. It will also emit a custom event when files are hovered on the the drop zone area. Here’s how it works step-by-step.

1 First we create the custom events using the @Output decorator and EventEmitter. The filesDropped event will emit the FileList, and the filesHovered event will emit a boolean.

2 The HostListener event can hook into standard JavaScript browser events. When files are dropped into element, we can get the transferred data and emit it to the parent component. It also necessary to call preventDefault() because the browser will navigate to the local file path when the files are dropped.

3 The directive also listens to the dragover and dragleave events to toggle and emit the custom filesHovered event. In the component, we will use this state to change the CSS class when the user is hovering over the drop zone with files.

file-drop.directive.ts

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

@Directive({
selector: '[fileDrop]'
})
export class FileDropDirective {

@Output() filesDropped = new EventEmitter<FileList>();
@Output() filesHovered = new EventEmitter<boolean>();

constructor() { }

@HostListener('drop', ['$event'])
onDrop($event) {
$event.preventDefault();

let transfer = $event.dataTransfer;
this.filesDropped.emit(transfer.files);
this.filesHovered.emit(false);
}

@HostListener('dragover', ['$event'])
onDragOver($event) {
event.preventDefault();

this.filesHovered.emit(true);
}

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


}

Upload Form Component

The upload-form component is responsible for receiving the files, converting them to the Upload class, then triggering upload service. The component listens for the filesDropped event, and when it occurs it loops through the FileList with Lodash, and pushes each file to firebase storage.

It also listens to the filesHovered event and toggles the dropzoneActive variable. In the template, this variable is used with ngClass to show the user when the files are hovered.

upload-form.component.ts

import { Component } from '@angular/core';
import { UploadService } from '../shared/upload.service';
import { Upload } from '../shared/upload';
import * as _ from "lodash";

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

currentUpload: Upload;
dropzoneActive:boolean = false;

constructor(private upSvc: UploadService) { }

dropzoneState($event: boolean) {
this.dropzoneActive = $event;
}

handleDrop(fileList: FileList) {

let filesIndex = _.range(fileList.length)

_.each(filesIndex, (idx) => {
this.currentUpload = new Upload(fileList[idx]);
this.upSvc.pushUpload(this.currentUpload)}
)
}

}

In the template, the attribute directive will emit the events automatically, so we just need to tell the component which function to run when they happen.

<div *ngIf="currentUpload">
<progress class="progress is-success" min=1 max=100 value="{{ currentUpload?.progress }}"></progress>
Progress: {{currentUpload?.name}} | {{currentUpload?.progress}}% Complete
</div>
<div class="box">

<h2>Drop File</h2>

<div class="dropzone"
fileDrop
(filesDropped)="handleDrop($event)"
(filesHovered)="dropzoneState($event)"
[ngClass]="{'active': dropzoneActive}">

<i class="fa fa-cloud-upload fa-2x"></i>

</div>
</div>

Ideas for Improvement

The current directive works, but there are ways it can be improved. First, the files are not being validated, so it’s possible for the user to upload files of any size or format. It would be a good idea to throw an error if the file exceeds a maximum size or doesn’t match a certain format. Second, it would be useful to add a button that cancels the Firebase upload process if it hangs from a poor internet connection.

That’s it for drag and drop file uploads with Angular. Good luck!