Created
February 23, 2015 13:39
Consuming Plupload Data URIs In An AngularJS Application
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
app.factory( | |
"dataUriCache", | |
function( $timeout ) { | |
// I store the cached data-uri values. Each value is intended to be cached at a | |
// key that represents the remote URL for the data item. | |
var cache = {}; | |
// I define the time (in milliseconds) that a cached data-uri before it is | |
// automatically flushed from the memory. | |
var cacheDuration = ( 120 * 1000 ); | |
// I store the eviction timers for the cached data-uri. | |
var timers = {}; | |
// Return the public API. | |
return({ | |
set: set, | |
get: get, | |
has: has, | |
remove: remove, | |
replace: replace | |
}); | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I cache the given data-uri under the given key (which is intended to be a URL | |
// that represents the remote version of the data). | |
// -- | |
// CAUTION: The data is not kept around indefinitely; once cached, it will be | |
// flushed from the cache within a relatively short time period. This way, the | |
// browser doesn't get bloated with data that is not going to be accessed. | |
function set( key, dataUri ) { | |
// Normalize the key so we don't accidentally conflict with built-in object | |
// prototype methods and properties. | |
cache[ key = normalizeKey( key ) ] = dataUri; | |
$timeout.cancel( timers[ key ] ); | |
timers[ key ] = $timeout( | |
function clearCache() { | |
console.warn( "Expiring data-uri for %s", key ); | |
delete( cache[ key ] ); | |
// Clear the closed-over variables. | |
key = dataUri = null; | |
}, | |
cacheDuration, | |
// Don't trigger digest - the application doesn't need to know about | |
// this change to the data-model. | |
false | |
); | |
return( dataUri ); | |
} | |
// I get the data-uri cached at the given key. | |
// -- | |
// NOTE: Returns NULL if not defined. | |
function get( key ) { | |
return( cache[ normalizeKey( key ) ] || null ); | |
} | |
// I determine if a data-uri is cached at the given key. | |
function has( key ) { | |
return( normalizeKey( key ) in cache ); | |
} | |
// I remove the data-uri cached at the given key. | |
function remove( key ) { | |
console.warn( "Evicting data-uri for %s", key ); | |
$timeout.cancel( timers[ key = normalizeKey( key ) ] ); | |
delete( cache[ key ] ); | |
} | |
// I return the data-uri cached at the given key. But, the remote object, | |
// represented by the cache-key (which is intended to be a URL) is loaded in the | |
// background. When (and if) the remote image is loaded, the cached data-uri is | |
// evicted from the cache. | |
function replace( key ) { | |
loadRemoteObject( key ); | |
return( get( key ) ); | |
} | |
// --- | |
// PRIVATE METHODS. | |
// --- | |
// I load the remote object represented by the given key (which is intended to be | |
// a URL). This will cache the object in the local browser cache, at which point | |
// the data-uri is evicted from the cache. | |
function loadRemoteObject( key ) { | |
angular.element( new Image() ) | |
.on( | |
"load", | |
function handleLoadEvent() { | |
console.info( "Remote object loaded at %s", key ); | |
// Now that the image has loaded, and is cached in the browser's | |
// local memory, we can evict the data-uri. | |
remove( key ); | |
// Clear the closed-cover variables. | |
key = null; | |
} | |
) | |
.on( | |
"error", | |
function handleErrorEvent() { | |
// Clear the closed-cover variables. | |
key = null; | |
} | |
) | |
.prop( "src", key ) | |
; | |
} | |
// I normalize the given key for use as cache or timer key. | |
function normalizeKey( key ) { | |
return( "url:" + key ); | |
} | |
} | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
app.controller( | |
"DetailController", | |
function( $scope, $exceptionHandler, imageService, dataUriCache ) { | |
// I am the image object that is being viewed. | |
$scope.image = null; | |
// Load the image data from the server. | |
loadRemoteData(); | |
// --- | |
// PRIVATE METHODS. | |
// --- | |
// I apply the remote data to the local view-model. | |
function applyRemoteData( image ) { | |
$scope.image = augmentImage( image ); | |
// Just because we have the image, it doesn't mean that remote image binary | |
// is actually available. But, we may have a data-uri version of it cached | |
// locally. In that case, let's consume the data-uri as the imageUrl, which | |
// will allow us to render the image immediately. | |
if ( dataUriCache.has( image.imageUrl ) ) { | |
// The data-uri cache has two modes of access: either you get the data- | |
// uri; or, you get the data-uri and the remote object is loaded in the | |
// background. We only want to use "replace" if we know that the remote | |
// image is available; otherwise, we run the risk of caching the wrong | |
// binary in the browser's cache. | |
if ( image.isFileAvailable ) { | |
// Get the data-uri and try to load the remote object in parallel. | |
// -- | |
// NOTE: Once the remote object is loaded, the data-uri cache is | |
// automatically flushed. | |
image.imageUrl = dataUriCache.replace( image.imageUrl ); | |
} else { | |
// Just get the data-uri - don't try to load the remote binary; this | |
// will allow the data-uri to stay in-memory for a bit longer. | |
image.imageUrl = dataUriCache.get( image.imageUrl ); | |
// Since we're using the data-uri in lieu of the remote image, we can | |
// flag the file as available. | |
image.isFileAvailable = true; | |
} | |
} | |
} | |
// I add the additional view-specific data to the image object. | |
function augmentImage( image ) { | |
var createdAt = new Date( image.createdAt ); | |
// Add a user-friendly data label for the created timestamp. | |
image.dateLabel = ( | |
createdAt.toDateString().replace( /^(Sun|Mon|Tue|Wen|Thr|Fri|Sat)/i, "$1," ) + | |
" at " + | |
createdAt.toTimeString().replace( / GMT.+/i, "" ) | |
); | |
return( image ); | |
} | |
// I load the selected image from the remote resource. | |
function loadRemoteData() { | |
// CAUTION: Inherited property - selectedImageID. | |
imageService.getImage( $scope.selectedImageID ) | |
.then( | |
function handleGetImagesResolve( response ) { | |
applyRemoteData( response ); | |
}, | |
function handleGetImagesReject( error ) { | |
console.warn( "Image data could not be loaded." ); | |
// CAUTION: Inherited method. | |
$scope.closeImage(); | |
} | |
) | |
; | |
} | |
} | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
app.controller( | |
"HomeController", | |
function( $scope, $rootScope, $q, imageService, globalUploader, dataUriCache, _ ) { | |
// I hold the list of images to render. | |
$scope.images = []; | |
// I am the ID of the currently-selected image. | |
$scope.selectedImageID = null; | |
// Pull the list of images from the remote repository. | |
loadRemoteData(); | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I close the current image detail. | |
$scope.closeImage = function() { | |
$scope.selectedImageID = null; | |
}; | |
// I delete the given image. | |
$scope.deleteImage = function( image ) { | |
$scope.images = _.without( $scope.images, image ); | |
// NOTE: Assuming no errors for this demo - not waiting for response. | |
imageService.deleteImage( image.id ); | |
}; | |
// I process the given files. These are expected to be mOxie file objects. I | |
// return a promise that will be done when all the files have been processed. | |
$scope.saveFiles = function( files ) { | |
var promises = _.map( files, saveFile ); | |
return( $q.all( promises ) ); | |
}; | |
// I open the detail view for the given image. | |
$scope.openImage = function( image ) { | |
$scope.selectedImageID = image.id; | |
}; | |
// --- | |
// PRIVATE METHODS. | |
// --- | |
// I apply the remote data to the local view-model. | |
function applyRemoteData( images ) { | |
$scope.images = augmentImages( images ); | |
} | |
// I augment the image for use in the local view-model. | |
function augmentImage( image ) { | |
return( image ); | |
} | |
// I aument the images for use in the local view-model. | |
function augmentImages( images ) { | |
return( _.each( images, augmentImage ) ); | |
} | |
// I load the images from the remote resource. | |
function loadRemoteData() { | |
imageService.getImages() | |
.then( | |
function handleGetImagesResolve( response ) { | |
applyRemoteData( response ); | |
}, | |
function handleGetImagesReject( error ) { | |
console.warn( "Error loading remote data." ); | |
} | |
) | |
; | |
} | |
// I save a file-record with the same name as the given file, then pass the file | |
// on to the application to be uploaded asynchronously. | |
function saveFile( file ) { | |
var image = null; | |
// We need to separate our promise chain a bit - the local uploader only cares | |
// about the image RECORDS that need to be "saved." The local uploader doesn't | |
// actually care about the global uploader, as this doesn't pertain to it's | |
// view-model / rendered state. | |
var savePromise = imageService.saveImage( file.name ) | |
.then( | |
function handleSaveResolve( response ) { | |
$scope.images.push( image = augmentImage( response.image ) ); | |
// NOTE: Pass response through chain so next promise can get at it. | |
return( response ); | |
}, | |
function handleSaveReject( error ) { | |
alert( "For some reason we couldn't save the file, " + file.name ); | |
// Pass-through the error (will ALSO be handled by the next | |
// error handler in the upload chain). | |
return( $q.reject( error ) ); | |
} | |
) | |
; | |
// Now that we have our "save promise", we can't hook into the global uploader | |
// workflow - sending the file to the uploader and then waiting for it to be | |
// done uploading. The global uploader doesn't know files from Adam; as such, | |
// we have to tell what the app to do when the global uploader has finished | |
// uploading a file. | |
savePromise | |
.then( | |
function handleSaveResolve( response ) { | |
return( | |
globalUploader.uploadFile( | |
file, | |
response.uploadSettings.url, | |
response.uploadSettings.data, | |
// Have the uploader extract the data-uri for the image. This | |
// will be made avialable in the .notify() handler. If this | |
// is omitted, only the resolve/reject handlers will be called. | |
true | |
) | |
); | |
} | |
) | |
.then( | |
function handleUploadResolve() { | |
// Once the file has been uploaded, we know that the remote binary | |
// can be reached at the known imageUrl; but, we need to let the | |
// server know that. | |
// -- | |
// NOTE: This could probably be replaced with some sort of t S3 / | |
// Simple Queue Service (SQS) integration. | |
imageService.finalizeImage( image.id ); | |
image.isFileAvailable = true; | |
}, | |
function handleUploadReject( error ) { | |
// CAUTION: The way this promise chain is configured, this error | |
// handler will also be invoked for "Save" errors as well. | |
alert( "For some reason we couldn't upload one of your files." ); | |
}, | |
function handleUploadNotify( dataUri ) { | |
// The notify event means that the uploader has extracted the image | |
// binary as a base64-encoded data-uri. We can use that in lieu of | |
// a remote image while the file is still being uploaded. By sticking | |
// this in the dataUriCache() service, we can also use it in the | |
// detail view, if the file still has yet to be uploaded. | |
image.imageUrl = dataUriCache.set( image.imageUrl, dataUri ); | |
// Since we're using the data-uri instead of the imageUrl, we can | |
// think of the image as being "available" for our intents and purposes. | |
image.isFileAvailable = true; | |
} | |
) | |
.finally( | |
function handleFinally() { | |
// Clear closed-over variables. | |
file = image = promise = null; | |
} | |
) | |
; | |
// Return the promise for the initial save - does not include the physical file | |
// upload to Amazon S3. | |
return( savePromise ); | |
} | |
} | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment