Created
July 10, 2015 11:13
-
-
Save bennadel/cba13cbb2fcd03b7f788 to your computer and use it in GitHub Desktop.
Encapsulating LocalStorage Access In AngularJS
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
<!doctype html> | |
<html ng-app="Demo"> | |
<head> | |
<meta charset="utf-8" /> | |
<title> | |
Encapsulating LocalStorage Access In AngularJS | |
</title> | |
<link rel="stylesheet" type="text/css" href="./demo.css"></link> | |
</head> | |
<body ng-controller="AppController as vm"> | |
<h1> | |
Encapsulating LocalStorage Access In AngularJS | |
</h1> | |
<h2> | |
You have {{ vm.friends.length }} friends! | |
</h2> | |
<!-- | |
This list of friends will be persisted in localStorage and will be seen | |
across page-refresh actions. | |
--> | |
<ul> | |
<li ng-repeat="friend in vm.friends track by friend.id"> | |
{{ friend.name }} ( <a ng-click="vm.removeFriend( friend )">delete</a> ) | |
</li> | |
</ul> | |
<form ng-submit="vm.processForm()"> | |
<input type="text" ng-model="vm.form.name" size="30" /> | |
<input type="submit" value="Add Friend" /> | |
</form> | |
<p> | |
<a ng-click="vm.logout()">Log out</a> ( <em>will kill local storage</em> ). | |
</p> | |
<!-- Load scripts. --> | |
<script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script> | |
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.16.min.js"></script> | |
<script type="text/javascript"> | |
// Create an application module for our demo. | |
angular.module( "Demo", [] ); | |
// --------------------------------------------------------------------------- // | |
// --------------------------------------------------------------------------- // | |
// I control the root of the application. | |
angular.module( "Demo" ).controller( | |
"AppController", | |
function provideAppController( $scope, $window, friendService, storage ) { | |
var vm = this; | |
// I hold the collection of friends. | |
vm.friends = []; | |
// I hold the form data for use with ngModel. | |
vm.form = { | |
name: "" | |
}; | |
loadRemoteData(); | |
// Expose the public API. | |
vm.logout = logout; | |
vm.processForm = processForm; | |
vm.removeFriend = removeFriend; | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// When the user wants to log out of the application, we don't want | |
// the in-memory cache to persist to disk. As such, we need to explicitly | |
// disable the persistence before we bounce them out of the app. | |
function logout() { | |
storage.disablePersist(); | |
$window.location.href = "./logout.htm"; | |
} | |
// I process the new-friend form in an attempt to add a new friend. | |
function processForm() { | |
if ( ! vm.form.name ) { | |
return; | |
} | |
friendService | |
.addFriend( vm.form.name ) | |
.then( loadRemoteData ) | |
; | |
vm.form.name = ""; | |
} | |
// I remove the given friend from the collection. | |
function removeFriend( friend ) { | |
// NOTE: Normally, I would optimistically remove the friend from the | |
// local collection; however, since I know that all of the data in | |
// this demo is client-side, I'm just going to reload the data as it | |
// will be loaded faster than the user can perceive. | |
friendService | |
.deleteFriend( friend.id ) | |
.then( loadRemoteData ) | |
; | |
} | |
// --- | |
// PRIVATE METHODS. | |
// --- | |
// I apply the remote data to the view-model. | |
function applyRemoteData( friends ) { | |
vm.friends = friends; | |
} | |
// I load the remote data for use in the view-model. | |
function loadRemoteData() { | |
friendService | |
.getFriends() | |
.then( applyRemoteData ) | |
; | |
} | |
} | |
); | |
// --------------------------------------------------------------------------- // | |
// --------------------------------------------------------------------------- // | |
// I provide a repository for friends. I use the storage service to persist data | |
// across page reloads. | |
angular.module( "Demo" ).factory( | |
"friendService", | |
function provideFriendService( $q, storage ) { | |
// Attempt to pull the friends out of storage. | |
// -- | |
// NOTE: Using .extractItem() instead of .getItem() since we don't | |
// really need the friends item to remain in storage once we have pulled | |
// it into this service (which, for this demo, does it's own caching). | |
// We'll be repopulating the cache in the onBeforePersist() event below, | |
// anyway; so, this will cut down on memory usage. | |
var friends = ( storage.extractItem( "friends" ) || [] ); | |
// Rather than trying to keep the service-data and the cache-data in | |
// constant sync, let's just hook into the persist event of the storage | |
// which will give us an opportunity to do just-in-time synchronization. | |
storage.onBeforePersist( | |
function handlePersist() { | |
storage.setItem( "friends", friends ); | |
} | |
); | |
// Return the public API. | |
return({ | |
addFriend: addFriend, | |
deleteFriend: deleteFriend, | |
getFriends: getFriends | |
}); | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I add a new friend with the given name. Returns a promise that resolves | |
// with the newly generated ID. | |
function addFriend( name ) { | |
var id = ( new Date() ).getTime(); | |
friends.push({ | |
id: id, | |
name: name | |
}); | |
return( $q.when( id ) ); | |
} | |
// I remove the friend with the given id. Returns a promise which resolves | |
// whether or not the friend actually existed. | |
function deleteFriend( id ) { | |
for ( var i = 0, length = friends.length ; i < length ; i++ ) { | |
if ( friends[ i ].id === id ) { | |
friends.splice( i, 1 ); | |
break; | |
} | |
} | |
return( $q.when() ); | |
} | |
// I get the entire collection of friends. Returns a promise. | |
function getFriends() { | |
// NOTE: We are using .copy() so that the internal cache can't be | |
// mutated through direct object references. | |
return( $q.when( angular.copy( friends ) ) ); | |
} | |
} | |
); | |
// --------------------------------------------------------------------------- // | |
// --------------------------------------------------------------------------- // | |
// This is just here to make sure that the storage component is loaded when the | |
// app is bootstrapped. This will cause the localStorage I/O overhead to be front- | |
// loaded in the app rather than caused by a particular user interaction (which | |
// the user is more likely to notice). | |
angular.module( "Demo" ).run( | |
function loadStorage( storage ) { | |
// ... just sit back and bask in the glory of dependency-injection. | |
} | |
); | |
// I am the localStorage key that will be used to persist data for this demo. | |
angular.module( "Demo" ).value( "storageKey", "angularjs_demo" ); | |
// I provide a storage API that uses an in-memory data cache that is persisted | |
// to the localStorage at the limits of the application life-cycle. | |
angular.module( "Demo" ).factory( | |
"storage", | |
function provideStorage( $exceptionHandler, $window, storageKey ) { | |
// Try to load the initial payload from localStorage. | |
var items = loadData(); | |
// I maintain a collection of callbacks that want to hook into the | |
// unload event of the in-memory cache. This will give the calling | |
// context a chance to update their relevant storage items before | |
// the data is persisted to localStorage. | |
var persistHooks = []; | |
// I determine if the cache should be persisted to localStorage when the | |
// application is unloaded. | |
var persistEnabled = true; | |
// During the application lifetime, we're going to be using in-memory | |
// data access (since localStorage I/O is relatively expensive and | |
// requires data to be serialized - two things we don't want during the | |
// user to "feel"). However, when the application unloads, we want to try | |
// to persist the in-memory cache to the localStorage. | |
$window.addEventListener( "beforeunload", persistData ); | |
// Return the public API. | |
return({ | |
clear: clear, | |
disablePersist: disablePersist, | |
enablePersist: enablePersist, | |
extractItem: extractItem, | |
getItem: getItem, | |
onBeforePersist: onBeforePersist, | |
removeItem: removeItem, | |
setItem: setItem | |
}); | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I clear the current item cache. | |
function clear() { | |
items = {}; | |
} | |
// I disable the persisting of the cache to localStorage on unload. | |
function disablePersist() { | |
persistEnabled = false; | |
} | |
// I enable the persisting of the cache to localStorage on unload. | |
function enablePersist() { | |
persistEnabled = true; | |
} | |
// I remove the given key from the cache and return the value that was | |
// cached at that key; returns null if the key didn't exist. | |
function extractItem( key ) { | |
var value = getItem( key ); | |
removeItem( key ); | |
return( value ); | |
} | |
// I return the item at the given key; returns null if not available. | |
function getItem( key ) { | |
key = normalizeKey( key ); | |
// NOTE: We are using .copy() so that the internal cache can't be | |
// mutated through direct object references. | |
return( ( key in items ) ? angular.copy( items[ key ] ) : null ); | |
} | |
// I add the given operator to persist hooks that will be invoked prior | |
// to unload-based persistence. | |
function onBeforePersist( operator ) { | |
persistHooks.push( operator ); | |
} | |
// I remove the given key from the cache. | |
function removeItem( key ) { | |
key = normalizeKey( key ); | |
delete( items[ key ] ); | |
} | |
// I store the item at the given key. | |
function setItem( key, value ) { | |
key = normalizeKey( key ); | |
// NOTE: We are using .copy() so that the internal cache can't be | |
// mutated through direct object references. | |
items[ key ] = angular.copy( value ); | |
} | |
// --- | |
// PRIVATE METHODS. | |
// --- | |
// I attempt to load the cache from the localStorage interface. Once the | |
// data is loaded, it is deleted from localStorage. | |
function loadData() { | |
// There's a chance that the localStorage isn't available, even in | |
// modern browsers (looking at you, Safari, running in Private mode). | |
try { | |
if ( storageKey in $window.localStorage ) { | |
var data = $window.localStorage.getItem( storageKey ); | |
$window.localStorage.removeItem( storageKey ); | |
// NOTE: Using .extend() here as a safe-guard to ensure that | |
// the value we return is actually a hash, even if the data | |
// is corrupted. | |
return( angular.extend( {}, angular.fromJson( data ) ) ); | |
} | |
} catch ( localStorageError ) { | |
$exceptionHandler( localStorageError ); | |
} | |
// If we made it this far, something went wrong. | |
return( {} ); | |
} | |
// I normalize the given cache key so that we never collide with any | |
// native object keys when looking up items. | |
function normalizeKey( key ) { | |
return( "storage_" + key ); | |
} | |
// I attempt to persist the cache to the localStorage. | |
function persistData() { | |
// Before we persist the data, invoke all of the before-persist hook | |
// operators so that consuming services have one last chance to | |
// synchronize their local data with the storage data. | |
for ( var i = 0, length = persistHooks.length ; i < length ; i++ ) { | |
try { | |
persistHooks[ i ](); | |
} catch ( persistHookError ) { | |
$exceptionHandler( persistHookError ); | |
} | |
} | |
// If persistence is disabled, skip the localStorage access. | |
if ( ! persistEnabled ) { | |
return; | |
} | |
// There's a chance that localStorage isn't available, even in modern | |
// browsers. And, even if it does exist, we may be attempting to store | |
// more data that we can based on per-domain quotas. | |
try { | |
$window.localStorage.setItem( storageKey, angular.toJson( items ) ); | |
} catch ( localStorageError ) { | |
$exceptionHandler( localStorageError ); | |
} | |
} | |
} | |
); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment