Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created July 10, 2015 11:13
Show Gist options
  • Save bennadel/cba13cbb2fcd03b7f788 to your computer and use it in GitHub Desktop.
Save bennadel/cba13cbb2fcd03b7f788 to your computer and use it in GitHub Desktop.
Encapsulating LocalStorage Access In AngularJS
<!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