From 1f7228584b3bcd042c54de6b32fdba660f87b7c3 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 27 Jan 2015 11:02:21 -0500 Subject: [PATCH 1/6] Implements a new feature that will get all the storage keys that the system knows of. --- .gitignore | 3 +- .jshintrc | 24 +++ src/angularLocalStorage.js | 427 ++++++++++++++++++++----------------- 3 files changed, 252 insertions(+), 202 deletions(-) create mode 100644 .jshintrc diff --git a/.gitignore b/.gitignore index 1c3e58b..130339d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .DS_Store -*.log \ No newline at end of file +*.log +.idea \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..7070643 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,24 @@ +{ + "node": true, + "browser": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 2, + "latedef": true, + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": true, + "strict": true, + "trailing": true, + "smarttabs": true, + "globals": { + "angular": false + } +} \ No newline at end of file diff --git a/src/angularLocalStorage.js b/src/angularLocalStorage.js index 6a52774..6c903cc 100644 --- a/src/angularLocalStorage.js +++ b/src/angularLocalStorage.js @@ -4,206 +4,231 @@ */ (function (window, angular, undefined) { - 'use strict'; - - angular.module('angularLocalStorage', ['ngCookies']).factory('storage', ['$parse', '$cookieStore', '$window', '$log', function ($parse, $cookieStore, $window, $log) { - /** - * Global Vars - */ - var storage = (typeof $window.localStorage === 'undefined') ? undefined : $window.localStorage; - var supported = !(typeof storage === 'undefined'); - var watchers = {}; - - if (supported) { - // When Safari (OS X or iOS) is in private browsing mode it appears as though localStorage - // is available, but trying to call .setItem throws an exception below: - // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to add something to storage that exceeded the quota." - var testKey = '__' + Math.round(Math.random() * 1e7); - - try { - localStorage.setItem(testKey, testKey); - localStorage.removeItem(testKey); - } - catch (err) { - supported = false; - } - } - - var privateMethods = { - /** - * Pass any type of a string from the localStorage to be parsed so it returns a usable version (like an Object) - * @param res - a string that will be parsed for type - * @returns {*} - whatever the real type of stored value was - */ - parseValue: function (res) { - var val; - try { - val = angular.fromJson(res); - if (typeof val === 'undefined') { - val = res; - } - if (val === 'true') { - val = true; - } - if (val === 'false') { - val = false; - } - if ($window.parseFloat(val) === val && !angular.isObject(val)) { - val = $window.parseFloat(val); - } - } catch (e) { - val = res; - } - return val; - }, - - getWatcherId: function(scope, key) { - return scope.$id + key; - } - }; - - var publicMethods = { - /** - * Set - let's you set a new localStorage key pair set - * @param key - a string that will be used as the accessor for the pair - * @param value - the value of the localStorage item - * @returns {*} - will return whatever it is you've stored in the local storage - */ - set: function (key, value) { - if (!supported) { - try { - $cookieStore.put(key, value); - return value; - } catch(e) { - $log.log('Local Storage not supported, make sure you have angular-cookies enabled.'); - } - } - var saver = angular.toJson(value); - storage.setItem(key, saver); - return privateMethods.parseValue(saver); - }, - - /** - * Get - let's you get the value of any pair you've stored - * @param key - the string that you set as accessor for the pair - * @returns {*} - Object,String,Float,Boolean depending on what you stored - */ - get: function (key) { - if (!supported) { - try { - return $cookieStore.get(key); - } catch (e) { - return null; - } - } - var item = storage.getItem(key); - return privateMethods.parseValue(item); - }, - - /** - * Remove - let's you nuke a value from localStorage - * @param key - the accessor value - * @returns {boolean} - if everything went as planned - */ - remove: function (key) { - if (!supported) { - try { - $cookieStore.remove(key); - return true; - } catch (e) { - return false; - } - } - storage.removeItem(key); - return true; - }, - - /** - * Bind - let's you directly bind a localStorage value to a $scope variable - * @param {Angular $scope} $scope - the current scope you want the variable available in - * @param {String} key - the name of the variable you are binding - * @param {Object|String} opts - (optional) custom options like default value or unique store name - * Here are the available options you can set: - * * defaultValue: the default value - * * storeName: add a custom store key value instead of using the scope variable name - * @returns {*} - returns whatever the stored value is - */ - bind: function ($scope, key, opts) { - var watcherId = privateMethods.getWatcherId($scope, key); - - var defaultOpts = { - defaultValue: '', - storeName: '' - }; - // Backwards compatibility with old defaultValue string - if (angular.isString(opts)) { - opts = angular.extend({},defaultOpts,{defaultValue:opts}); - } else { - // If no defined options we use defaults otherwise extend defaults - opts = (angular.isUndefined(opts)) ? defaultOpts : angular.extend(defaultOpts,opts); - } - - // Set the storeName key for the localStorage entry - // use user defined in specified - var storeName = opts.storeName || key; - var scopeVal = $scope.$eval(key); - - // If a value doesn't already exist store it as is - if (publicMethods.get(storeName) === null && typeof scopeVal === 'undefined') { - publicMethods.set(storeName, opts.defaultValue); - } - - // assign it to the $scope value - if(typeof scopeVal === 'undefined') { - $parse(key).assign($scope, publicMethods.get(storeName)); - } - - // Register a listener for changes on the $scope value - // to update the localStorage value - watchers[watcherId] = $scope.$watch(key, function (val) { - if (angular.isDefined(val)) { - publicMethods.set(storeName, val); - } - }, true); - - return publicMethods.get(storeName); - }, - /** - * Unbind - let's you unbind a variable from localStorage while removing the value from both - * the localStorage and the local variable and sets it to null - * @param $scope - the scope the variable was initially set in - * @param key - the name of the variable you are unbinding - * @param storeName - (optional) if you used a custom storeName you will have to specify it here as well - */ - unbind: function($scope,key,storeName) { - var watcherId = privateMethods.getWatcherId($scope, key); - - storeName = storeName || key; - $parse(key).assign($scope, null); - publicMethods.remove(storeName); - - // Trying to unbind a watcher if it really was here - // Maybe it should do nothing at all in this case? E.g. you can't unbind what you haven't bound yet. - if(watchers[watcherId]) { - watchers[watcherId](); - delete watchers[watcherId]; - } - }, - /** - * Clear All - let's you clear out ALL localStorage variables, use this carefully! - */ - clearAll: function() { - storage.clear(); - }, - /** - * Check if cookie fallback is active right now - */ - isCookieFallbackActive: function() { - return !supported; - } - }; - - return publicMethods; - }]); + 'use strict'; + + angular.module('angularLocalStorage', ['ngCookies']).factory('storage', ['$parse', '$cookieStore', '$window', '$log', function ($parse, $cookieStore, $window, $log) { + /** + * Global Vars + */ + var storage = (typeof $window.localStorage === 'undefined') ? undefined : $window.localStorage; + var supported = (typeof storage !== 'undefined'); + var watchers = {}; + + if (supported) { + // When Safari (OS X or iOS) is in private browsing mode it appears as though localStorage + // is available, but trying to call .setItem throws an exception below: + // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to add something to storage that exceeded the quota." + var testKey = '__' + Math.round(Math.random() * 1e7); + + try { + localStorage.setItem(testKey, testKey); + localStorage.removeItem(testKey); + } + catch (err) { + supported = false; + } + } + + var privateMethods = { + /** + * Pass any type of a string from the localStorage to be parsed so it returns a usable version (like an Object) + * @param res - a string that will be parsed for type + * @returns {*} - whatever the real type of stored value was + */ + parseValue: function (res) { + var val; + try { + val = angular.fromJson(res); + if (typeof val === 'undefined') { + val = res; + } + if (val === 'true') { + val = true; + } + if (val === 'false') { + val = false; + } + if ($window.parseFloat(val) === val && !angular.isObject(val)) { + val = $window.parseFloat(val); + } + } catch (e) { + val = res; + } + return val; + }, + + getWatcherId: function (scope, key) { + return scope.$id + key; + } + }; + + var publicMethods = { + /** + * Set - let's you set a new localStorage key pair set + * @param key - a string that will be used as the accessor for the pair + * @param value - the value of the localStorage item + * @returns {*} - will return whatever it is you've stored in the local storage + */ + set: function (key, value) { + if (!supported) { + try { + $cookieStore.put(key, value); + return value; + } catch (e) { + $log.log('Local Storage not supported, make sure you have angular-cookies enabled.'); + } + } + var saver = angular.toJson(value); + storage.setItem(key, saver); + return privateMethods.parseValue(saver); + }, + + /** + * Get - let's you get the value of any pair you've stored + * @param key - the string that you set as accessor for the pair + * @returns {*} - Object,String,Float,Boolean depending on what you stored + */ + get: function (key) { + if (!supported) { + try { + return $cookieStore.get(key); + } catch (e) { + return null; + } + } + var item = storage.getItem(key); + return privateMethods.parseValue(item); + }, + + /** + * Remove - let's you nuke a value from localStorage + * @param key - the accessor value + * @returns {boolean} - if everything went as planned + */ + remove: function (key) { + if (!supported) { + try { + $cookieStore.remove(key); + return true; + } catch (e) { + return false; + } + } + storage.removeItem(key); + return true; + }, + + /** + * Bind - let's you directly bind a localStorage value to a $scope variable + * @param {Angular $scope} $scope - the current scope you want the variable available in + * @param {String} key - the name of the variable you are binding + * @param {Object|String} opts - (optional) custom options like default value or unique store name + * Here are the available options you can set: + * * defaultValue: the default value + * * storeName: add a custom store key value instead of using the scope variable name + * @returns {*} - returns whatever the stored value is + */ + bind: function ($scope, key, opts) { + var watcherId = privateMethods.getWatcherId($scope, key); + + var defaultOpts = { + defaultValue: '', + storeName: '' + }; + // Backwards compatibility with old defaultValue string + if (angular.isString(opts)) { + opts = angular.extend({}, defaultOpts, {defaultValue: opts}); + } else { + // If no defined options we use defaults otherwise extend defaults + opts = (angular.isUndefined(opts)) ? defaultOpts : angular.extend(defaultOpts, opts); + } + + // Set the storeName key for the localStorage entry + // use user defined in specified + var storeName = opts.storeName || key; + var scopeVal = $scope.$eval(key); + + // If a value doesn't already exist store it as is + if (publicMethods.get(storeName) === null && typeof scopeVal === 'undefined') { + publicMethods.set(storeName, opts.defaultValue); + } + + // assign it to the $scope value + if (typeof scopeVal === 'undefined') { + $parse(key).assign($scope, publicMethods.get(storeName)); + } + + // Register a listener for changes on the $scope value + // to update the localStorage value + watchers[watcherId] = $scope.$watch(key, function (val) { + if (angular.isDefined(val)) { + publicMethods.set(storeName, val); + } + }, true); + + return publicMethods.get(storeName); + }, + /** + * Unbind - let's you unbind a variable from localStorage while removing the value from both + * the localStorage and the local variable and sets it to null + * @param $scope - the scope the variable was initially set in + * @param key - the name of the variable you are unbinding + * @param storeName - (optional) if you used a custom storeName you will have to specify it here as well + */ + unbind: function ($scope, key, storeName) { + var watcherId = privateMethods.getWatcherId($scope, key); + + storeName = storeName || key; + $parse(key).assign($scope, null); + publicMethods.remove(storeName); + + // Trying to unbind a watcher if it really was here + // Maybe it should do nothing at all in this case? E.g. you can't unbind what you haven't bound yet. + if (watchers[watcherId]) { + watchers[watcherId](); + delete watchers[watcherId]; + } + }, + /** + * Clear All - let's you clear out ALL localStorage variables, use this carefully! + */ + clearAll: function () { + storage.clear(); + }, + /** + * Check if cookie fallback is active right now + */ + isCookieFallbackActive: function () { + return !supported; + }, + + /** + * Allows the caller to obtain all the keys that are saved in Cookies or LocalStorage. Pulls all the keys + * that are saved in LocalStorage if LocalStorage is supported. Otherwise it will pull all the keys that + * are saved in the browser cookies and return those. + * + * Uses: String.trim() - ECMAScript 1.5+ + * + * @returns array + */ + getStorageKeys: function() { + var keys = []; + + if(!supported) { + var cookieArr = document.cookie.split(';'); + for( var cnt = 0, cntLen = cookieArr.length; cnt < cntLen; ++cnt) { + keys.push(cookieArr[cnt].split('=')[0].trim()); + } + } else { + for (var i = 0, len = localStorage.length; i < len; ++i) { + keys.push(localStorage.key(i)); + } + } + return keys; + } + }; + + return publicMethods; + }]); })(window, window.angular); From c890a87ada4d4f945ef6b42b83b4ae057f641c0b Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 27 Jan 2015 11:23:03 -0500 Subject: [PATCH 2/6] Updated unit tests to cover the new method. Removed spaces from the keys that will be used in Storage. This is best practices as it provides a consistent way to store and retrieve. --- src/angularLocalStorage.js | 2 + test/angularLocalStorage.spec.js | 325 +++++++++++++++++-------------- 2 files changed, 180 insertions(+), 147 deletions(-) diff --git a/src/angularLocalStorage.js b/src/angularLocalStorage.js index 6c903cc..3feac6b 100644 --- a/src/angularLocalStorage.js +++ b/src/angularLocalStorage.js @@ -70,6 +70,8 @@ * @returns {*} - will return whatever it is you've stored in the local storage */ set: function (key, value) { + // Remove spaces from the front and back of key + key = key.trim(); if (!supported) { try { $cookieStore.put(key, value); diff --git a/test/angularLocalStorage.spec.js b/test/angularLocalStorage.spec.js index 5ce2e24..eb3a7d1 100644 --- a/test/angularLocalStorage.spec.js +++ b/test/angularLocalStorage.spec.js @@ -1,149 +1,180 @@ describe('angularLocalStorage module', function () { - var storage, testValue, scope; - - beforeEach(function () { - module('angularLocalStorage'); - - inject(function ($injector) { - storage = $injector.get('storage'); - }); - }); - - describe('when use set() && get() methods', function () { - - beforeEach(function () { - storage.set('spec', 'some test string'); - }); - - beforeEach(function () { - testValue = storage.get('spec'); - }); - - it('should store value in localStorage', function () { - expect(testValue).toBe('some test string'); - }); - }); - - describe('when bind() $scope field to localStorage', function () { - beforeEach(function () { - inject(function ($rootScope) { - scope = $rootScope.$new(); - - scope.$apply(function () { - scope.spec = true; - }); - - storage.bind(scope, 'spec'); - - scope.$apply(function () { - scope.spec = false; - }); - }); - }); - - beforeEach(function () { - testValue = storage.get('spec'); - }); - - it('should have $scope value', function () { - expect(testValue).toEqual(scope.spec); - }); - - it('should not store undefined value', function () { - scope.$apply(function () { - scope.spec = undefined; - }); - - expect(testValue).toEqual(false); - expect(scope.spec).toBeUndefined(); - }); - - it('should store default value when passed as string', function() { - scope.$apply(function(){ - storage.bind(scope,'defaultedSpec','someDefault'); - }); - expect(scope.defaultedSpec).toEqual('someDefault'); - }); - - it('should store default value when passed as options object', function() { - scope.$apply(function(){ - storage.bind(scope,'defaultedSpecObj',{defaultValue: 'someNewDefault'}); - }); - expect(scope.defaultedSpecObj).toEqual('someNewDefault'); - }); - - it('using a custom storeName to bind variable', function() { - scope.$apply(function(){ - storage.bind(scope,'customStored',{defaultValue: 'randomValue123' ,storeName: 'myCustomStore'}); - scope.directFromLocal = storage.get('myCustomStore'); - }); - expect(scope.customStored).toEqual('randomValue123'); - expect(scope.directFromLocal).toEqual('randomValue123'); - }); - - it('should prefer existing scope variable over storage and default value', function() { - scope.spec = 'NewValue'; - storage.set('storedSpec', 'OldValue'); - scope.$apply(function(){ - storage.bind(scope, 'spec', {defaultValue: 'DefValue' ,storeName: 'storedSpec'}); - }); - scope.$apply(function(){ - scope.spec = scope.spec + 'Updated'; - }); - expect(scope.spec).toEqual('NewValueUpdated'); - expect(storage.get('storedSpec')).toEqual('NewValueUpdated'); - }); - }); - - - describe('when unbind() variable that clears localStorage and the variable', function () { - var testLocalStorageValue, testLocalVariableValue; - - beforeEach(function () { - storage.unbind(scope, 'spec'); - }); - - beforeEach(function () { - testLocalStorageValue = storage.get('spec'); - testLocalVariableValue = scope.spec; - }); - - it('should not contain field in storage', function () { - expect(testLocalStorageValue).toBeNull(); - }); - - it('should not contain field in scope', function () { - expect(testLocalVariableValue).toBeNull(); - }); - }); - - describe('when remove() field from localStorage', function () { - beforeEach(function () { - storage.remove('spec'); - }); - - beforeEach(function () { - testValue = storage.get('spec'); - }); - - it('should not contain field', function () { - expect(testValue).toBeNull(); - }); - }); - - describe('when use clearAll() method all should be gone', function () { - - beforeEach(function () { - storage.set('spec', 'some test string'); - }); - - beforeEach(function () { - storage.clearAll(); - testValue = storage.get('spec'); - }); - - it('should return null for value in localStorage', function () { - expect(testValue).toBeNull(); - }); - }); + var storage, testValue, scope; + + beforeEach(function () { + module('angularLocalStorage'); + + inject(function ($injector) { + storage = $injector.get('storage'); + }); + }); + + describe('when use set() && get() methods', function () { + + beforeEach(function () { + storage.set('spec', 'some test string'); + }); + + beforeEach(function () { + testValue = storage.get('spec'); + }); + + it('should store value in localStorage', function () { + expect(testValue).toBe('some test string'); + }); + }); + + describe('when use set() && get() methods when keys contain spaces', function () { + + beforeEach(function () { + storage.set(' spec ', 'some test string'); + }); + + beforeEach(function () { + testValue = storage.get('spec'); + }); + + it('should store value in localStorage', function () { + expect(testValue).toBe('some test string'); + }); + }); + + describe('when bind() $scope field to localStorage', function () { + beforeEach(function () { + inject(function ($rootScope) { + scope = $rootScope.$new(); + + scope.$apply(function () { + scope.spec = true; + }); + + storage.bind(scope, 'spec'); + + scope.$apply(function () { + scope.spec = false; + }); + }); + }); + + beforeEach(function () { + testValue = storage.get('spec'); + }); + + it('should have $scope value', function () { + expect(testValue).toEqual(scope.spec); + }); + + it('should not store undefined value', function () { + scope.$apply(function () { + scope.spec = undefined; + }); + + expect(testValue).toEqual(false); + expect(scope.spec).toBeUndefined(); + }); + + it('should store default value when passed as string', function () { + scope.$apply(function () { + storage.bind(scope, 'defaultedSpec', 'someDefault'); + }); + expect(scope.defaultedSpec).toEqual('someDefault'); + }); + + it('should store default value when passed as options object', function () { + scope.$apply(function () { + storage.bind(scope, 'defaultedSpecObj', {defaultValue: 'someNewDefault'}); + }); + expect(scope.defaultedSpecObj).toEqual('someNewDefault'); + }); + + it('using a custom storeName to bind variable', function () { + scope.$apply(function () { + storage.bind(scope, 'customStored', {defaultValue: 'randomValue123', storeName: 'myCustomStore'}); + scope.directFromLocal = storage.get('myCustomStore'); + }); + expect(scope.customStored).toEqual('randomValue123'); + expect(scope.directFromLocal).toEqual('randomValue123'); + }); + + it('should prefer existing scope variable over storage and default value', function () { + scope.spec = 'NewValue'; + storage.set('storedSpec', 'OldValue'); + scope.$apply(function () { + storage.bind(scope, 'spec', {defaultValue: 'DefValue', storeName: 'storedSpec'}); + }); + scope.$apply(function () { + scope.spec = scope.spec + 'Updated'; + }); + expect(scope.spec).toEqual('NewValueUpdated'); + expect(storage.get('storedSpec')).toEqual('NewValueUpdated'); + }); + }); + + + describe('when unbind() variable that clears localStorage and the variable', function () { + var testLocalStorageValue, testLocalVariableValue; + + beforeEach(function () { + storage.unbind(scope, 'spec'); + }); + + beforeEach(function () { + testLocalStorageValue = storage.get('spec'); + testLocalVariableValue = scope.spec; + }); + + it('should not contain field in storage', function () { + expect(testLocalStorageValue).toBeNull(); + }); + + it('should not contain field in scope', function () { + expect(testLocalVariableValue).toBeNull(); + }); + }); + + describe('when remove() field from localStorage', function () { + beforeEach(function () { + storage.remove('spec'); + }); + + beforeEach(function () { + testValue = storage.get('spec'); + }); + + it('should not contain field', function () { + expect(testValue).toBeNull(); + }); + }); + + describe('when use clearAll() method all should be gone', function () { + + beforeEach(function () { + storage.set('spec', 'some test string'); + }); + + beforeEach(function () { + storage.clearAll(); + testValue = storage.get('spec'); + }); + + it('should return null for value in localStorage', function () { + expect(testValue).toBeNull(); + }); + }); + + describe('when use getStorageKeys() method all should be returned', function () { + + beforeEach(function () { + storage.set('abcKey', 'some test string'); + storage.set('key123', 'test 123'); + }); + + beforeEach(function () { + array = storage.getStorageKeys(); + }); + + it('should return null for value in localStorage', function () { + expect(array.length).toEqual(2); + }); + }); }); \ No newline at end of file From 1c48b381411146be3dd368f8b52cf4f8c55b1c77 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 27 Jan 2015 11:28:44 -0500 Subject: [PATCH 3/6] Updated the README to reflect the new method available --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index e9fbbc3..635db92 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,9 @@ The simpliest localStorage module you will ever use. Allowing you to set, get, a // clear all localStorage values storage.clearAll(); + + // gets all the keys in the storage system returned as an array + storage.getStorageKeys(); // ['key','key2'] ``` ## Bower From fec26ea8f4ee214919415b3fe5d8b92ecb54f6ec Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 27 Jan 2015 12:37:38 -0500 Subject: [PATCH 4/6] Reverted the whitespace changes, and now have just the code modified. --- .jshintrc | 24 -- src/angularLocalStorage.js | 454 +++++++++++++++---------------- test/angularLocalStorage.spec.js | 343 +++++++++++------------ 3 files changed, 390 insertions(+), 431 deletions(-) delete mode 100644 .jshintrc diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 7070643..0000000 --- a/.jshintrc +++ /dev/null @@ -1,24 +0,0 @@ -{ - "node": true, - "browser": true, - "esnext": true, - "bitwise": true, - "camelcase": true, - "curly": true, - "eqeqeq": true, - "immed": true, - "indent": 2, - "latedef": true, - "newcap": true, - "noarg": true, - "quotmark": "single", - "regexp": true, - "undef": true, - "unused": true, - "strict": true, - "trailing": true, - "smarttabs": true, - "globals": { - "angular": false - } -} \ No newline at end of file diff --git a/src/angularLocalStorage.js b/src/angularLocalStorage.js index 3feac6b..730002b 100644 --- a/src/angularLocalStorage.js +++ b/src/angularLocalStorage.js @@ -4,233 +4,231 @@ */ (function (window, angular, undefined) { - 'use strict'; - - angular.module('angularLocalStorage', ['ngCookies']).factory('storage', ['$parse', '$cookieStore', '$window', '$log', function ($parse, $cookieStore, $window, $log) { - /** - * Global Vars - */ - var storage = (typeof $window.localStorage === 'undefined') ? undefined : $window.localStorage; - var supported = (typeof storage !== 'undefined'); - var watchers = {}; - - if (supported) { - // When Safari (OS X or iOS) is in private browsing mode it appears as though localStorage - // is available, but trying to call .setItem throws an exception below: - // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to add something to storage that exceeded the quota." - var testKey = '__' + Math.round(Math.random() * 1e7); - - try { - localStorage.setItem(testKey, testKey); - localStorage.removeItem(testKey); - } - catch (err) { - supported = false; - } - } - - var privateMethods = { - /** - * Pass any type of a string from the localStorage to be parsed so it returns a usable version (like an Object) - * @param res - a string that will be parsed for type - * @returns {*} - whatever the real type of stored value was - */ - parseValue: function (res) { - var val; - try { - val = angular.fromJson(res); - if (typeof val === 'undefined') { - val = res; - } - if (val === 'true') { - val = true; - } - if (val === 'false') { - val = false; - } - if ($window.parseFloat(val) === val && !angular.isObject(val)) { - val = $window.parseFloat(val); - } - } catch (e) { - val = res; - } - return val; - }, - - getWatcherId: function (scope, key) { - return scope.$id + key; - } - }; - - var publicMethods = { - /** - * Set - let's you set a new localStorage key pair set - * @param key - a string that will be used as the accessor for the pair - * @param value - the value of the localStorage item - * @returns {*} - will return whatever it is you've stored in the local storage - */ - set: function (key, value) { - // Remove spaces from the front and back of key - key = key.trim(); - if (!supported) { - try { - $cookieStore.put(key, value); - return value; - } catch (e) { - $log.log('Local Storage not supported, make sure you have angular-cookies enabled.'); - } - } - var saver = angular.toJson(value); - storage.setItem(key, saver); - return privateMethods.parseValue(saver); - }, - - /** - * Get - let's you get the value of any pair you've stored - * @param key - the string that you set as accessor for the pair - * @returns {*} - Object,String,Float,Boolean depending on what you stored - */ - get: function (key) { - if (!supported) { - try { - return $cookieStore.get(key); - } catch (e) { - return null; - } - } - var item = storage.getItem(key); - return privateMethods.parseValue(item); - }, - - /** - * Remove - let's you nuke a value from localStorage - * @param key - the accessor value - * @returns {boolean} - if everything went as planned - */ - remove: function (key) { - if (!supported) { - try { - $cookieStore.remove(key); - return true; - } catch (e) { - return false; - } - } - storage.removeItem(key); - return true; - }, - - /** - * Bind - let's you directly bind a localStorage value to a $scope variable - * @param {Angular $scope} $scope - the current scope you want the variable available in - * @param {String} key - the name of the variable you are binding - * @param {Object|String} opts - (optional) custom options like default value or unique store name - * Here are the available options you can set: - * * defaultValue: the default value - * * storeName: add a custom store key value instead of using the scope variable name - * @returns {*} - returns whatever the stored value is - */ - bind: function ($scope, key, opts) { - var watcherId = privateMethods.getWatcherId($scope, key); - - var defaultOpts = { - defaultValue: '', - storeName: '' - }; - // Backwards compatibility with old defaultValue string - if (angular.isString(opts)) { - opts = angular.extend({}, defaultOpts, {defaultValue: opts}); - } else { - // If no defined options we use defaults otherwise extend defaults - opts = (angular.isUndefined(opts)) ? defaultOpts : angular.extend(defaultOpts, opts); - } - - // Set the storeName key for the localStorage entry - // use user defined in specified - var storeName = opts.storeName || key; - var scopeVal = $scope.$eval(key); - - // If a value doesn't already exist store it as is - if (publicMethods.get(storeName) === null && typeof scopeVal === 'undefined') { - publicMethods.set(storeName, opts.defaultValue); - } - - // assign it to the $scope value - if (typeof scopeVal === 'undefined') { - $parse(key).assign($scope, publicMethods.get(storeName)); - } - - // Register a listener for changes on the $scope value - // to update the localStorage value - watchers[watcherId] = $scope.$watch(key, function (val) { - if (angular.isDefined(val)) { - publicMethods.set(storeName, val); - } - }, true); - - return publicMethods.get(storeName); - }, - /** - * Unbind - let's you unbind a variable from localStorage while removing the value from both - * the localStorage and the local variable and sets it to null - * @param $scope - the scope the variable was initially set in - * @param key - the name of the variable you are unbinding - * @param storeName - (optional) if you used a custom storeName you will have to specify it here as well - */ - unbind: function ($scope, key, storeName) { - var watcherId = privateMethods.getWatcherId($scope, key); - - storeName = storeName || key; - $parse(key).assign($scope, null); - publicMethods.remove(storeName); - - // Trying to unbind a watcher if it really was here - // Maybe it should do nothing at all in this case? E.g. you can't unbind what you haven't bound yet. - if (watchers[watcherId]) { - watchers[watcherId](); - delete watchers[watcherId]; - } - }, - /** - * Clear All - let's you clear out ALL localStorage variables, use this carefully! - */ - clearAll: function () { - storage.clear(); - }, - /** - * Check if cookie fallback is active right now - */ - isCookieFallbackActive: function () { - return !supported; - }, - - /** - * Allows the caller to obtain all the keys that are saved in Cookies or LocalStorage. Pulls all the keys - * that are saved in LocalStorage if LocalStorage is supported. Otherwise it will pull all the keys that - * are saved in the browser cookies and return those. - * - * Uses: String.trim() - ECMAScript 1.5+ - * - * @returns array - */ - getStorageKeys: function() { - var keys = []; - - if(!supported) { - var cookieArr = document.cookie.split(';'); - for( var cnt = 0, cntLen = cookieArr.length; cnt < cntLen; ++cnt) { - keys.push(cookieArr[cnt].split('=')[0].trim()); - } - } else { - for (var i = 0, len = localStorage.length; i < len; ++i) { - keys.push(localStorage.key(i)); - } - } - return keys; - } - }; - - return publicMethods; - }]); + 'use strict'; + + angular.module('angularLocalStorage', ['ngCookies']).factory('storage', ['$parse', '$cookieStore', '$window', '$log', function ($parse, $cookieStore, $window, $log) { + /** + * Global Vars + */ + var storage = (typeof $window.localStorage === 'undefined') ? undefined : $window.localStorage; + var supported = (typeof storage !== 'undefined'); + var watchers = {}; + + if (supported) { + // When Safari (OS X or iOS) is in private browsing mode it appears as though localStorage + // is available, but trying to call .setItem throws an exception below: + // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to add something to storage that exceeded the quota." + var testKey = '__' + Math.round(Math.random() * 1e7); + + try { + localStorage.setItem(testKey, testKey); + localStorage.removeItem(testKey); + } + catch (err) { + supported = false; + } + } + + var privateMethods = { + /** + * Pass any type of a string from the localStorage to be parsed so it returns a usable version (like an Object) + * @param res - a string that will be parsed for type + * @returns {*} - whatever the real type of stored value was + */ + parseValue: function (res) { + var val; + try { + val = angular.fromJson(res); + if (typeof val === 'undefined') { + val = res; + } + if (val === 'true') { + val = true; + } + if (val === 'false') { + val = false; + } + if ($window.parseFloat(val) === val && !angular.isObject(val)) { + val = $window.parseFloat(val); + } + } catch (e) { + val = res; + } + return val; + }, + + getWatcherId: function(scope, key) { + return scope.$id + key; + } + }; + + var publicMethods = { + /** + * Set - let's you set a new localStorage key pair set + * @param key - a string that will be used as the accessor for the pair + * @param value - the value of the localStorage item + * @returns {*} - will return whatever it is you've stored in the local storage + */ + set: function (key, value) { + if (!supported) { + try { + $cookieStore.put(key, value); + return value; + } catch(e) { + $log.log('Local Storage not supported, make sure you have angular-cookies enabled.'); + } + } + var saver = angular.toJson(value); + storage.setItem(key, saver); + return privateMethods.parseValue(saver); + }, + + /** + * Get - let's you get the value of any pair you've stored + * @param key - the string that you set as accessor for the pair + * @returns {*} - Object,String,Float,Boolean depending on what you stored + */ + get: function (key) { + if (!supported) { + try { + return $cookieStore.get(key); + } catch (e) { + return null; + } + } + var item = storage.getItem(key); + return privateMethods.parseValue(item); + }, + + /** + * Remove - let's you nuke a value from localStorage + * @param key - the accessor value + * @returns {boolean} - if everything went as planned + */ + remove: function (key) { + if (!supported) { + try { + $cookieStore.remove(key); + return true; + } catch (e) { + return false; + } + } + storage.removeItem(key); + return true; + }, + + /** + * Bind - let's you directly bind a localStorage value to a $scope variable + * @param {Angular $scope} $scope - the current scope you want the variable available in + * @param {String} key - the name of the variable you are binding + * @param {Object|String} opts - (optional) custom options like default value or unique store name + * Here are the available options you can set: + * * defaultValue: the default value + * * storeName: add a custom store key value instead of using the scope variable name + * @returns {*} - returns whatever the stored value is + */ + bind: function ($scope, key, opts) { + var watcherId = privateMethods.getWatcherId($scope, key); + + var defaultOpts = { + defaultValue: '', + storeName: '' + }; + // Backwards compatibility with old defaultValue string + if (angular.isString(opts)) { + opts = angular.extend({},defaultOpts,{defaultValue:opts}); + } else { + // If no defined options we use defaults otherwise extend defaults + opts = (angular.isUndefined(opts)) ? defaultOpts : angular.extend(defaultOpts,opts); + } + + // Set the storeName key for the localStorage entry + // use user defined in specified + var storeName = opts.storeName || key; + var scopeVal = $scope.$eval(key); + + // If a value doesn't already exist store it as is + if (publicMethods.get(storeName) === null && typeof scopeVal === 'undefined') { + publicMethods.set(storeName, opts.defaultValue); + } + + // assign it to the $scope value + if(typeof scopeVal === 'undefined') { + $parse(key).assign($scope, publicMethods.get(storeName)); + } + + // Register a listener for changes on the $scope value + // to update the localStorage value + watchers[watcherId] = $scope.$watch(key, function (val) { + if (angular.isDefined(val)) { + publicMethods.set(storeName, val); + } + }, true); + + return publicMethods.get(storeName); + }, + /** + * Unbind - let's you unbind a variable from localStorage while removing the value from both + * the localStorage and the local variable and sets it to null + * @param $scope - the scope the variable was initially set in + * @param key - the name of the variable you are unbinding + * @param storeName - (optional) if you used a custom storeName you will have to specify it here as well + */ + unbind: function($scope,key,storeName) { + var watcherId = privateMethods.getWatcherId($scope, key); + + storeName = storeName || key; + $parse(key).assign($scope, null); + publicMethods.remove(storeName); + + // Trying to unbind a watcher if it really was here + // Maybe it should do nothing at all in this case? E.g. you can't unbind what you haven't bound yet. + if(watchers[watcherId]) { + watchers[watcherId](); + delete watchers[watcherId]; + } + }, + /** + * Clear All - let's you clear out ALL localStorage variables, use this carefully! + */ + clearAll: function() { + storage.clear(); + }, + /** + * Check if cookie fallback is active right now + */ + isCookieFallbackActive: function() { + return !supported; + }, + + /** + * Allows the caller to obtain all the keys that are saved in Cookies or LocalStorage. Pulls all the keys + * that are saved in LocalStorage if LocalStorage is supported. Otherwise it will pull all the keys that + * are saved in the browser cookies and return those. + * + * Uses: String.trim() - ECMAScript 1.5+ + * + * @returns array + */ + getStorageKeys: function() { + var keys = []; + + if(!supported) { + var cookieArr = document.cookie.split(';'); + for( var cnt = 0, cntLen = cookieArr.length; cnt < cntLen; ++cnt) { + keys.push(cookieArr[cnt].split('=')[0].trim()); + } + } else { + for (var i = 0, len = localStorage.length; i < len; ++i) { + keys.push(localStorage.key(i)); + } + } + return keys; + } + }; + + return publicMethods; + }]); })(window, window.angular); diff --git a/test/angularLocalStorage.spec.js b/test/angularLocalStorage.spec.js index eb3a7d1..29e5116 100644 --- a/test/angularLocalStorage.spec.js +++ b/test/angularLocalStorage.spec.js @@ -1,180 +1,165 @@ describe('angularLocalStorage module', function () { - var storage, testValue, scope; - - beforeEach(function () { - module('angularLocalStorage'); - - inject(function ($injector) { - storage = $injector.get('storage'); - }); - }); - - describe('when use set() && get() methods', function () { - - beforeEach(function () { - storage.set('spec', 'some test string'); - }); - - beforeEach(function () { - testValue = storage.get('spec'); - }); - - it('should store value in localStorage', function () { - expect(testValue).toBe('some test string'); - }); - }); - - describe('when use set() && get() methods when keys contain spaces', function () { - - beforeEach(function () { - storage.set(' spec ', 'some test string'); - }); - - beforeEach(function () { - testValue = storage.get('spec'); - }); - - it('should store value in localStorage', function () { - expect(testValue).toBe('some test string'); - }); - }); - - describe('when bind() $scope field to localStorage', function () { - beforeEach(function () { - inject(function ($rootScope) { - scope = $rootScope.$new(); - - scope.$apply(function () { - scope.spec = true; - }); - - storage.bind(scope, 'spec'); - - scope.$apply(function () { - scope.spec = false; - }); - }); - }); - - beforeEach(function () { - testValue = storage.get('spec'); - }); - - it('should have $scope value', function () { - expect(testValue).toEqual(scope.spec); - }); - - it('should not store undefined value', function () { - scope.$apply(function () { - scope.spec = undefined; - }); - - expect(testValue).toEqual(false); - expect(scope.spec).toBeUndefined(); - }); - - it('should store default value when passed as string', function () { - scope.$apply(function () { - storage.bind(scope, 'defaultedSpec', 'someDefault'); - }); - expect(scope.defaultedSpec).toEqual('someDefault'); - }); - - it('should store default value when passed as options object', function () { - scope.$apply(function () { - storage.bind(scope, 'defaultedSpecObj', {defaultValue: 'someNewDefault'}); - }); - expect(scope.defaultedSpecObj).toEqual('someNewDefault'); - }); - - it('using a custom storeName to bind variable', function () { - scope.$apply(function () { - storage.bind(scope, 'customStored', {defaultValue: 'randomValue123', storeName: 'myCustomStore'}); - scope.directFromLocal = storage.get('myCustomStore'); - }); - expect(scope.customStored).toEqual('randomValue123'); - expect(scope.directFromLocal).toEqual('randomValue123'); - }); - - it('should prefer existing scope variable over storage and default value', function () { - scope.spec = 'NewValue'; - storage.set('storedSpec', 'OldValue'); - scope.$apply(function () { - storage.bind(scope, 'spec', {defaultValue: 'DefValue', storeName: 'storedSpec'}); - }); - scope.$apply(function () { - scope.spec = scope.spec + 'Updated'; - }); - expect(scope.spec).toEqual('NewValueUpdated'); - expect(storage.get('storedSpec')).toEqual('NewValueUpdated'); - }); - }); - - - describe('when unbind() variable that clears localStorage and the variable', function () { - var testLocalStorageValue, testLocalVariableValue; - - beforeEach(function () { - storage.unbind(scope, 'spec'); - }); - - beforeEach(function () { - testLocalStorageValue = storage.get('spec'); - testLocalVariableValue = scope.spec; - }); - - it('should not contain field in storage', function () { - expect(testLocalStorageValue).toBeNull(); - }); - - it('should not contain field in scope', function () { - expect(testLocalVariableValue).toBeNull(); - }); - }); - - describe('when remove() field from localStorage', function () { - beforeEach(function () { - storage.remove('spec'); - }); - - beforeEach(function () { - testValue = storage.get('spec'); - }); - - it('should not contain field', function () { - expect(testValue).toBeNull(); - }); - }); - - describe('when use clearAll() method all should be gone', function () { - - beforeEach(function () { - storage.set('spec', 'some test string'); - }); - - beforeEach(function () { - storage.clearAll(); - testValue = storage.get('spec'); - }); - - it('should return null for value in localStorage', function () { - expect(testValue).toBeNull(); - }); - }); - - describe('when use getStorageKeys() method all should be returned', function () { - - beforeEach(function () { - storage.set('abcKey', 'some test string'); - storage.set('key123', 'test 123'); - }); - - beforeEach(function () { - array = storage.getStorageKeys(); - }); - - it('should return null for value in localStorage', function () { - expect(array.length).toEqual(2); - }); - }); -}); \ No newline at end of file + var storage, testValue, scope; + + beforeEach(function () { + module('angularLocalStorage'); + + inject(function ($injector) { + storage = $injector.get('storage'); + }); + }); + + describe('when use set() && get() methods', function () { + + beforeEach(function () { + storage.set('spec', 'some test string'); + }); + + beforeEach(function () { + testValue = storage.get('spec'); + }); + + it('should store value in localStorage', function () { + expect(testValue).toBe('some test string'); + }); + }); + + describe('when bind() $scope field to localStorage', function () { + beforeEach(function () { + inject(function ($rootScope) { + scope = $rootScope.$new(); + + scope.$apply(function () { + scope.spec = true; + }); + + storage.bind(scope, 'spec'); + + scope.$apply(function () { + scope.spec = false; + }); + }); + }); + + beforeEach(function () { + testValue = storage.get('spec'); + }); + + it('should have $scope value', function () { + expect(testValue).toEqual(scope.spec); + }); + + it('should not store undefined value', function () { + scope.$apply(function () { + scope.spec = undefined; + }); + + expect(testValue).toEqual(false); + expect(scope.spec).toBeUndefined(); + }); + + it('should store default value when passed as string', function() { + scope.$apply(function(){ + storage.bind(scope,'defaultedSpec','someDefault'); + }); + expect(scope.defaultedSpec).toEqual('someDefault'); + }); + + it('should store default value when passed as options object', function() { + scope.$apply(function(){ + storage.bind(scope,'defaultedSpecObj',{defaultValue: 'someNewDefault'}); + }); + expect(scope.defaultedSpecObj).toEqual('someNewDefault'); + }); + + it('using a custom storeName to bind variable', function() { + scope.$apply(function(){ + storage.bind(scope,'customStored',{defaultValue: 'randomValue123' ,storeName: 'myCustomStore'}); + scope.directFromLocal = storage.get('myCustomStore'); + }); + expect(scope.customStored).toEqual('randomValue123'); + expect(scope.directFromLocal).toEqual('randomValue123'); + }); + + it('should prefer existing scope variable over storage and default value', function() { + scope.spec = 'NewValue'; + storage.set('storedSpec', 'OldValue'); + scope.$apply(function(){ + storage.bind(scope, 'spec', {defaultValue: 'DefValue' ,storeName: 'storedSpec'}); + }); + scope.$apply(function(){ + scope.spec = scope.spec + 'Updated'; + }); + expect(scope.spec).toEqual('NewValueUpdated'); + expect(storage.get('storedSpec')).toEqual('NewValueUpdated'); + }); + }); + + + describe('when unbind() variable that clears localStorage and the variable', function () { + var testLocalStorageValue, testLocalVariableValue; + + beforeEach(function () { + storage.unbind(scope, 'spec'); + }); + + beforeEach(function () { + testLocalStorageValue = storage.get('spec'); + testLocalVariableValue = scope.spec; + }); + + it('should not contain field in storage', function () { + expect(testLocalStorageValue).toBeNull(); + }); + + it('should not contain field in scope', function () { + expect(testLocalVariableValue).toBeNull(); + }); + }); + + describe('when remove() field from localStorage', function () { + beforeEach(function () { + storage.remove('spec'); + }); + + beforeEach(function () { + testValue = storage.get('spec'); + }); + + it('should not contain field', function () { + expect(testValue).toBeNull(); + }); + }); + + describe('when use clearAll() method all should be gone', function () { + + beforeEach(function () { + storage.set('spec', 'some test string'); + }); + + beforeEach(function () { + storage.clearAll(); + testValue = storage.get('spec'); + }); + + it('should return null for value in localStorage', function () { + expect(testValue).toBeNull(); + }); + }); + + describe('when use getStorageKeys() method all should be returned', function () { + + beforeEach(function () { + storage.set('abcKey', 'some test string'); + storage.set('key123', 'test 123'); + }); + + beforeEach(function () { + array = storage.getStorageKeys(); + }); + + it('should return null for value in localStorage', function () { + expect(array.length).toEqual(2); + }); + }); +}); From 1831bc7bc31c78e729af2c78d146123fe2de01f0 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 27 Jan 2015 13:17:24 -0500 Subject: [PATCH 5/6] Updated the code based upon code review feedback. Altered the method to be getKeys, which is consistent with the naming pattern in the rest of the class. Changed the unit test to provide an accurate description if failed. Changed the README to reflect the naming change. --- README.md | 2 +- src/angularLocalStorage.js | 7 +++---- test/angularLocalStorage.spec.js | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 635db92..7d05141 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ The simpliest localStorage module you will ever use. Allowing you to set, get, a storage.clearAll(); // gets all the keys in the storage system returned as an array - storage.getStorageKeys(); // ['key','key2'] + storage.getKeys(); // ['key','key2'] ``` ## Bower diff --git a/src/angularLocalStorage.js b/src/angularLocalStorage.js index 730002b..fea7d69 100644 --- a/src/angularLocalStorage.js +++ b/src/angularLocalStorage.js @@ -203,15 +203,14 @@ }, /** - * Allows the caller to obtain all the keys that are saved in Cookies or LocalStorage. Pulls all the keys - * that are saved in LocalStorage if LocalStorage is supported. Otherwise it will pull all the keys that - * are saved in the browser cookies and return those. + * Allows the caller to obtain all the keys that are saved in Cookies or LocalStorage. Gets all the keys + * that are saved in LocalStorage or Cookie. * * Uses: String.trim() - ECMAScript 1.5+ * * @returns array */ - getStorageKeys: function() { + getKeys: function() { var keys = []; if(!supported) { diff --git a/test/angularLocalStorage.spec.js b/test/angularLocalStorage.spec.js index 29e5116..2c8ec04 100644 --- a/test/angularLocalStorage.spec.js +++ b/test/angularLocalStorage.spec.js @@ -147,7 +147,7 @@ describe('angularLocalStorage module', function () { }); }); - describe('when use getStorageKeys() method all should be returned', function () { + describe('when use getKeys() method all should be returned', function () { beforeEach(function () { storage.set('abcKey', 'some test string'); @@ -155,10 +155,10 @@ describe('angularLocalStorage module', function () { }); beforeEach(function () { - array = storage.getStorageKeys(); + array = storage.getKeys(); }); - it('should return null for value in localStorage', function () { + it('should return an array equaling the length of two', function () { expect(array.length).toEqual(2); }); }); From 0347cf98f7e951820ea49fe959dea7bc2229679a Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 27 Jan 2015 13:23:38 -0500 Subject: [PATCH 6/6] Updated unit test to evaluate the return from the getKeys method. It will ensure that in the array key123 and abcKey are returned. --- test/angularLocalStorage.spec.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/angularLocalStorage.spec.js b/test/angularLocalStorage.spec.js index 2c8ec04..9ac96e4 100644 --- a/test/angularLocalStorage.spec.js +++ b/test/angularLocalStorage.spec.js @@ -161,5 +161,10 @@ describe('angularLocalStorage module', function () { it('should return an array equaling the length of two', function () { expect(array.length).toEqual(2); }); + + it('should return the same keys that were put into storage', function() { + expect(array.indexOf("key123")).toBeGreaterThan(-1); + expect(array.indexOf("abcKey")).toBeGreaterThan(-1); + }); }); });