Using Plupload As A Global Uploader In AngularJS
For years, I've been using Plupload, as an HTML5 uploader, with great success. But, typically, I use Plupload in a one-off basis. Meaning, one context, one uploader. This works well but, it requires the user to remain in a single context while her files are being uploaded. As an experiment, I wanted to see if I could break that paradigm and create a single, global uploader that handles uploads asynchronously. Then, each unique context could create dropzones and file-inputs that simply "hook into" the global uploader. This would allow the user to move around the single-page application while her files upload in the background.
View this project on my GitHub account.
In the past, I've used the BeforeUpload event to create just-in-time database records immediately prior to upload. This works nicely; but, it makes it difficult to represent the "pending" data since there is no persisted counterpart. As such, in this experiment, I wanted to widen the gap between record-creation and file-uploading. In this experiment, I'm creating the image records before the selected files even get added to the global uploader queue.
This approach has benefits and drawbacks. The benefit is that it forces you to think about the persisted data and the "media" as two parallel concepts that exist symbiotically. This removes a lot of the fear around "what if my upload fails" - then it fails, and you deal with it in the application. Creating loose-coupling between these two aspects of the data model also allows them to be scaled independently and pushed to different remote servers (as we're doing in this experiment, using Plupload to push the file directly to Amazon S3).
The drawback is that your application now needs to know if and when the media files have been processed; and, it needs to be able to represent a record that is missing its associated media file. In this experiment, I haven't quite gone that far, for the sake of simplicity. As image records are rendered locally, I assume the media file is missing until a "file uploaded" event is triggered. But, there's no persisted flag that indicates whether or not the media file has been processed (ie, uploaded).
As far as I'm concerned, the benefits outweigh the drawbacks.
In my experiment, the Controller saves the record and then broadcasts an event that the global uploader is listening for. The global uploader then uploads the specified file and broadcasts an event when the upload is complete. All of the relevant controllers listen for that "uploaded" event and then update their local view-model as needed.
All of the communication is done inside Controllers and Directives, but the following is the HTML (view) for the experiment. As you can see, there is a Global Uploader at the top; this is a fixed-position element that hangs out at the bottom of the user's browser window. Each section then has its own dropzone which can accept files from the user. Since the global uploader exists outside of the sections, the user can navigate from one section to the other without aborting the currently queued file uploads.
<!doctype html>
<html ng-app="PluploadApp">
<head>
<meta charset="utf-8" />
<title>
Using Plupload As A Global Uploader In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="css/app.css"></link>
</head>
<body ng-controller="HomeController">
<h1>
Using Plupload As A Global Uploader In AngularJS
</h1>
<!-- BEGIN: Global Uploader. -->
<div
bn-global-uploader
class="m-global-uploader"
ng-class="{ active: queue.length }">
<ul class="queue">
<li
ng-repeat="item in queue track by item.id"
class="item">
{{ item.percent }}%
</li>
</ul>
</div>
<!-- END: Global Uploader. -->
<!-- BEGIN: Section Selection. -->
<p class="m-section-selector">
Choose a section:
<a
ng-click="showSectionOne()"
ng-class="{ active: ( section == 'one' ) }"
>Section One</a>
|
<a
ng-click="showSectionTwo()"
ng-class="{ active: ( section == 'two' ) }"
>Section Two</a>
</p>
<!-- END: Section Selection. -->
<!-- BEGIN: Sections. -->
<div ng-switch="section">
<!-- BEGIN: Section One. -->
<div
ng-switch-when="one"
ng-controller="SectionOneController">
<h2>
Section One
</h2>
<!-- BEGIN: Local Uploader. -->
<div
bn-section-uploader="saveFiles( files )"
class="m-section-uploader">
<div class="instructions">
<span class="ready">Drag & Drop Images for Section One</span>
<span class="hold">Preparing Upload...</span>
</div>
</div>
<!-- END: Local Uploader. -->
<!-- BEGIN: Image List. -->
<ul class="m-images">
<li
ng-repeat="image in images"
class="image">
<div ng-switch="image.isPlaceHolder" class="thumbnail">
<span ng-switch-when="true">Uploading...</span>
<img ng-switch-when="false" ng-src="{{ image.imageUrl }}" />
</div>
<div class="name">
{{ image.clientFile }}
</div>
<a ng-click="deleteImage( image )" class="delete">×</a>
</li>
</ul>
<!-- END: Image List. -->
</div>
<!-- END: Section One. -->
<!-- BEGIN: Section Two. -->
<div
ng-switch-when="two"
ng-controller="SectionTwoController">
<h2>
Section Two
</h2>
<!-- BEGIN: Local Uploader. -->
<div
bn-section-uploader="saveFiles( files )"
class="m-section-uploader">
<div class="instructions">
<span class="ready">Drag & Drop Images for Section Two</span>
<span class="hold">Preparing Upload...</span>
</div>
</div>
<!-- END: Local Uploader. -->
<!-- BEGIN: Image List. -->
<ul class="m-images">
<li
ng-repeat="image in images"
class="image">
<div ng-switch="image.isPlaceHolder" class="thumbnail">
<span ng-switch-when="true">Uploading...</span>
<img ng-switch-when="false" ng-src="{{ image.imageUrl }}" />
</div>
<div class="name">
{{ image.clientFile }}
</div>
<a ng-click="deleteImage( image )" class="delete">×</a>
</li>
</ul>
<!-- END: Image List. -->
</div>
<!-- END: Section Two. -->
</div>
<!-- END: Sections. -->
<!-- Vendor Scripts. -->
<script type="text/javascript" src="vendor/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="vendor/angular/angular-1.2.26.min.js"></script>
<script type="text/javascript" src="vendor/plupload/plupload.full.min.js"></script>
<script type="text/javascript" src="vendor/lodash/lodash-2.4.1.min.js"></script>
<!-- Application Scripts. -->
<script type="text/javascript" src="app/app.js"></script>
<script type="text/javascript" src="app/global-uploader/global-uploader-directive.js"></script>
<script type="text/javascript" src="app/home/home-controller.js"></script>
<script type="text/javascript" src="app/section-one/section-controller.js"></script>
<script type="text/javascript" src="app/section-two/section-controller.js"></script>
<script type="text/javascript" src="app/section-uploader/section-uploader-directive.js"></script>
<script type="text/javascript" src="app/services/image-service.js"></script>
<script type="text/javascript" src="app/services/lodash-service.js"></script>
<script type="text/javascript" src="app/services/natural-sort-service.js"></script>
<script type="text/javascript" src="app/services/plupload-service.js"></script>
</body>
</html>
While separating the file upload from its associated record does present some new challenges, I think the benefits are profound. This is definitely the direction I want to be moving in; and, as always, I'm super excited that Plupload makes this all so [relatively] easy! Damn you Plupload, I wish I could quit you!
Want to use code from this post? Check out the license.
Reader Comments
Thank you Ben for this post. I'll implement it in the existing environment and then if it works well, I'll come back to tell you how much I love you :)
@Dani,
Ha ha, so very glad to help :D