angular-localforage-setitems
Version:
Angular service & directive for https://github.com/mozilla/localForage (Offline storage, improved.) with added performant setItems function
745 lines (638 loc) • 27 kB
JavaScript
/**
* Jack in the Box inc.
* We added a more performant setItems() function into angular-localForage
*
* angular-localforage-setitems - Angular service & directive for https://github.com/mozilla/localForage (Offline storage, improved.) with added performant setItems function
* @version v1.0.0
* @link https://github.com/jackintheboxxd/angular-localForage-setItems
* @license MIT
* @author Christopher Soto <christopher.soto@jackinthebox.com>
*
* FORKED FROM:
* @link https://github.com/ocombe/angular-localForage
* @license MIT
* @author Olivier Combe <olivier.combe@gmail.com>
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('localforage')) :
typeof define === 'function' && define.amd ? define(['exports', 'localforage'], factory) :
(factory((global.localforageSetItems = global.localforageSetItems || {}),global.localforage));
}(this, function (exports,localforage) { 'use strict';
localforage = 'default' in localforage ? localforage['default'] : localforage;
function getSerializerPromise(localForageInstance) {
if (getSerializerPromise.result) {
return getSerializerPromise.result;
}
if (!localForageInstance || typeof localForageInstance.getSerializer !== 'function') {
Promise.reject(new Error('localforage.getSerializer() was not available! ' + 'localforage v1.4+ is required!'));
}
getSerializerPromise.result = localForageInstance.getSerializer();
return getSerializerPromise.result;
}
function executeCallback(promise, callback) {
if (callback) {
promise.then(function (result) {
callback(null, result);
}, function (error) {
callback(error);
});
}
}
function forEachItem(items, keyFn, valueFn, loopFn) {
function ensurePropGetterMethod(propFn, defaultPropName) {
var propName = propFn || defaultPropName;
if ((!propFn || typeof propFn !== 'function') && typeof propName === 'string') {
propFn = function propFn(item) {
return item[propName];
};
}
return propFn;
}
var result = [];
// http://stackoverflow.com/questions/4775722/check-if-object-is-array
if (Object.prototype.toString.call(items) === '[object Array]') {
keyFn = ensurePropGetterMethod(keyFn, 'key');
valueFn = ensurePropGetterMethod(valueFn, 'value');
for (var i = 0, len = items.length; i < len; i++) {
var item = items[i];
result.push(loopFn(keyFn(item), valueFn(item)));
}
} else {
for (var prop in items) {
if (items.hasOwnProperty(prop)) {
result.push(loopFn(prop, items[prop]));
}
}
}
return result;
}
function setItemsIndexedDB(items, keyFn, valueFn, callback) {
var localforageInstance = this;
var promise = localforageInstance.ready().then(function () {
return new Promise(function (resolve, reject) {
// Inspired from @lu4 PR mozilla/localForage#318
var dbInfo = localforageInstance._dbInfo;
var transaction = dbInfo.db.transaction(dbInfo.storeName, 'readwrite');
var store = transaction.objectStore(dbInfo.storeName);
var lastError;
transaction.oncomplete = function() {
resolve(items);
};
transaction.onabort = transaction.onerror = function(event) {
reject(lastError || event.target);
};
function requestOnError(evt) {
var request = evt.target || this;
lastError = request.error || request.transaction.error;
reject(lastError);
}
forEachItem(items, keyFn, valueFn, function(key, value) {
// The reason we don't _save_ null is because IE 10 does
// not support saving the `null` type in IndexedDB. How
// ironic, given the bug below!
// See: https://github.com/mozilla/localForage/issues/161
if (value === null) {
value = undefined;
}
var request = store.put(value, key);
request.onerror = requestOnError;
});
});
});
executeCallback(promise, callback);
return promise;
}
function setItemsWebsql(items, keyFn, valueFn, callback) {
var localforageInstance = this;
var promise = new Promise(function (resolve, reject) {
localforageInstance.ready().then(function () {
return getSerializerPromise(localforageInstance);
}).then(function (serializer) {
// Inspired from @lu4 PR mozilla/localForage#318
var dbInfo = localforageInstance._dbInfo;
dbInfo.db.transaction(function (t) {
var query = 'INSERT OR REPLACE INTO ' + dbInfo.storeName + ' (key, value) VALUES (?, ?)';
var itemPromises = forEachItem(items, keyFn, valueFn, function (key, value) {
return new Promise(function (resolve, reject) {
serializer.serialize(value, function (value, error) {
if (error) {
reject(error);
} else {
t.executeSql(query, [key, value], function () {
resolve();
}, function (t, error) {
reject(error);
});
}
});
});
});
Promise.all(itemPromises).then(function () {
resolve(items);
}, reject);
}, function (sqlError) {
reject(sqlError);
} /*, function() {
if (resolving) {
resolve(items);
}
}*/);
}).catch(reject);
});
executeCallback(promise, callback);
return promise;
}
function setItemsGeneric(items, keyFn, valueFn, callback) {
var localforageInstance = this;
var itemPromises = forEachItem(items, keyFn, valueFn, function (key, value) {
return localforageInstance.setItem(key, value);
});
var promise = Promise.all(itemPromises);
executeCallback(promise, callback);
return promise;
}
function localforageSetItems(items, keyFn, valueFn, callback) {
var localforageInstance = this;
var currentDriver = localforageInstance.driver();
if (currentDriver === localforageInstance.INDEXEDDB) {
return setItemsIndexedDB.call(localforageInstance, items, keyFn, valueFn, callback);
} else if (currentDriver === localforageInstance.WEBSQL) {
return setItemsWebsql.call(localforageInstance, items, keyFn, valueFn, callback);
} else {
return setItemsGeneric.call(localforageInstance, items, keyFn, valueFn, callback);
}
}
function extendPrototype(localforage) {
var localforagePrototype = Object.getPrototypeOf(localforage);
if (localforagePrototype) {
localforagePrototype.setItems = localforageSetItems;
localforagePrototype.setItems.indexedDB = function () {
return setItemsIndexedDB.apply(this, arguments);
};
localforagePrototype.setItems.websql = function () {
return setItemsWebsql.apply(this, arguments);
};
localforagePrototype.setItems.generic = function () {
return setItemsGeneric.apply(this, arguments);
};
}
}
var extendPrototypeResult = extendPrototype(localforage);
exports.setItemsGeneric = setItemsGeneric;
exports.localforageSetItems = localforageSetItems;
exports.extendPrototype = extendPrototype;
exports.extendPrototypeResult = extendPrototypeResult;
}));
(function(root, factory) {
'use strict';
if(typeof define === 'function' && define.amd) { // AMD
define(['localforage'], function(localforage) {
factory(root.angular, localforage);
});
} else if(typeof exports === 'object' || typeof global === 'object') {
var angular = root.angular || (window && window.angular);
module.exports = factory(angular, require('localforage')); // Node/Browserify
} else {
return factory(root.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();
};
// 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;
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');
}
var promises = [];
angular.forEach(key, function(k, index) {
promises.push(self.setItem(k, value[index]))
});
return $q.all(promises);
} else {
var deferred = $q.defer(),
args = arguments,
localCopy = typeof Blob !== 'undefined' && typeof ArrayBuffer !== 'undefined' && (value instanceof Blob || value instanceof ArrayBuffer) ? value : angular.copy(value);
//avoid $promises attributes from value objects, if present.
if(angular.isObject(localCopy) && angular.isDefined(localCopy.$promise)) {
delete localCopy.$promise; //delete attribut from object structure.
}
self._localforage.setItem(self.prefix() + key, localCopy).then(function success() {
if(notify.setItem) {
$rootScope.$broadcast('LocalForageModule.setItem', {
key: key,
newvalue: localCopy,
driver: self.driver()
});
}
deferred.resolve(localCopy);
}, function error(data) {
self.onError(data, args, self.setItem, deferred);
});
return deferred.promise;
}
};
function isEmpty(obj) {
for(var prop in obj) {
if(obj.hasOwnProperty(prop)) {
return false;
}
}
return true && angular.toJson(obj) === angular.toJson({});
}
LocalForageInstance.prototype.setItems = function setItems(items) {
// throw error on undefined key, we allow undefined value because... why not ?
if(angular.isUndefined(items)) {
throw new Error("You must define a key to set");
}
var self = this;
var deferred = $q.defer();
var args = arguments;
var localCopy = angular.copy(items);
console.log(self);
if(angular.isArray(items) || angular.isObject(items)) {
if(isEmpty(items)) {
// if its empty lets not bother with processing it with setItems
deferred.resolve('empty');
} else {
// avoid $promises attributes from value objects, if present.
if(angular.isObject(localCopy) && angular.isDefined(localCopy.$promise)) {
delete localCopy.$promise; // delete attribute
}
self._localforage.setItems(localCopy).then(function success() {
if(notify.setItems) {
$rootScope.$broadcast('LocalForageModule.setItems', {
newValue: localCopy,
driver: self.driver()
});
}
deferred.resolve(localCopy);
}, function error(data) {
self.onError(data, args, self.setItems, deferred);
});
}
return deferred.promise;
}
}
// Directly get a value from storage
LocalForageInstance.prototype.getItem = function getItem(key) {
// 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() {
for (var i = 0; i < key.length; i++) {
if (angular.isUndefined(res[i])) {
res[i] = null;
}
}
deferred.resolve(res);
});
} else {
promise = self._localforage.getItem(self.prefix() + key).then(function(item) {
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) {
// throw error on undefined key
if(angular.isUndefined(key)) {
throw new Error("You must define a key to pull");
}
var self = this,
deferred = $q.defer(),
onError = function error(err) {
deferred.reject(err);
};
self.getItem(key).then(function success(value) {
self.removeItem(key).then(function success() {
deferred.resolve(value);
}, onError);
}, onError);
return deferred.promise;
};
// 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 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).then(function(item) {
if (item !== null) { // If it does exist assign it to the $scope value
model.assign($scope, item);
} else if(!angular.isUndefined(opts.defaultValue)) { // If a value doesn't already exist store it as is
model.assign($scope, opts.defaultValue);
self.setItem(opts.key, opts.defaultValue);
}
// Register a listener for changes on the $scope value
// to update the localForage value
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);
}
};
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;
});