angular-localforage
Version:
Angular service & directive for https://github.com/mozilla/localForage (Offline storage, improved.)
547 lines (471 loc) • 18.4 kB
JavaScript
/**
* angular-localforage - Angular service & directive for https://github.com/mozilla/localForage (Offline storage, improved.)
* @version v1.3.8
* @link https://github.com/ocombe/angular-localForage
* @license MIT
* @author Olivier Combe <olivier.combe@gmail.com>
*/
(function(root, factory) {
'use strict';
var angular = (root && root.angular) || (window && window.angular);
if(typeof define === 'function' && define.amd) { // AMD
define(['localforage'], function(localforage) {
return factory(angular, localforage);
});
} else if(typeof exports === 'object' || typeof global === 'object') {
if(typeof module === 'undefined') {
global.module.exports = factory(angular, require('localforage')); // NW.js
} else {
module.exports = factory(angular, require('localforage')); // Node/Browserify
}
} else {
return factory(angular, root.localforage); // Browser
}
})(this, function(angular, localforage, undefined) {
'use strict';
var angularLocalForage = angular.module('LocalForageModule', ['ng']);
angularLocalForage.provider('$localForage', function() {
var lfInstances = {},
defaultConfig = {
name: 'lf'
},
// Send signals for each of the following actions ?
notify = {
setItem: false,
removeItem: false
},
watchers = {};
// Setter for notification config, itemSet & itemRemove should be booleans
this.setNotify = function(itemSet, itemRemove) {
notify = {
setItem: itemSet,
removeItem: itemRemove
};
};
this.config = function(config) {
if(!angular.isObject(config)) {
throw new Error('The config parameter should be an object');
}
angular.extend(defaultConfig, config);
};
this.$get = ['$rootScope', '$q', '$parse', function($rootScope, $q, $parse) {
var LocalForageInstance = function LocalForageInstance(params) {
if(angular.isDefined(params)) {
this._localforage = localforage.createInstance(params);
} else {
this._localforage = localforage;
localforage.config(defaultConfig);
}
};
LocalForageInstance.prototype.createInstance = function createInstance(config) {
if(angular.isObject(config)) { // create new instance
config = angular.extend({}, defaultConfig, config);
if(angular.isDefined(lfInstances[config.name])) {
throw new Error('A localForage instance with the name ' + config.name + ' is already defined.');
}
lfInstances[config.name] = new LocalForageInstance(config);
return lfInstances[config.name];
} else {
throw new Error('The parameter should be a config object.')
}
};
LocalForageInstance.prototype.instance = function instance(name) {
if(angular.isUndefined(name)) {
return lfInstances[defaultConfig.name];
} else if(angular.isString(name)) {
if(angular.isDefined(lfInstances[name])) {
return lfInstances[name];
} else {
throw new Error('No localForage instance of that name exists.')
}
} else {
throw new Error('The parameter should be a string.')
}
};
// Setter for the storage driver
LocalForageInstance.prototype.setDriver = function setDriver(driver) {
return this._localforage.setDriver(driver);
};
// Getter for the storage driver
LocalForageInstance.prototype.driver = function driver() {
return this._localforage.driver();
};
// Define a new driver for upstream consistency
LocalForageInstance.prototype.defineDriver = function defineDriver(driver) {
return this._localforage.defineDriver(driver);
}
// Directly adds a value to storage
LocalForageInstance.prototype.setItem = function setItem(key, value) {
// throw error on undefined key, we allow undefined value because... why not ?
if(angular.isUndefined(key)) {
throw new Error("You must define a key to set");
}
var self = this;
var args = arguments;
var localCopy;
if(angular.isArray(key)) {
if(!angular.isArray(value)) {
throw new Error('If you set an array of keys, the values should be an array too');
}
return $q.all(key.map(function (k, index) {
return self.setItem(k, value[index]);
}));
}
localCopy = stripMeta(value);
return self._localforage
.setItem(self.prefix() + key, localCopy)
.then(function success() {
if(notify.setItem) {
$rootScope.$broadcast('LocalForageModule.setItem', {
key: key,
newvalue: localCopy,
driver: self.driver()
});
}
return localCopy;
})
.catch(onError(args, self.setItem));
function stripMeta(value) {
var copy;
if (angular.isArray(value)) {
return value.map(stripMeta);
} else if (angular.isObject(value) && value.constructor === Object) {
copy = angular.extend({}, value);
angular.isDefined(copy.$promise) && delete copy.$promise;
angular.isDefined(copy.$$hashKey) && delete copy.$$hashKey;
return Object
.keys(copy)
.reduce(function stripEntries(acc, key) {
acc[key] = stripMeta(copy[key]);
return acc;
}, {});
}
return value;
}
};
// Directly get a value from storage
LocalForageInstance.prototype.getItem = function getItem(key, rejectOnNull) {
// throw error on undefined key
if(angular.isUndefined(key)) {
throw new Error("You must define a key to get");
}
var deferred = $q.defer(),
args = arguments,
self = this,
promise;
if(angular.isArray(key)) {
var res = [],
found = 0;
promise = self._localforage.iterate(function(value, k) {
var index = key.indexOf(self.prefix() + k);
if(index > -1) {
res[index] = value;
found++;
}
if(found === key.length) {
return res;
}
}).then(function() {
var shouldResolve = true;
for (var i = 0; i < key.length; i++) {
if (angular.isUndefined(res[i])) {
res[i] = null;
shouldResolve = false;
}
}
if (shouldResolve || !rejectOnNull) {
deferred.resolve(res);
} else {
deferred.reject(res);
}
});
} else {
promise = self._localforage.getItem(self.prefix() + key).then(function(item) {
if (rejectOnNull && item === null) {
deferred.reject(item);
} else {
deferred.resolve(item);
}
});
}
promise.then(null, function error(data) {
self.onError(data, args, self.getItem, deferred);
});
return deferred.promise;
};
// Iterate over all the values in storage
LocalForageInstance.prototype.iterate = function iterate(callback) {
// throw error on undefined key
if(angular.isUndefined(callback)) {
throw new Error("You must define a callback to iterate");
}
var deferred = $q.defer(),
args = arguments,
self = this;
self._localforage.iterate(callback).then(function success(item) {
deferred.resolve(item);
}, function error(data) {
self.onError(data, args, self.iterate, deferred);
});
return deferred.promise;
};
// Remove an item from storage
LocalForageInstance.prototype.removeItem = function removeItem(key) {
// throw error on undefined key
if(angular.isUndefined(key)) {
throw new Error("You must define a key to remove");
}
var self = this;
if(angular.isArray(key)) {
var promises = [];
angular.forEach(key, function(k, index) {
promises.push(self.removeItem(k));
});
return $q.all(promises);
} else {
var deferred = $q.defer(),
args = arguments;
self._localforage.removeItem(self.prefix() + key).then(function success() {
if(notify.removeItem) {
$rootScope.$broadcast('LocalForageModule.removeItem', {key: key, driver: self.driver()});
}
deferred.resolve();
}, function error(data) {
self.onError(data, args, self.removeItem, deferred);
});
return deferred.promise;
}
};
// Get an item and removes it from storage
LocalForageInstance.prototype.pull = function pull(key) {
var self = this;
var itemValue;
if(angular.isUndefined(key)) {
throw new Error("You must define a key to pull");
}
return self
.getItem(key)
.then(function (value) {
itemValue = value;
return self.removeItem(key);
})
.then(function () {
return itemValue;
});
};
// Remove all data for this app from storage
LocalForageInstance.prototype.clear = function clear() {
var deferred = $q.defer(),
args = arguments,
self = this;
self._localforage.clear().then(function success(keys) {
deferred.resolve();
}, function error(data) {
self.onError(data, args, self.clear, deferred);
});
return deferred.promise;
// return $q(function (resolve, reject) {
// self._localforage
// .clear()
// .then(function (keys) {
// resolve();
// })
// .catch(onError(arguments, self.clear));
// });
};
// Return the key for item at position n
LocalForageInstance.prototype.key = function key(n) {
// throw error on undefined n
if(angular.isUndefined(n)) {
throw new Error("You must define a position to get for the key function");
}
var deferred = $q.defer(),
args = arguments,
self = this;
self._localforage.key(n).then(function success(key) {
deferred.resolve(key);
}, function error(data) {
self.onError(data, args, self.key, deferred);
});
return deferred.promise;
};
var keys = function keys() {
var deferred = $q.defer(),
args = arguments,
self = this;
self._localforage.keys().then(function success(keyList) {
if(defaultConfig.oldPrefix && self.driver() === 'localStorageWrapper') {
var tempKeyList = [];
for(var i = 0, len = keyList.length; i < len; i++) {
tempKeyList.push(
keyList[i].substr(self.prefix().length, keyList[i].length)
);
}
keyList = tempKeyList;
}
deferred.resolve(keyList);
}, function error(data) {
self.onError(data, args, self.keys, deferred);
});
return deferred.promise;
};
// Return the list of keys stored for this application
LocalForageInstance.prototype.keys = keys;
// deprecated
LocalForageInstance.prototype.getKeys = keys;
// Returns the number of keys in this storage
LocalForageInstance.prototype.length = function() {
var deferred = $q.defer(),
args = arguments,
self = this;
self._localforage.length().then(function success(length) {
deferred.resolve(length);
}, function error(data) {
self.onError(data, args, length, deferred);
});
return deferred.promise;
};
/**
* Bind - let's you directly bind a LocalForage value to a $scope variable
* @param {Angular $scope} $scope - the current scope you want the variable available in
* @param {String/Object} opts - the key name of the variable you are binding OR an object with the key and custom options like default value or instance name
* Here are the available options you can set:
* * key: the key used in storage and in the scope (if scopeKey isn't defined)
* * defaultValue: the default value
* * name: name of the instance that should store the data
* * scopeKey: the key used in the scope
* @returns {*} - returns whatever the stored value is
*/
LocalForageInstance.prototype.bind = function bind($scope, opts) {
if(angular.isString(opts)) {
opts = {
key: opts
};
} else if(!angular.isObject(opts) || angular.isUndefined(opts.key)) {
throw new Error("You must define a key to bind");
}
var defaultOpts = {
defaultValue: '',
name: defaultConfig.name
};
// If no defined options we use defaults otherwise extend defaults
opts = angular.extend({}, defaultOpts, opts);
var self = lfInstances[opts.name];
if(angular.isUndefined(self)) {
throw new Error("You must use the name of an existing instance");
}
// Set the storeName key for the LocalForage entry
// use user defined in specified
var scopeKey = opts.scopeKey || opts.key,
model = $parse(scopeKey);
return self
.getItem(opts.key, true)
.then(function(item) {
model.assign($scope, item);
return item;
})
.catch(function () {
model.assign($scope, opts.defaultValue);
return self.setItem(opts.key, opts.defaultValue);
})
.then(function (item) {
if(angular.isDefined(watchers[opts.key])) {
watchers[opts.key]();
}
watchers[opts.key] = $scope.$watch(scopeKey, function(val) {
if(angular.isDefined(val)) {
self.setItem(opts.key, val);
}
}, true);
return item;
});
};
/**
* Unbind - let's you unbind a variable from localForage while removing the value from both
* the localForage and the local variable and sets it to null
* @param {String/Object} opts - the key name of the variable you are unbinding OR an object with the key and custom options like default value or instance name
* Here are the available options you can set:
* * key: the key used in storage and in the scope (if scopeKey isn't defined)
* * name: name of the instance that should store the data
* * scopeKey: the key used in the scope
*/
LocalForageInstance.prototype.unbind = function unbind($scope, opts) {
if(angular.isString(opts)) {
opts = {
key: opts
}
} else if(!angular.isObject(opts) || angular.isUndefined(opts.key)) {
throw new Error("You must define a key to unbind");
}
var defaultOpts = {
scopeKey: opts.key,
name: defaultConfig.name
};
// If no defined options we use defaults otherwise extend defaults
opts = angular.extend({}, defaultOpts, opts);
var self = lfInstances[opts.name];
if(angular.isUndefined(self)) {
throw new Error("You must use the name of an existing instance");
}
$parse(opts.scopeKey).assign($scope, null);
if(angular.isDefined(watchers[opts.key])) {
watchers[opts.key](); // unwatch
delete watchers[opts.key];
}
return self.removeItem(opts.key);
};
LocalForageInstance.prototype.prefix = function() {
return this.driver() === 'localStorageWrapper' && defaultConfig.oldPrefix ? this._localforage.config().name + '.' : '';
};
// Handling errors
LocalForageInstance.prototype.onError = function(err, args, fct, deferred) {
// test for private browsing errors in Firefox & Safari
if(((angular.isObject(err) && err.name ? err.name === 'InvalidStateError' : (angular.isString(err) && err === 'InvalidStateError')) && this.driver() === 'asyncStorage')
|| (angular.isObject(err) && err.code && err.code === 5)) {
var self = this;
self.setDriver('localStorageWrapper').then(function() {
fct.apply(self, args).then(function(item) {
deferred.resolve(item);
}, function(data) {
deferred.reject(data);
});
}, function() {
deferred.reject(err);
});
} else {
deferred.reject(err);
}
};
function onError(args, fct) {
return function(err) {
if(((angular.isObject(err) && err.name ? err.name === 'InvalidStateError' : (angular.isString(err) && err === 'InvalidStateError')) && this.driver() === 'asyncStorage')
|| (angular.isObject(err) && err.code && err.code === 5)) {
var self = this;
return self
.setDriver('localStorageWrapper')
.then(function () {
return fct.apply(self, args);
});
}
return $q.reject(err);
};
}
lfInstances[defaultConfig.name] = new LocalForageInstance();
return lfInstances[defaultConfig.name];
}]
});
angularLocalForage.directive('localForage', ['$localForage', function($localForage) {
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
var opts = $scope.$eval($attrs.localForage);
if(angular.isObject(opts) && angular.isDefined(opts.key)) {
$localForage.bind($scope, opts);
} else {
$localForage.bind($scope, $attrs.localForage);
}
}
}
}]);
return angularLocalForage.name;
});