Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created February 23, 2015 13:39
Consuming Plupload Data URIs In An AngularJS Application
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 );
}
}
);
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();
}
)
;
}
}
);
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