UNPKG

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
/** * 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; });