├── .bowerrc ├── .gitignore ├── bower.json ├── package.json ├── LICENSE ├── Gruntfile.js ├── ngStorage.min.js ├── CHANGELOG.md ├── ngStorage.27.js ├── README.md ├── ngStorage.js └── test └── spec.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "components" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | components 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngstorage", 3 | "main": "./ngStorage.js", 4 | "keywords": [ 5 | "angular", 6 | "cookie", 7 | "storage", 8 | "local", 9 | "localstorage", 10 | "session", 11 | "sessionstorage" 12 | ], 13 | "dependencies": { 14 | "angular": ">=1.0.8" 15 | }, 16 | "devDependencies": { 17 | "angular-mocks": ">=1.0.8", 18 | "chai": "~1.8.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngStorage", 3 | "version": "0.3.0", 4 | "private": true, 5 | "author": "Gias Kay Lee", 6 | "licenses": [ 7 | { 8 | "type": "MIT", 9 | "url": "https://github.com/gsklee/ngStorage/blob/master/LICENSE" 10 | } 11 | ], 12 | "scripts": { 13 | "test": "./node_modules/.bin/grunt test" 14 | }, 15 | "devDependencies": { 16 | "grunt": "~0.4.1", 17 | "grunt-cli": "~0.1.11", 18 | "grunt-contrib-uglify": "~0.2.4", 19 | "grunt-karma": "~0.7.1", 20 | "karma-chrome-launcher": "~0.1.4", 21 | "karma-firefox-launcher": "~0.1.3", 22 | "karma-mocha": "~0.1.0", 23 | "karma-phantomjs-launcher": "~0.1.4" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Gias Kay Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | var browsers = [ 5 | //'Chrome', 6 | 'PhantomJS', 7 | //'Firefox' 8 | ] 9 | grunt.initConfig({ 10 | pkg: grunt.file.readJSON('package.json'), 11 | 12 | karma: { 13 | unit: { 14 | options: { 15 | files: [ 16 | 'components/angular/angular.js', 17 | 'components/angular-mocks/angular-mocks.js', 18 | 'components/chai/chai.js', 19 | 'ngStorage.js', 20 | 'test/spec.js' 21 | ] 22 | }, 23 | 24 | frameworks: ['mocha'], 25 | 26 | browsers: browsers, 27 | 28 | singleRun: true 29 | } 30 | }, 31 | 32 | uglify: { 33 | options: { 34 | banner: '/*! <%= pkg.name %> <%= pkg.version %> | Copyright (c) <%= grunt.template.today("yyyy") %> Gias Kay Lee | MIT License */' 35 | }, 36 | 37 | build: { 38 | src: '<%= pkg.name %>.js', 39 | dest: '<%= pkg.name %>.min.js' 40 | } 41 | } 42 | }); 43 | 44 | grunt.loadNpmTasks('grunt-contrib-uglify'); 45 | grunt.loadNpmTasks('grunt-karma'); 46 | 47 | grunt.registerTask('test', ['karma']); 48 | 49 | grunt.registerTask('default', [ 50 | 'test', 51 | 'uglify' 52 | ]); 53 | }; 54 | -------------------------------------------------------------------------------- /ngStorage.min.js: -------------------------------------------------------------------------------- 1 | /*! ngStorage 0.3.0 | Copyright (c) 2014 Gias Kay Lee | MIT License */!function(){"use strict";function a(a){var b={prefix:"ngStorage-",prefixLength:10};return{setPrefix:function(a){b.prefix=a,b.prefixLength=a.length},$get:["$rootScope","$window","$log","$timeout",function(c,d,e,f){function g(){var b=d[a];if(b&&"localStorage"===a){var c="__"+Math.round(1e7*Math.random());try{b.setItem(c,c),b.removeItem(c)}catch(f){e.warn("This browser does not support Web Storage!"),b=void 0}}return b}var h,i,j=g(),k={$default:function(a){for(var b in a)angular.isDefined(k[b])||(k[b]=a[b]);return k},$reset:function(a){for(var b in k)"$"===b[0]||delete k[b];return k.$default(a)},$save:function(){if(f.cancel(i),!angular.equals(k,h)){angular.forEach(k,function(a,c){angular.isDefined(a)&&"$"!==c[0]&&j.setItem(b.prefix+c,angular.toJson(a)),delete h[c]});for(var a in h)j.removeItem(b.prefix+a);h=angular.copy(k)}},$supported:!!j};if(!j){var l,m={};j={setItem:function(a,b){return m[a]=String(b)},getItem:function(a){return m.hasOwnProperty(a)?m[a]:l},removeItem:function(a){return delete m[a]},clear:function(){return m={}},length:0}}for(var n,o=0,p=j.length;p>o;o++)(n=j.key(o))&&b.prefix===n.slice(0,b.prefixLength)&&(k[n.slice(b.prefixLength)]=angular.fromJson(j.getItem(n)));return h=angular.copy(k),c.$watch(function(){i||(i=f(function(){k.$save()},100))}),"localStorage"===a&&d.addEventListener&&d.addEventListener("storage",function(a){b.prefix===a.key.slice(0,b.prefixLength)&&(a.newValue?k[a.key.slice(b.prefixLength)]=angular.fromJson(a.newValue):delete k[a.key.slice(b.prefixLength)],h=angular.copy(k),c.$apply())}),k}]}}angular.module("ngStorage",[]).provider("$localStorage",a("localStorage")).provider("$sessionStorage",a("sessionStorage"))}(); 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.3.0 / 2013.10.16 2 | * Remove the force overwrite on each cycle which has been causing inadvertent side effects such as breaking object references, changing `$$hashKey`s, or modifying user code behaviors. 3 | * Add dirty-check debouncing. ([#2](https://github.com/gsklee/ngStorage/issues/2)) 4 | * Now incorporating Grunt to empower unit testing as well as uglification. ([#14](https://github.com/gsklee/ngStorage/issues/14)) 5 | * A few bugfixes, some of which are IE-only. ([#9](https://github.com/gsklee/ngStorage/issues/9), [#10](https://github.com/gsklee/ngStorage/issues/10), [#11](https://github.com/gsklee/ngStorage/issues/11)) 6 | 7 | --- 8 | 9 | ### 0.2.3 / 2013.08.26 10 | * Fix dependency version definitions in `bower.json`. 11 | 12 | --- 13 | 14 | ### 0.2.2 / 2013.08.09 15 | * Add explicit DI annotation. ([#5](https://github.com/gsklee/ngStorage/issues/5)) 16 | * Fix an error in IE9 when Web Storage is empty. ([#8](https://github.com/gsklee/ngStorage/issues/8)) 17 | * Use the standard `addEventListener()` instead of jqLite's `bind()` to avoid the jQuery-specific `event.originalEvent`. ([#6](https://github.com/gsklee/ngStorage/issues/6)) 18 | 19 | --- 20 | 21 | ### 0.2.1 / 2013.07.24 22 | * Improve compatibility with existing Web Storage data using `ngStorage-` as the namespace. ([#3](https://github.com/gsklee/ngStorage/issues/3), [#4](https://github.com/gsklee/ngStorage/issues/4)) 23 | 24 | --- 25 | 26 | ### 0.2.0 / 2013.07.19 27 | * ***BREAKING CHANGE:*** `$clear()` has been replaced by `$reset()` and now accepts an optional parameter as the default content after reset. 28 | * Add `$default()` to make default value binding easier. 29 | * Data changes in `$localStorage` now propagate to different browser tabs. 30 | * Improve compatibility with existing Web Storage data. ([#1](https://github.com/gsklee/ngStorage/issues/1)) 31 | * Properties being hooked onto the services with a `$` prefix are considered to belong to AngularJS inner workings and will no longer be written into Web Storage. 32 | 33 | --- 34 | 35 | ### 0.1.0 / 2013.07.07 36 | * Initial release. 37 | -------------------------------------------------------------------------------- /ngStorage.27.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | 5 | /** 6 | * @ngdoc overview 7 | * @name ngStorage 8 | */ 9 | 10 | angular.module('ngStorage', []). 11 | 12 | /** 13 | * @ngdoc object 14 | * @name ngStorage.$localStorage 15 | * @requires $rootScope 16 | * @requires $window 17 | */ 18 | 19 | provider('$localStorage', _storageProvider('localStorage')). 20 | 21 | /** 22 | * @ngdoc object 23 | * @name ngStorage.$sessionStorage 24 | * @requires $rootScope 25 | * @requires $window 26 | */ 27 | 28 | provider('$sessionStorage', _storageProvider('sessionStorage')); 29 | 30 | function _storageProvider(storageType) { 31 | var prefix = 'ngStorage-'; 32 | return { 33 | setPrefix: function(value) { 34 | if (angular.isString(value)) { 35 | prefix = value; 36 | } 37 | }, 38 | $get: [ 39 | '$rootScope', 40 | '$window', 41 | '$timeout', 42 | '$log', 43 | function( 44 | $rootScope, 45 | $window, 46 | $timeout, 47 | $log 48 | ){ 49 | // #9: Assign a placeholder object if Web Storage is unavailable to prevent breaking the entire AngularJS app 50 | var webStorage = $window[storageType] || ($log.warn('This browser does not support Web Storage!'), {}), 51 | $storage = { 52 | $default: function(items) { 53 | for (var k in items) { 54 | angular.isDefined($storage[k]) || ($storage[k] = items[k]); 55 | } 56 | 57 | return $storage; 58 | }, 59 | $reset: function(items) { 60 | for (var k in $storage) { 61 | '$' === k[0] || delete $storage[k]; 62 | } 63 | 64 | return $storage.$default(items); 65 | } 66 | }, 67 | prefixLen = prefix.length, 68 | _last$storage, 69 | _debounce; 70 | 71 | for (var i = 0, k; i < webStorage.length; i++) { 72 | // #8, #10: `webStorage.key(i)` may be an empty string (or throw an exception in IE9 if `webStorage` is empty) 73 | (k = webStorage.key(i)) && prefix === k.slice(0, prefixLen) && ($storage[k.slice(prefixLen)] = angular.fromJson(webStorage.getItem(k))); 74 | } 75 | 76 | _last$storage = angular.copy($storage); 77 | 78 | $rootScope.$watch(function() { 79 | _debounce || (_debounce = $timeout(function() { 80 | _debounce = null; 81 | 82 | if (!angular.equals($storage, _last$storage)) { 83 | angular.forEach($storage, function(v, k) { 84 | angular.isDefined(v) && '$' !== k[0] && webStorage.setItem(prefix + k, angular.toJson(v)); 85 | 86 | delete _last$storage[k]; 87 | }); 88 | 89 | for (var k in _last$storage) { 90 | webStorage.removeItem(prefix + k); 91 | } 92 | 93 | _last$storage = angular.copy($storage); 94 | } 95 | }, 100)); 96 | }); 97 | 98 | // #6: Use `$window.addEventListener` instead of `angular.element` to avoid the jQuery-specific `event.originalEvent` 99 | 'localStorage' === storageType && $window.addEventListener && $window.addEventListener('storage', function(event) { 100 | if (prefix === event.key.slice(0, prefixLen)) { 101 | event.newValue ? $storage[event.key.slice(prefixLen)] = angular.fromJson(event.newValue) : delete $storage[event.key.slice(prefixLen)]; 102 | 103 | _last$storage = angular.copy($storage); 104 | 105 | $rootScope.$apply(); 106 | } 107 | }); 108 | 109 | return $storage; 110 | } 111 | ] 112 | } 113 | } 114 | 115 | })(); 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ### Disclaimer 3 | 4 | The original author of [ngStorage](https://github.com/gsklee/ngStorage) is [Gias Kay Lee](https://github.com/gsklee) 5 | 6 | As the repository was silent for about 11 months I decided to start a fork and began merging the pull requests into this fork. 7 | 8 | ngStorage 9 | ========= 10 | 11 | An [AngularJS](https://github.com/angular/angular.js) module that makes Web Storage working in the *Angular Way*. Contains two services: `$localStorage` and `$sessionStorage`. 12 | 13 | ### Differences with Other Implementations 14 | 15 | * **No Getter 'n' Setter Bullshit** - Right from AngularJS homepage: "Unlike other frameworks, there is no need to [...] wrap the model in accessors methods. Just plain old JavaScript here." Now you can enjoy the same benefit while achieving data persistence with Web Storage. 16 | 17 | * **sessionStorage** - We got this often-overlooked buddy covered. 18 | 19 | * **Cleanly-Authored Code** - Written in the *Angular Way*, well-structured with testability in mind. 20 | 21 | * **No Cookie Fallback** - With Web Storage being [readily available](http://caniuse.com/namevalue-storage) in [all the browsers AngularJS officially supports](http://docs.angularjs.org/misc/faq#canidownloadthesourcebuildandhosttheangularjsenvironmentlocally), such fallback is largely redundant. 22 | 23 | Install 24 | ======= 25 | 26 | ```bash 27 | bower install ngstorage 28 | ``` 29 | 30 | Usage 31 | ===== 32 | 33 | ### Require ngStorage and Inject the Services 34 | 35 | ```javascript 36 | angular.module('app', [ 37 | 'ngStorage' 38 | ]).controller('Ctrl', function( 39 | $scope, 40 | $localStorage, 41 | $sessionStorage 42 | ){}); 43 | ``` 44 | 45 | ### Read and Write | [Demo](http://plnkr.co/edit/3vfRkvG7R9DgQxtWbGHz?p=preview) 46 | 47 | Pass `$localStorage` (or `$sessionStorage`) by reference to a hook under `$scope` in plain ol' JavaScript: 48 | 49 | ```javascript 50 | $scope.$storage = $localStorage; 51 | ``` 52 | 53 | And use it like you-already-know: 54 | 55 | ```html 56 |
57 | 58 | 59 | ``` 60 | 61 | > Optionally, specify default values using the `$default()` method: 62 | > 63 | > ```javascript 64 | > $scope.$storage = $localStorage.$default({ 65 | > counter: 42 66 | > }); 67 | > ``` 68 | 69 | With this setup, changes will be automatically sync'd between `$scope.$storage`, `$localStorage`, and localStorage - even across different browser tabs! 70 | 71 | ### Read and Write Alternative (Not Recommended) | [Demo](http://plnkr.co/edit/9ZmkzRkYzS3iZkG8J5IK?p=preview) 72 | 73 | If you're not fond of the presence of `$scope.$storage`, you can always use watchers: 74 | 75 | ```javascript 76 | $scope.counter = $localStorage.counter || 42; 77 | 78 | $scope.$watch('counter', function() { 79 | $localStorage.counter = $scope.counter; 80 | }); 81 | 82 | $scope.$watch(function() { 83 | return angular.toJson($localStorage); 84 | }, function() { 85 | $scope.counter = $localStorage.counter; 86 | }); 87 | ``` 88 | 89 | This, however, is not the way ngStorage is designed to be used with. As can be easily seen by comparing the demos, this approach is way more verbose, and may have potential performance implications as the values being watched quickly grow. 90 | 91 | ### Delete | [Demo](http://plnkr.co/edit/o4w3VGqmp8opfrWzvsJy?p=preview) 92 | 93 | Plain ol' JavaScript again, what else could you better expect? 94 | 95 | ```javascript 96 | // Both will do 97 | delete $scope.$storage.counter; 98 | delete $localStorage.counter; 99 | ``` 100 | 101 | This will delete the corresponding entry inside the Web Storage. 102 | 103 | ### Delete Everything | [Demo](http://plnkr.co/edit/YiG28KTFdkeFXskolZqs?p=preview) 104 | 105 | If you wish to clear the Storage in one go, use the `$reset()` method: 106 | 107 | ```javascript 108 | $localStorage.$reset(); 109 | ```` 110 | 111 | > Optionally, pass in an object you'd like the Storage to reset to: 112 | > 113 | > ```javascript 114 | > $localStorage.$reset({ 115 | > counter: 42 116 | > }); 117 | > ``` 118 | 119 | ### Saving 120 | 121 | When using the module you should not care if or when something is written to the browsers storage engine. However, if you have a critical part, when you need to know the exact timing, you can use the `$save()` method: 122 | 123 | ```javascript 124 | $localStorage.value = 100; 125 | $localStorage.$save() 126 | ```` 127 | 128 | ### Supported 129 | 130 | The $storage will expose the state of support for the browsers storage. 131 | 132 | ```javascript 133 | $localStorage.$supported === false; 134 | $sessionStorage.$supported === false; 135 | ```` 136 | 137 | If `$supported` is false your data cannot be saved. 138 | 139 | ### Permitted Values | [Demo](http://plnkr.co/edit/n0acYLdhk3AeZmPOGY9Z?p=preview) 140 | 141 | You can store anything except those [not supported by JSON](http://www.json.org/js.html): 142 | 143 | * `Infinity`, `NaN` - Will be replaced with `null`. 144 | * `undefined`, Function - Will be removed. 145 | 146 | ### Minification 147 | Just run `$ npm install` to install dependencies. Then run `$ grunt` for minification. 148 | 149 | Todos 150 | ===== 151 | 152 | * ngdoc Documentation 153 | * Namespace Support 154 | * Version Control - Changes need to change the version numbering 155 | 156 | Contributors 157 | ============ 158 | * [#Issue 39](https://github.com/gsklee/ngStorage/issues/39) - Tobias Kopelke - The reason for the fork 159 | * [#Issue 41](https://github.com/gsklee/ngStorage/pull/41) - d10n - https://github.com/d10n 160 | * [#Issue 60](https://github.com/gsklee/ngStorage/pull/60) - Dmitri Zaitsev - https://github.com/dmitriz 161 | * [#Issue 63](https://github.com/gsklee/ngStorage/pull/63) - mrkoreye - https://github.com/mrkoreye 162 | * [#Issue 68](https://github.com/gsklee/ngStorage/pull/68) - Ken Baltrinic - https://github.com/kbaltrinic 163 | 164 | Any contribution will be appreciated. 165 | -------------------------------------------------------------------------------- /ngStorage.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc overview 6 | * @name ngStorage 7 | */ 8 | 9 | angular.module('ngStorage', []). 10 | 11 | /** 12 | * @ngdoc object 13 | * @name ngStorage.$localStorage 14 | * @requires $rootScope 15 | * @requires $window 16 | */ 17 | 18 | provider('$localStorage', _storageProvider('localStorage')). 19 | 20 | /** 21 | * @ngdoc object 22 | * @name ngStorage.$sessionStorage 23 | * @requires $rootScope 24 | * @requires $window 25 | */ 26 | 27 | provider('$sessionStorage', _storageProvider('sessionStorage')); 28 | 29 | function _storageProvider(storageType) { 30 | var settings = { 31 | prefix : 'ngStorage-', 32 | prefixLength : 10 33 | }; 34 | 35 | return { 36 | setPrefix: function(prefix) { 37 | settings.prefix = prefix; 38 | settings.prefixLength = prefix.length; 39 | }, 40 | $get: [ 41 | '$rootScope', 42 | '$window', 43 | '$log', 44 | '$timeout', 45 | function( 46 | $rootScope, 47 | $window, 48 | $log, 49 | $timeout 50 | ){ 51 | 52 | function getStorageImplementation() { 53 | var storageImpl = $window[storageType]; 54 | 55 | // When Safari (OS X or iOS) is in private browsing mode, it appears as though localStorage 56 | // is available, but trying to call .setItem throws an exception below: 57 | // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to add something to storage that exceeded the quota." 58 | if (storageImpl && storageType === 'localStorage') { 59 | var key = '__' + Math.round(Math.random() * 1e7); 60 | 61 | try { 62 | storageImpl.setItem(key, key); 63 | storageImpl.removeItem(key); 64 | } catch (err) { 65 | $log.warn('This browser does not support Web Storage!'); 66 | storageImpl = undefined; 67 | } 68 | } 69 | 70 | return storageImpl; 71 | } 72 | 73 | var webStorage = getStorageImplementation(), 74 | $storage = { 75 | $default: function(items) { 76 | for (var k in items) { 77 | angular.isDefined($storage[k]) || ($storage[k] = items[k]); 78 | } 79 | 80 | return $storage; 81 | }, 82 | $reset: function(items) { 83 | for (var k in $storage) { 84 | '$' === k[0] || delete $storage[k]; 85 | } 86 | 87 | return $storage.$default(items); 88 | }, 89 | $save: function() { 90 | $timeout.cancel(_debounce); 91 | if (!angular.equals($storage, _last$storage)) { 92 | angular.forEach($storage, function(v, k) { 93 | angular.isDefined(v) && '$' !== k[0] && webStorage.setItem(settings.prefix + k, angular.toJson(v)); 94 | 95 | delete _last$storage[k]; 96 | }); 97 | 98 | for (var k in _last$storage) { 99 | webStorage.removeItem(settings.prefix + k); 100 | } 101 | 102 | _last$storage = angular.copy($storage); 103 | } 104 | }, 105 | $supported: !!webStorage 106 | }, 107 | _last$storage, 108 | _debounce; 109 | 110 | // #9: Assign a placeholder object if Web Storage is unavailable to prevent breaking the entire AngularJS app 111 | if(!webStorage) { 112 | var data = {}, 113 | undef; 114 | webStorage = { 115 | setItem: function(id, val) { 116 | return data[id] = String(val); 117 | }, 118 | getItem: function(id) { 119 | return data.hasOwnProperty(id) ? data[id] : undef; 120 | }, 121 | removeItem: function(id) { 122 | return delete data[id]; 123 | }, 124 | clear: function() { 125 | return data = {}; 126 | }, 127 | // This is only a shim and not meant to be updated. It avoids webStorage.length == undefined in the loop below. 128 | length: 0 129 | }; 130 | } 131 | 132 | for (var i = 0, k, storageLength = webStorage.length; i < storageLength; i++) { 133 | // #8, #10: `webStorage.key(i)` may be an empty string (or throw an exception in IE9 if `webStorage` is empty) 134 | (k = webStorage.key(i)) && settings.prefix === k.slice(0, settings.prefixLength) && ($storage[k.slice(settings.prefixLength)] = angular.fromJson(webStorage.getItem(k))); 135 | } 136 | 137 | _last$storage = angular.copy($storage); 138 | 139 | $rootScope.$watch(function() { 140 | _debounce || (_debounce = $timeout(function() { 141 | $storage.$save(); 142 | }, 100)); 143 | }); 144 | 145 | // #6: Use `$window.addEventListener` instead of `angular.element` to avoid the jQuery-specific `event.originalEvent` 146 | 'localStorage' === storageType && $window.addEventListener && $window.addEventListener('storage', function(event) { 147 | if (settings.prefix === event.key.slice(0, settings.prefixLength)) { 148 | event.newValue ? $storage[event.key.slice(settings.prefixLength)] = angular.fromJson(event.newValue) : delete $storage[event.key.slice(settings.prefixLength)]; 149 | 150 | _last$storage = angular.copy($storage); 151 | 152 | $rootScope.$apply(); 153 | } 154 | }); 155 | 156 | return $storage; 157 | } 158 | ] 159 | } 160 | } 161 | 162 | })(); 163 | -------------------------------------------------------------------------------- /test/spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('ngStorage', function() { 4 | var expect = chai.expect; 5 | 6 | var clearStorage = function($storage) { 7 | delete $storage.$reset; 8 | delete $storage.$default; 9 | delete $storage.$save; 10 | delete $storage.$supported; 11 | }; 12 | 13 | describeStorageBehaviorFor('localStorage'); 14 | describeStorageBehaviorFor('sessionStorage'); 15 | 16 | function describeStorageBehaviorFor(storageType) { 17 | 18 | var $window, $rootScope, $storage, $timeout, $storageProvider; 19 | 20 | function initStorage(initialValues, init) { 21 | 22 | $window = { 23 | eventHandlers: {}, 24 | addEventListener: function(event, handler) { 25 | this.eventHandlers[event] = handler; 26 | } 27 | }; 28 | 29 | $window[storageType] = { 30 | length: Object.keys(initialValues).length, 31 | data: initialValues, 32 | getItem: function(key) { return this.data[key]; }, 33 | setItem: function(key, value) { 34 | this.data[key] = value; 35 | this.length = Object.keys(this.data).length; 36 | }, 37 | removeItem: function(key) { 38 | delete this.data[key]; 39 | this.length = Object.keys(this.data).length; 40 | }, 41 | key: function(i) { return Object.keys(this.data)[i]; } 42 | }; 43 | 44 | init && init($window[storageType]); 45 | 46 | module(function($provide) { 47 | $provide.value('$window', $window); 48 | }); 49 | 50 | inject(['$rootScope', '$' + storageType, '$timeout', 51 | function(_$rootScope_, _$storage_, _$timeout_) { 52 | $rootScope = _$rootScope_; 53 | $storage = _$storage_; 54 | $timeout = _$timeout_; 55 | } 56 | ]); 57 | 58 | return $window[storageType]; 59 | } 60 | 61 | describe('$' + storageType, function() { 62 | 63 | describe("default-namespace", function() { 64 | 65 | beforeEach(module('ngStorage')); 66 | 67 | it('should contain a ' + storageType + ' service', function() { 68 | expect(storageType).not.to.equal(null); 69 | }); 70 | 71 | it('should, upon loading, contain a value for each ngStorage- key in window.' + 72 | storageType, function() { 73 | 74 | initStorage({ 75 | nonNgStorage: 'this should be ingored', 76 | 'ngStorage-string': '"a string"', 77 | 'ngStorage-number': '123', 78 | 'ngStorage-bool': 'true', 79 | 'ngStorage-object': '{"string":"a string", "number": 123, "bool": true}' 80 | }); 81 | 82 | clearStorage($storage); 83 | 84 | expect($storage).to.deep.equal({ 85 | string: 'a string', 86 | number: 123, 87 | bool: true, 88 | object: { string:'a string', number: 123, bool: true } 89 | }); 90 | 91 | }); 92 | 93 | it('should add a key to window.' + storageType + ' when a key is added to $storage', 94 | function() { 95 | 96 | initStorage({}); 97 | $storage.newKey = 'some value'; 98 | $rootScope.$digest(); 99 | $timeout.flush(); 100 | expect($window[storageType].data) 101 | .to.deep.equal({'ngStorage-newKey': '"some value"'}); 102 | }); 103 | 104 | it('should update the associated key in window.' + storageType + ' when a key in $' + 105 | storageType + ' is updated', function() { 106 | 107 | initStorage({'ngStorage-existing': '"update me"'}); 108 | $storage.existing = 'updated'; 109 | $rootScope.$digest(); 110 | 111 | $timeout.flush(); 112 | expect($window[storageType].data) 113 | .to.deep.equal({'ngStorage-existing': '"updated"'}); 114 | }); 115 | 116 | it('should update the associated key in window.' + storageType + ' when a key in $' + 117 | storageType + ' is updated instantanious when $save is called', function() { 118 | 119 | initStorage({'ngStorage-existing': '"update me"'}); 120 | $storage.existing = 'updated'; 121 | $rootScope.$digest(); 122 | 123 | expect($window[storageType].data) 124 | .to.deep.equal({'ngStorage-existing': '"update me"'}); 125 | 126 | $storage.$save(); 127 | 128 | expect($window[storageType].data) 129 | .to.deep.equal({'ngStorage-existing': '"updated"'}); 130 | }); 131 | 132 | it('should delete the associated key from window.' + storageType + ' when a key in $' + 133 | storageType + ' is deleted', function() { 134 | 135 | initStorage({'ngStorage-existing': '"delete me"'}); 136 | delete $storage.existing; 137 | $rootScope.$digest(); 138 | $timeout.flush(); 139 | expect($window[storageType].data).to.deep.equal({}); 140 | 141 | }); 142 | 143 | describe('when $reset is called with no arguments', function() { 144 | 145 | beforeEach(function() { 146 | 147 | initStorage({ 148 | nonNgStorage: 'this should not be changed', 149 | 'ngStorage-delete': '"this should be deleted"' 150 | }); 151 | 152 | $storage.$reset(); 153 | $rootScope.$digest(); 154 | $timeout.flush(); 155 | }); 156 | 157 | it('should delete all ngStorage- keys from window.' + storageType, function() { 158 | 159 | expect($window[storageType].data).to.deep.equal({ 160 | nonNgStorage: 'this should not be changed' 161 | }); 162 | 163 | }); 164 | 165 | it('should delete all keys from $' + storageType, function() { 166 | 167 | clearStorage($storage); 168 | 169 | expect($storage).to.deep.equal({}); 170 | 171 | }); 172 | 173 | }); 174 | 175 | describe('when $reset is called with an object', function() { 176 | 177 | beforeEach(function() { 178 | 179 | initStorage({ 180 | nonNgStorage: 'this should not be changed', 181 | 'ngStorage-delete': '"this should be deleted"' 182 | }); 183 | 184 | $storage.$reset({some: 'value'}); 185 | $rootScope.$digest(); 186 | $timeout.flush(); 187 | }); 188 | 189 | it('should reset the ngStorage- keys on window.' + storageType + 190 | ' to match the object', function() { 191 | 192 | expect($window[storageType].data).to.deep.equal({ 193 | nonNgStorage: 'this should not be changed', 194 | 'ngStorage-some': '"value"' 195 | }); 196 | 197 | }); 198 | 199 | it('should reset $' + storageType + ' to match the object', function() { 200 | 201 | clearStorage($storage); 202 | 203 | expect($storage).to.deep.equal({some: 'value'}); 204 | 205 | }); 206 | 207 | }); 208 | 209 | describe('when $default is called', function() { 210 | 211 | beforeEach(function() { 212 | 213 | initStorage({ 214 | nonNgStorage: 'this should not be changed', 215 | 'ngStorage-existing': '"this should not be replaced"' 216 | }); 217 | 218 | $storage.$default({ 219 | existing: 'oops! replaced!', 220 | 'new': 'new value' 221 | }); 222 | 223 | $rootScope.$digest(); 224 | $timeout.flush(); 225 | }); 226 | 227 | it('should should add any missing ngStorage- keys on window.' + storageType, 228 | function() { 229 | 230 | expect($window[storageType].data['ngStorage-new']) 231 | .to.equal('"new value"'); 232 | 233 | }); 234 | 235 | it('should should add any missing values to $' + storageType, function() { 236 | 237 | expect($storage['new']).to.equal('new value'); 238 | 239 | }); 240 | 241 | it('should should not modify any existing ngStorage- keys on window.' + storageType, 242 | function() { 243 | 244 | expect($window[storageType].data['ngStorage-existing']) 245 | .to.equal('"this should not be replaced"'); 246 | 247 | }); 248 | 249 | it('should should not modify any existing values on $' + storageType, function() { 250 | 251 | expect($storage['existing']) 252 | .to.equal('this should not be replaced'); 253 | 254 | }); 255 | }); 256 | 257 | if (storageType == 'localStorage') { 258 | 259 | describe('when an ngStorage- value in window.localStorage is updated', function() { 260 | 261 | beforeEach(function() { 262 | 263 | initStorage({'ngStorage-existing': '"update me"'}); 264 | 265 | var updateEvent = { 266 | key: 'ngStorage-existing', 267 | newValue: '"updated"' 268 | }; 269 | $window.eventHandlers.storage(updateEvent); 270 | }); 271 | 272 | it('should reflect the update', function() { 273 | expect($storage.existing).to.equal('updated'); 274 | }); 275 | }); 276 | 277 | describe('when an ngStorage- value in window.localStorage is added', function() { 278 | 279 | beforeEach(function() { 280 | 281 | initStorage({}); 282 | 283 | var updateEvent = { 284 | key: 'ngStorage-value', 285 | newValue: '"new"' 286 | }; 287 | $window.eventHandlers.storage(updateEvent); 288 | }); 289 | 290 | it('should reflect the addition', function() { 291 | expect($storage.value).to.equal('new'); 292 | }); 293 | }); 294 | 295 | describe('when an ngStorage- value in window.localStorage is deleted', function() { 296 | beforeEach(function() { 297 | initStorage({'ngStorage-existing': '"delete me"'}); 298 | var updateEvent = { 299 | key: 'ngStorage-existing', 300 | }; 301 | $window.eventHandlers.storage(updateEvent); 302 | }); 303 | 304 | it('should reflect the deletion', function() { 305 | expect($storage.existing).to.be.undefined; 306 | }); 307 | }); 308 | 309 | describe("when window.localStorage is not available (safari)", function() { 310 | beforeEach(function() { 311 | // invalidate storage 312 | initStorage({}, function(storage) { 313 | storage.setItem = function(id, val) { 314 | throw Error('Not available!'); 315 | }; 316 | }) 317 | }); 318 | 319 | it('should, upon loading, contain a value for each ngStorage- key in window.localStorage', function() { 320 | expect(function() { 321 | $storage.newKey = 'some value'; 322 | $rootScope.$digest(); 323 | $timeout.flush(); 324 | }).not.to.throw(); 325 | }); 326 | 327 | it('should, upon loading, contain a value for each ngStorage- key in window.localStorage', function() { 328 | expect($storage.$supported).to.be.equal(false); 329 | }); 330 | 331 | }); 332 | } 333 | }); 334 | 335 | describe("anything-namespace", function() { 336 | beforeEach(module('ngStorage', [ 337 | '$' + storageType + 'Provider', 338 | function($storageProvider) { 339 | $storageProvider.setPrefix("anything-"); 340 | } 341 | ])); 342 | 343 | it('should contain a ' + storageType + ' service', function() { 344 | expect(storageType).not.to.equal(null); 345 | }); 346 | 347 | it('should, upon loading, contain a value for each ngStorage- key in window.' + 348 | storageType, function() { 349 | 350 | initStorage({ 351 | nonNgStorage: 'this should be ingored', 352 | 'anything-string': '"a string"', 353 | 'anything-number': '123', 354 | 'anything-bool': 'true', 355 | 'anything-object': '{"string":"a string", "number": 123, "bool": true}' 356 | }); 357 | 358 | clearStorage($storage); 359 | 360 | expect($storage).to.deep.equal({ 361 | string: 'a string', 362 | number: 123, 363 | bool: true, 364 | object: { string:'a string', number: 123, bool: true } 365 | }); 366 | }); 367 | }); 368 | }); 369 | } 370 | }); 371 | 372 | 373 | --------------------------------------------------------------------------------