├── .gitdown ├── README.md ├── _badges.md ├── _browser-compatibility.md ├── _contributing.md ├── _development.md ├── _installation.md └── _usage.md ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── changes.md ├── config.js ├── dist ├── angular-locker.js ├── angular-locker.min.js └── angular-locker.min.js.map ├── gulpfile.babel.js ├── jsdoc.json ├── package.json ├── src └── angular-locker.js └── test ├── karma.conf.js ├── mock └── storageMock.js └── spec └── angular-locker.spec.js /.gitdown/README.md: -------------------------------------------------------------------------------- 1 | angular-locker 2 | ============== 3 | 4 | A simple & configurable abstraction for local/session storage in angular projects - providing a fluent api that is powerful and easy to use. 5 | 6 | {"gitdown": "include", "file": ".gitdown/_badges.md"} 7 | 8 | {"gitdown": "contents"} 9 | 10 | {"gitdown": "include", "file": ".gitdown/_installation.md"} 11 | 12 | {"gitdown": "include", "file": ".gitdown/_usage.md"} 13 | 14 | {"gitdown": "include", "file": ".gitdown/_browser-compatibility.md"} 15 | 16 | {"gitdown": "include", "file": ".gitdown/_contributing.md"} 17 | 18 | {"gitdown": "include", "file": ".gitdown/_development.md"} 19 | 20 | ## License 21 | 22 | {"gitdown": "include", "file": "LICENSE"} 23 | -------------------------------------------------------------------------------- /.gitdown/_badges.md: -------------------------------------------------------------------------------- 1 | [![Build Status](http://img.shields.io/travis/tymondesigns/angular-locker/master.svg?style=flat-square)](https://travis-ci.org/tymondesigns/angular-locker) 2 | [![Code Climate](http://img.shields.io/codeclimate/github/tymondesigns/angular-locker.svg?style=flat-square)](https://codeclimate.com/github/tymondesigns/angular-locker) 3 | [![Test Coverage](http://img.shields.io/codeclimate/coverage/github/tymondesigns/angular-locker.svg?style=flat-square)](https://codeclimate.com/github/tymondesigns/angular-locker) 4 | [![License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](http://www.opensource.org/licenses/MIT) 5 | [![NPM Release](https://img.shields.io/npm/v/angular-locker.svg?style=flat-square)](https://www.npmjs.org/package/angular-locker) 6 | [![NPM Monthly Downloads](https://img.shields.io/npm/dm/angular-locker.svg?style=flat-square)](https://www.npmjs.org/package/angular-locker) -------------------------------------------------------------------------------- /.gitdown/_browser-compatibility.md: -------------------------------------------------------------------------------- 1 | ## Browser Compatibility 2 | 3 | IE8 is not supported because I am utilising `Object.keys()` 4 | 5 | To check if the browser natively supports local and session storage, you can do the following: 6 | 7 | ```js 8 | if (! locker.supported()) { 9 | // load a polyfill? 10 | } 11 | ``` 12 | 13 | I would recommend using [Remy's Storage polyfill](https://gist.github.com/remy/350433) if you want to support older browsers. 14 | 15 | For the latest browser compatibility chart see [HERE](http://caniuse.com/namevalue-storage) -------------------------------------------------------------------------------- /.gitdown/_contributing.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Take care to maintain the existing coding style using the provided `.jscsrc` file. Add unit tests for any new or changed functionality. Lint and test your code using Gulp. -------------------------------------------------------------------------------- /.gitdown/_development.md: -------------------------------------------------------------------------------- 1 | ## Development 2 | 3 | ```bash 4 | $ npm install 5 | $ gulp 6 | ``` -------------------------------------------------------------------------------- /.gitdown/_installation.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | #### via bower 4 | 5 | ```bash 6 | $ bower install angular-locker 7 | ``` 8 | 9 | #### via npm 10 | 11 | ```bash 12 | $ npm install angular-locker 13 | ``` 14 | 15 | #### via jsDelivr CDN 16 | 17 | http://www.jsdelivr.com/#!angular.locker 18 | 19 | #### manual 20 | 21 | Simply download the zip file [HERE](https://github.com/tymondesigns/angular-locker/archive/master.zip) and include `dist/angular-locker.min.js` in your project. 22 | 23 | {"gitdown": "filesize", "file": "dist/angular-locker.min.js", "gzip": true} Minified & gzipped. -------------------------------------------------------------------------------- /.gitdown/_usage.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | ### Adding to your project 4 | 5 | Add `angular-locker` as a dependency 6 | 7 | ```js 8 | angular.module('myApp', ['angular-locker']) 9 | ``` 10 | 11 | Configure via `lockerProvider` (*optional*) 12 | 13 | ```js 14 | .config(['lockerProvider', function config(lockerProvider) { 15 | lockerProvider.defaults({ 16 | driver: 'session', 17 | namespace: 'myApp', 18 | separator: '.', 19 | eventsEnabled: true, 20 | extend: {} 21 | }); 22 | }]); 23 | ``` 24 | 25 | *Note*: You can also pass `false` to `namespace` if you prefer to not have a namespace in your keys. 26 | 27 | inject `locker` into your controller/service/directive etc 28 | 29 | ```js 30 | .factory('MyFactory', ['locker', function MyFactory(locker) { 31 | locker.put('someKey', 'someVal'); 32 | }]); 33 | ``` 34 | 35 | #### Extending Locker 36 | 37 | You can pass in an implementation of the [Storage Interface](https://developer.mozilla.org/en-US/docs/Web/API/Storage) to the `lockerProvider` as described above. e.g. 38 | 39 | ```js 40 | lockerProvider.defaults({ 41 | extend: { 42 | myCustomStore: function () { 43 | // getItem 44 | // setItem 45 | // removeItem 46 | // etc 47 | } 48 | } 49 | }); 50 | 51 | // then use as normal 52 | locker.driver('myCustomStore').put('foo', 'bar'); 53 | ``` 54 | 55 | See my [storageMock](https://github.com/tymondesigns/angular-locker/blob/master/test/mock/storageMock.js) for an example on how to define a custom implementation. 56 | 57 | ---------------------------- 58 | 59 | ### Switching storage drivers 60 | 61 | There may be times where you will want to dynamically switch between using local and session storage. 62 | To achieve this, simply chain the `driver()` setter to specify what storage driver you want to use, as follows: 63 | 64 | ```js 65 | // put an item into session storage 66 | locker.driver('session').put('sessionKey', ['some', 'session', 'data']); 67 | 68 | // this time use local storage 69 | locker.driver('local').put('localKey', ['some', 'persistent', 'things']); 70 | ``` 71 | 72 | ### Switching namespace 73 | 74 | ```js 75 | // add an item within a different namespace 76 | locker.namespace('otherNamespace').put('foo', 'bar'); 77 | ``` 78 | 79 | Omitting the driver or namespace setters will respect whatever default was specified via `lockerProvider`. 80 | 81 | ---------------------------- 82 | 83 | ### Adding items to locker 84 | 85 | there are several ways to add something to locker: 86 | 87 | You can add Objects, Arrays, whatever :) 88 | 89 | locker will automatically serialize your objects/arrays in local/session storage 90 | 91 | ```js 92 | locker.put('someString', 'anyDataType'); 93 | locker.put('someObject', { foo: 'I will be serialized', bar: 'pretty cool eh' }); 94 | locker.put('someArray', ['foo', 'bar', 'baz']); 95 | // etc 96 | ``` 97 | 98 | #### adding via value function param 99 | 100 | Inserts specified key and return value of function 101 | 102 | ```js 103 | locker.put('someKey', function() { 104 | var obj = { foo: 'bar', bar: 'baz' }; 105 | // some other logic 106 | return obj; 107 | }); 108 | ``` 109 | 110 | The current value will be passed into the function so you can perform logic on the current value, before returning it. e.g. 111 | 112 | ```js 113 | locker.put('someKey', ['foo', 'bar']); 114 | 115 | locker.put('someKey', function(current) { 116 | current.push('baz'); 117 | 118 | return current; 119 | }); 120 | 121 | locker.get('someKey'); // = ['foo', 'bar', 'baz'] 122 | ``` 123 | 124 | If the current value is not already set then you can pass a third parameter as a default that will be returned instead. e.g. 125 | 126 | ```js 127 | // given locker.get('foo') is not defined 128 | locker.put('foo', function (current) { 129 | // current will equal 'bar' 130 | }, 'bar'); 131 | ``` 132 | 133 | #### adding multiple items at once by passing a single object 134 | 135 | This will add each key/value pair as a **separate** item in storage 136 | 137 | ```js 138 | locker.put({ 139 | someKey: 'johndoe', 140 | anotherKey: ['some', 'random', 'array'], 141 | boolKey: true 142 | }); 143 | ``` 144 | 145 | #### adding via key function param 146 | 147 | Inserts each item from the returned Object, similar to above 148 | 149 | ```js 150 | locker.put(function() { 151 | // some logic 152 | return { 153 | foo: ['lorem', 'ipsum', 'dolor'], 154 | user: { 155 | username: 'johndoe', 156 | displayName: 'Johnny Doe', 157 | active: true, 158 | role: 'user' 159 | } 160 | }; 161 | }); 162 | ``` 163 | 164 | #### conditionally adding an item if it doesn't already exist 165 | 166 | For this functionality you can use the `add()` method. 167 | 168 | If the key already exists then no action will be taken and `false` will be returned 169 | 170 | ```js 171 | locker.add('someKey', 'someVal'); // true or false - whether the item was added or not 172 | ``` 173 | 174 | ---------------------------- 175 | 176 | ### Retrieving items from locker 177 | 178 | ```js 179 | // locker.put('fooArray', ['bar', 'baz', 'bob']); 180 | 181 | locker.get('fooArray'); // ['bar', 'baz', 'bob'] 182 | ``` 183 | 184 | #### setting a default value 185 | 186 | if the key does not exist then, if specified the default will be returned 187 | 188 | ```js 189 | locker.get('keyDoesNotExist', 'a default value'); // 'a default value' 190 | ``` 191 | 192 | #### retrieving multiple items at once 193 | 194 | You may pass an array to the `get()` method to return an Object containing the specified keys (if they exist) 195 | 196 | ```js 197 | locker.get(['someKey', 'anotherKey', 'foo']); 198 | 199 | // will return something like... 200 | { 201 | someKey: 'someValue', 202 | anotherKey: true, 203 | foo: 'bar' 204 | } 205 | ``` 206 | 207 | #### deleting afterwards 208 | 209 | You can also retrieve an item and then delete it via the `pull()` method 210 | 211 | ```js 212 | // locker.put('someKey', { foo: 'bar', baz: 'bob' }); 213 | 214 | locker.pull('someKey', 'defaultVal'); // { foo: 'bar', baz: 'bob' } 215 | 216 | // then... 217 | 218 | locker.get('someKey', 'defaultVal'); // 'defaultVal' 219 | ``` 220 | 221 | #### all items 222 | 223 | You can retrieve all items within the current namespace 224 | 225 | This will return an object containing all the key/value pairs in storage 226 | 227 | ```js 228 | locker.all(); 229 | // or 230 | locker.namespace('somethingElse').all(); 231 | ``` 232 | 233 | #### counting items 234 | 235 | To count the number of items within a given namespace: 236 | 237 | ```js 238 | locker.count(); 239 | // or 240 | locker.namespace('somethingElse').count(); 241 | ``` 242 | 243 | ---------------------------- 244 | 245 | ### Checking item exists in locker 246 | 247 | You can determine whether an item exists in the current namespace via 248 | 249 | ```js 250 | locker.has('someKey'); // true or false 251 | // or 252 | locker.namespace('foo').has('bar'); 253 | 254 | // e.g. 255 | if (locker.has('user.authToken') ) { 256 | // we're logged in 257 | } else { 258 | // go to login page or something 259 | } 260 | ``` 261 | 262 | ---------------------------- 263 | 264 | ### Removing items from locker 265 | 266 | The simplest way to remove an item is to pass the key to the `forget()` method 267 | 268 | ```js 269 | locker.forget('keyToRemove'); 270 | // or 271 | locker.driver('session').forget('sessionKey'); 272 | // etc.. 273 | ``` 274 | 275 | #### removing multiple items at once 276 | 277 | You can also pass an array. 278 | 279 | ```js 280 | locker.forget(['keyToRemove', 'anotherKeyToRemove', 'something', 'else']); 281 | ``` 282 | 283 | #### removing all within namespace 284 | 285 | you can remove all the items within the currently set namespace via the `clean()` method 286 | 287 | ```js 288 | locker.clean(); 289 | // or 290 | locker.namespace('someOtherNamespace').clean(); 291 | ``` 292 | #### removing all items within the currently set storage driver 293 | 294 | ```js 295 | locker.empty(); 296 | ``` 297 | 298 | ---------------------------- 299 | 300 | ### Events 301 | 302 | There are 3 events that can be fired during various operations, these are: 303 | 304 | ```js 305 | // fired when a new item is added to storage 306 | $rootScope.$on('locker.item.added', function (e, payload) { 307 | // payload is equal to: 308 | { 309 | driver: 'local', // the driver that was set when the event was fired 310 | namespace: 'locker', // the namespace that was set when the event was fired 311 | key: 'foo', // the key that was added 312 | value: 'bar' // the value that was added 313 | } 314 | }); 315 | ``` 316 | 317 | ```js 318 | // fired when an item is removed from storage 319 | $rootScope.$on('locker.item.forgotten', function (e, payload) { 320 | // payload is equal to: 321 | { 322 | driver: 'local', // the driver that was set when the event was fired 323 | namespace: 'locker', // the namespace that was set when the event was fired 324 | key: 'foo', // the key that was removed 325 | } 326 | }); 327 | ``` 328 | 329 | ```js 330 | // fired when an item's value changes to something new 331 | $rootScope.$on('locker.item.updated', function (e, payload) { 332 | // payload is equal to: 333 | { 334 | driver: 'local', // the driver that was set when the event was fired 335 | namespace: 'locker', // the namespace that was set when the event was fired 336 | key: 'foo', // the key that was updated 337 | oldValue: 'bar', // the value that was set before the item was updated 338 | newValue: 'baz' // the new value that the item was updated to 339 | } 340 | }); 341 | ``` 342 | 343 | ---------------------------- 344 | 345 | ### Binding to a $scope property 346 | 347 | You can bind a scope property to a key in storage. Whenever the $scope value changes, it will automatically be persisted in storage. e.g. 348 | 349 | ```js 350 | app.controller('AppCtrl', ['$scope', function ($scope) { 351 | 352 | locker.bind($scope, 'foo'); 353 | 354 | $scope.foo = ['bar', 'baz']; 355 | 356 | locker.get('foo'); // = ['bar', 'baz'] 357 | 358 | }]); 359 | ``` 360 | 361 | You can also set a default value via the third parameter: 362 | 363 | ```js 364 | app.controller('AppCtrl', ['$scope', function ($scope) { 365 | 366 | locker.bind($scope, 'foo', 'someDefault'); 367 | 368 | $scope.foo; // = 'someDefault' 369 | 370 | locker.get('foo'); // = 'someDefault' 371 | 372 | }]); 373 | ``` 374 | 375 | To unbind the $scope property, simply use the unbind method: 376 | 377 | 378 | ```js 379 | app.controller('AppCtrl', ['$scope', function ($scope) { 380 | 381 | locker.unbind($scope, 'foo'); 382 | 383 | $scope.foo; // = undefined 384 | 385 | locker.get('foo'); // = undefined 386 | 387 | }]); 388 | ``` 389 | 390 | ---------------------------- 391 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | vendor 3 | test/coverage -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "jscs-jsdoc" 4 | ], 5 | "jsDoc": { 6 | "checkAnnotations": true, 7 | "checkParamNames": true, 8 | "checkRedundantParams": true, 9 | "checkReturnTypes": true, 10 | "checkRedundantReturns": true, 11 | "requireReturnTypes": true, 12 | "requireNewlineAfterDescription": true, 13 | "checkTypes": "capitalizedNativeCase", 14 | "enforceExistence": "exceptExports" 15 | }, 16 | 17 | "requireCurlyBraces": [ 18 | "else", 19 | "for", 20 | "while", 21 | "do", 22 | "try", 23 | "catch" 24 | ], 25 | "validateIndentation": 4, 26 | "validateQuoteMarks": "'", 27 | 28 | "disallowMultipleLineStrings": true, 29 | "disallowMixedSpacesAndTabs": true, 30 | "disallowTrailingWhitespace": true, 31 | 32 | "disallowKeywordsOnNewLine": ["else"], 33 | 34 | "requireSpaceAfterKeywords": [ 35 | "if", 36 | "else", 37 | "for", 38 | "while", 39 | "do", 40 | "switch", 41 | "return", 42 | "try", 43 | "catch" 44 | ], 45 | 46 | "requireSpaceBeforeBinaryOperators": [ 47 | "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", 48 | "&=", "|=", "^=", "+=", 49 | 50 | "+", "-", "*", "/", "%", "<<", ">>", ">>>", "&", 51 | "|", "^", "&&", "||", "===", "==", ">=", 52 | "<=", "<", ">", "!=", "!==" 53 | ], 54 | 55 | "requireSpaceAfterBinaryOperators": true, 56 | "requireSpacesInConditionalExpression": true, 57 | "requireSpaceBeforeBlockStatements": true, 58 | "requireSpacesInForStatement": true, 59 | "requireLineFeedAtFileEnd": true, 60 | "requireSpaceAfterLineComment": true, 61 | "requireCamelCaseOrUpperCaseIdentifiers": true, 62 | "requireSpacesInFunctionExpression": { 63 | "beforeOpeningRoundBrace": true, 64 | "beforeOpeningCurlyBrace": true 65 | }, 66 | 67 | "requireSpaceAfterPrefixUnaryOperators": ["!"], 68 | 69 | "disallowSpacesInCallExpression": true, 70 | "disallowSpaceAfterObjectKeys": true, 71 | "requireSpaceBeforeObjectValues": true, 72 | "requireCapitalizedConstructors": true, 73 | "requireDotNotation": true, 74 | "validateParameterSeparator": ", ", 75 | 76 | "maximumLineLength": 120 77 | } -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": false, 8 | "eqeqeq": true, 9 | "indent": 4, 10 | "immed": true, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": false, 18 | "trailing": false, 19 | "globals" : { 20 | "angular": true, 21 | "define": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 4 5 | - 6 6 | - 8 7 | before_script: 8 | - npm install -g gulp codeclimate-test-reporter 9 | after_script: 10 | - codeclimate-test-reporter < test/coverage/lcov/lcov.info 11 | 12 | env: 13 | global: 14 | - secure: jGnioqs1TSYzv+IfKLf6weOn7b7+xkEkGPhnVJ/YngvGBiyu5G3aFdJRV5xZos4H2lhhsIHUej1ZocLM61bFLdgLp8p5khEXP8TSZPsO3OGyAj4ZhFRtNwK6vcY1y1Lez8CA/3Clikz8iSm5Twg83hwofLr7C57kQJ22gruISm4= 15 | - secure: cigU5ReZLTT2l75BFfEvSBGy+GZ68ivSfs561Kc22vSDidEBb/1x1Ydv7cjNLbyGoT43MJBYFHL5xeyyY9haBn03sUpXHu/PjNULJFfQUWpizqhfSmC7OZJNVuonESruq4k7yHWGgUEE9aHuJGi8cDw1cB0k6TMAtY7LExQNLaE= 16 | - secure: YHkhMjp5bOE8unA9zwpwLuPy0V9Ml3Sa5uQ8SFAnyDIDoo7YC5SZHXf5FBOOUlL9u0+buYNKT9YseiSUCpqMK09Q1JsN4xZXl660QjaZTYNki+1bleEAmGEYXr60XNOSs6c53b7nxg7FtS1ozRCgx43bEBKXCJKHnbPEVwQrcUI= 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Sean Tymon 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-locker 2 | ============== 3 | 4 | A simple & configurable abstraction for local/session storage in angular projects - providing a fluent api that is powerful and easy to use. 5 | 6 | [![Build Status](http://img.shields.io/travis/tymondesigns/angular-locker/master.svg?style=flat-square)](https://travis-ci.org/tymondesigns/angular-locker) 7 | [![Test Coverage](http://img.shields.io/codeclimate/coverage/github/tymondesigns/angular-locker.svg?style=flat-square)](https://codeclimate.com/github/tymondesigns/angular-locker) 8 | [![License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](http://www.opensource.org/licenses/MIT) 9 | [![NPM Release](https://img.shields.io/npm/v/angular-locker.svg?style=flat-square)](https://www.npmjs.org/package/angular-locker) 10 | [![NPM Monthly Downloads](https://img.shields.io/npm/dm/angular-locker.svg?style=flat-square)](https://www.npmjs.org/package/angular-locker) 11 | [![NPM Total Downloads](https://img.shields.io/npm/dt/angular-locker.svg?style=flat-square)](https://www.npmjs.org/package/angular-locker) 12 | 13 | * [Installation](#installation) 14 | * [Usage](#usage) 15 | * [Adding to your project](#usage-adding-to-your-project) 16 | * [Switching storage drivers](#usage-switching-storage-drivers) 17 | * [Switching namespace](#usage-switching-namespace) 18 | * [Adding items to locker](#usage-adding-items-to-locker) 19 | * [Retrieving items from locker](#usage-retrieving-items-from-locker) 20 | * [Checking item exists in locker](#usage-checking-item-exists-in-locker) 21 | * [Removing items from locker](#usage-removing-items-from-locker) 22 | * [Events](#usage-events) 23 | * [Binding to a $scope property](#usage-binding-to-a-scope-property) 24 | * [Browser Compatibility](#browser-compatibility) 25 | * [Contributing](#contributing) 26 | * [Development](#development) 27 | * [License](#license) 28 | 29 | 30 |

Installation

31 | 32 |

via bower

33 | 34 | ```bash 35 | $ bower install angular-locker 36 | ``` 37 | 38 |

via npm

39 | 40 | ```bash 41 | $ npm install angular-locker 42 | ``` 43 | 44 |

via jsDelivr CDN

45 | 46 | http://www.jsdelivr.com/#!angular.locker 47 | 48 |

manual

49 | 50 | Simply download the zip file [HERE](https://github.com/tymondesigns/angular-locker/archive/master.zip) and include `dist/angular-locker.min.js` in your project. 51 | 52 | 1.66 KB Minified & gzipped. 53 | 54 |

Usage

55 | 56 |

Adding to your project

57 | 58 | Add `angular-locker` as a dependency 59 | 60 | ```js 61 | angular.module('myApp', ['angular-locker']) 62 | ``` 63 | 64 | Configure via `lockerProvider` (*optional*) 65 | 66 | ```js 67 | .config(['lockerProvider', function config(lockerProvider) { 68 | lockerProvider.defaults({ 69 | driver: 'session', 70 | namespace: 'myApp', 71 | separator: '.', 72 | eventsEnabled: true, 73 | extend: {} 74 | }); 75 | }]); 76 | ``` 77 | 78 | *Note*: You can also pass `false` to `namespace` if you prefer to not have a namespace in your keys. 79 | 80 | inject `locker` into your controller/service/directive etc 81 | 82 | ```js 83 | .factory('MyFactory', ['locker', function MyFactory(locker) { 84 | locker.put('someKey', 'someVal'); 85 | }]); 86 | ``` 87 | 88 |

Extending Locker

89 | 90 | You can pass in an implementation of the [Storage Interface](https://developer.mozilla.org/en-US/docs/Web/API/Storage) to the `lockerProvider` as described above. e.g. 91 | 92 | ```js 93 | lockerProvider.defaults({ 94 | extend: { 95 | myCustomStore: function () { 96 | // getItem 97 | // setItem 98 | // removeItem 99 | // etc 100 | } 101 | } 102 | }); 103 | 104 | // then use as normal 105 | locker.driver('myCustomStore').put('foo', 'bar'); 106 | ``` 107 | 108 | See my [storageMock](https://github.com/tymondesigns/angular-locker/blob/master/test/mock/storageMock.js) for an example on how to define a custom implementation. 109 | 110 | ---------------------------- 111 | 112 |

Switching storage drivers

113 | 114 | There may be times where you will want to dynamically switch between using local and session storage. 115 | To achieve this, simply chain the `driver()` setter to specify what storage driver you want to use, as follows: 116 | 117 | ```js 118 | // put an item into session storage 119 | locker.driver('session').put('sessionKey', ['some', 'session', 'data']); 120 | 121 | // this time use local storage 122 | locker.driver('local').put('localKey', ['some', 'persistent', 'things']); 123 | ``` 124 | 125 |

Switching namespace

126 | 127 | ```js 128 | // add an item within a different namespace 129 | locker.namespace('otherNamespace').put('foo', 'bar'); 130 | ``` 131 | 132 | Omitting the driver or namespace setters will respect whatever default was specified via `lockerProvider`. 133 | 134 | ---------------------------- 135 | 136 |

Adding items to locker

137 | 138 | there are several ways to add something to locker: 139 | 140 | You can add Objects, Arrays, whatever :) 141 | 142 | locker will automatically serialize your objects/arrays in local/session storage 143 | 144 | ```js 145 | locker.put('someString', 'anyDataType'); 146 | locker.put('someObject', { foo: 'I will be serialized', bar: 'pretty cool eh' }); 147 | locker.put('someArray', ['foo', 'bar', 'baz']); 148 | // etc 149 | ``` 150 | 151 |

adding via value function param

152 | 153 | Inserts specified key and return value of function 154 | 155 | ```js 156 | locker.put('someKey', function() { 157 | var obj = { foo: 'bar', bar: 'baz' }; 158 | // some other logic 159 | return obj; 160 | }); 161 | ``` 162 | 163 | The current value will be passed into the function so you can perform logic on the current value, before returning it. e.g. 164 | 165 | ```js 166 | locker.put('someKey', ['foo', 'bar']); 167 | 168 | locker.put('someKey', function(current) { 169 | current.push('baz'); 170 | 171 | return current; 172 | }); 173 | 174 | locker.get('someKey'); // = ['foo', 'bar', 'baz'] 175 | ``` 176 | 177 | If the current value is not already set then you can pass a third parameter as a default that will be returned instead. e.g. 178 | 179 | ```js 180 | // given locker.get('foo') is not defined 181 | locker.put('foo', function (current) { 182 | // current will equal 'bar' 183 | }, 'bar'); 184 | ``` 185 | 186 |

adding multiple items at once by passing a single object

187 | 188 | This will add each key/value pair as a **separate** item in storage 189 | 190 | ```js 191 | locker.put({ 192 | someKey: 'johndoe', 193 | anotherKey: ['some', 'random', 'array'], 194 | boolKey: true 195 | }); 196 | ``` 197 | 198 |

adding via key function param

199 | 200 | Inserts each item from the returned Object, similar to above 201 | 202 | ```js 203 | locker.put(function() { 204 | // some logic 205 | return { 206 | foo: ['lorem', 'ipsum', 'dolor'], 207 | user: { 208 | username: 'johndoe', 209 | displayName: 'Johnny Doe', 210 | active: true, 211 | role: 'user' 212 | } 213 | }; 214 | }); 215 | ``` 216 | 217 |

conditionally adding an item if it doesn't already exist

218 | 219 | For this functionality you can use the `add()` method. 220 | 221 | If the key already exists then no action will be taken and `false` will be returned 222 | 223 | ```js 224 | locker.add('someKey', 'someVal'); // true or false - whether the item was added or not 225 | ``` 226 | 227 | ---------------------------- 228 | 229 |

Retrieving items from locker

230 | 231 | ```js 232 | // locker.put('fooArray', ['bar', 'baz', 'bob']); 233 | 234 | locker.get('fooArray'); // ['bar', 'baz', 'bob'] 235 | ``` 236 | 237 |

setting a default value

238 | 239 | if the key does not exist then, if specified the default will be returned 240 | 241 | ```js 242 | locker.get('keyDoesNotExist', 'a default value'); // 'a default value' 243 | ``` 244 | 245 |

retrieving multiple items at once

246 | 247 | You may pass an array to the `get()` method to return an Object containing the specified keys (if they exist) 248 | 249 | ```js 250 | locker.get(['someKey', 'anotherKey', 'foo']); 251 | 252 | // will return something like... 253 | { 254 | someKey: 'someValue', 255 | anotherKey: true, 256 | foo: 'bar' 257 | } 258 | ``` 259 | 260 |

deleting afterwards

261 | 262 | You can also retrieve an item and then delete it via the `pull()` method 263 | 264 | ```js 265 | // locker.put('someKey', { foo: 'bar', baz: 'bob' }); 266 | 267 | locker.pull('someKey', 'defaultVal'); // { foo: 'bar', baz: 'bob' } 268 | 269 | // then... 270 | 271 | locker.get('someKey', 'defaultVal'); // 'defaultVal' 272 | ``` 273 | 274 |

all items

275 | 276 | You can retrieve all items within the current namespace 277 | 278 | This will return an object containing all the key/value pairs in storage 279 | 280 | ```js 281 | locker.all(); 282 | // or 283 | locker.namespace('somethingElse').all(); 284 | ``` 285 | 286 |

counting items

287 | 288 | To count the number of items within a given namespace: 289 | 290 | ```js 291 | locker.count(); 292 | // or 293 | locker.namespace('somethingElse').count(); 294 | ``` 295 | 296 | ---------------------------- 297 | 298 |

Checking item exists in locker

299 | 300 | You can determine whether an item exists in the current namespace via 301 | 302 | ```js 303 | locker.has('someKey'); // true or false 304 | // or 305 | locker.namespace('foo').has('bar'); 306 | 307 | // e.g. 308 | if (locker.has('user.authToken') ) { 309 | // we're logged in 310 | } else { 311 | // go to login page or something 312 | } 313 | ``` 314 | 315 | ---------------------------- 316 | 317 |

Removing items from locker

318 | 319 | The simplest way to remove an item is to pass the key to the `forget()` method 320 | 321 | ```js 322 | locker.forget('keyToRemove'); 323 | // or 324 | locker.driver('session').forget('sessionKey'); 325 | // etc.. 326 | ``` 327 | 328 |

removing multiple items at once

329 | 330 | You can also pass an array. 331 | 332 | ```js 333 | locker.forget(['keyToRemove', 'anotherKeyToRemove', 'something', 'else']); 334 | ``` 335 | 336 |

removing all within namespace

337 | 338 | you can remove all the items within the currently set namespace via the `clean()` method 339 | 340 | ```js 341 | locker.clean(); 342 | // or 343 | locker.namespace('someOtherNamespace').clean(); 344 | ``` 345 |

removing all items within the currently set storage driver

346 | 347 | ```js 348 | locker.empty(); 349 | ``` 350 | 351 | ---------------------------- 352 | 353 |

Events

354 | 355 | There are 3 events that can be fired during various operations, these are: 356 | 357 | ```js 358 | // fired when a new item is added to storage 359 | $rootScope.$on('locker.item.added', function (e, payload) { 360 | // payload is equal to: 361 | { 362 | driver: 'local', // the driver that was set when the event was fired 363 | namespace: 'locker', // the namespace that was set when the event was fired 364 | key: 'foo', // the key that was added 365 | value: 'bar' // the value that was added 366 | } 367 | }); 368 | ``` 369 | 370 | ```js 371 | // fired when an item is removed from storage 372 | $rootScope.$on('locker.item.forgotten', function (e, payload) { 373 | // payload is equal to: 374 | { 375 | driver: 'local', // the driver that was set when the event was fired 376 | namespace: 'locker', // the namespace that was set when the event was fired 377 | key: 'foo', // the key that was removed 378 | } 379 | }); 380 | ``` 381 | 382 | ```js 383 | // fired when an item's value changes to something new 384 | $rootScope.$on('locker.item.updated', function (e, payload) { 385 | // payload is equal to: 386 | { 387 | driver: 'local', // the driver that was set when the event was fired 388 | namespace: 'locker', // the namespace that was set when the event was fired 389 | key: 'foo', // the key that was updated 390 | oldValue: 'bar', // the value that was set before the item was updated 391 | newValue: 'baz' // the new value that the item was updated to 392 | } 393 | }); 394 | ``` 395 | 396 | ---------------------------- 397 | 398 |

Binding to a $scope property

399 | 400 | You can bind a scope property to a key in storage. Whenever the $scope value changes, it will automatically be persisted in storage. e.g. 401 | 402 | ```js 403 | app.controller('AppCtrl', ['$scope', function ($scope) { 404 | 405 | locker.bind($scope, 'foo'); 406 | 407 | $scope.foo = ['bar', 'baz']; 408 | 409 | locker.get('foo'); // = ['bar', 'baz'] 410 | 411 | }]); 412 | ``` 413 | 414 | You can also set a default value via the third parameter: 415 | 416 | ```js 417 | app.controller('AppCtrl', ['$scope', function ($scope) { 418 | 419 | locker.bind($scope, 'foo', 'someDefault'); 420 | 421 | $scope.foo; // = 'someDefault' 422 | 423 | locker.get('foo'); // = 'someDefault' 424 | 425 | }]); 426 | ``` 427 | 428 | To unbind the $scope property, simply use the unbind method: 429 | 430 | 431 | ```js 432 | app.controller('AppCtrl', ['$scope', function ($scope) { 433 | 434 | locker.unbind($scope, 'foo'); 435 | 436 | $scope.foo; // = undefined 437 | 438 | locker.get('foo'); // = undefined 439 | 440 | }]); 441 | ``` 442 | 443 | ---------------------------- 444 | 445 | 446 |

Browser Compatibility

447 | 448 | IE8 is not supported because I am utilising `Object.keys()` 449 | 450 | To check if the browser natively supports local and session storage, you can do the following: 451 | 452 | ```js 453 | if (! locker.supported()) { 454 | // load a polyfill? 455 | } 456 | ``` 457 | 458 | I would recommend using [Remy's Storage polyfill](https://gist.github.com/remy/350433) if you want to support older browsers. 459 | 460 | For the latest browser compatibility chart see [HERE](http://caniuse.com/namevalue-storage) 461 | 462 |

Contributing

463 | 464 | Take care to maintain the existing coding style using the provided `.jscsrc` file. Add unit tests for any new or changed functionality. Lint and test your code using Gulp. 465 | 466 |

Development

467 | 468 | ```bash 469 | $ npm install 470 | $ gulp 471 | ``` 472 | 473 |

License

474 | 475 | The MIT License (MIT) 476 | 477 | Copyright (c) 2014 Sean Tymon 478 | 479 | Permission is hereby granted, free of charge, to any person obtaining a copy 480 | of this software and associated documentation files (the "Software"), to deal 481 | in the Software without restriction, including without limitation the rights 482 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 483 | copies of the Software, and to permit persons to whom the Software is 484 | furnished to do so, subject to the following conditions: 485 | 486 | The above copyright notice and this permission notice shall be included in all 487 | copies or substantial portions of the Software. 488 | 489 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 490 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 491 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 492 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 493 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 494 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 495 | SOFTWARE. 496 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-locker", 3 | "version": "2.0.5", 4 | "homepage": "https://github.com/tymondesigns/angular-locker", 5 | "authors": [ 6 | "Sean Tymon " 7 | ], 8 | "description": "A simple & configurable abstraction for local/session storage in angular projects.", 9 | "main": "dist/angular-locker.js", 10 | "keywords": [ 11 | "angular-locker", 12 | "angular", 13 | "locker", 14 | "storage", 15 | "localStorage", 16 | "sessionStorage", 17 | "session", 18 | "local" 19 | ], 20 | "dependencies": { 21 | "angular": ">=1.2.0 <2.0.0" 22 | }, 23 | "devDependencies": { 24 | "angular-mocks": ">=1.2.0 <2.0.0" 25 | }, 26 | "license": "MIT", 27 | "ignore": [ 28 | "**/.*", 29 | "node_modules", 30 | "bower_components", 31 | "test" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /changes.md: -------------------------------------------------------------------------------- 1 | ### 2.0.5 2 | 3 | ##### Fixes 4 | 5 | - Fix compatibility with latest Chrome #38 6 | 7 | ### 2.0.4 8 | 9 | ##### Fixes 10 | 11 | - Fix Webpack support #30 12 | 13 | ### 2.0.3 14 | 15 | ##### General / Improvements 16 | 17 | - adding better Browserify & Webpack support 18 | 19 | ### 2.0.2 20 | 21 | ##### Fixes 22 | 23 | - Fixed issue with multiple sequential calls to same driver - see #22 24 | 25 | ##### General / Improvements 26 | 27 | - improving docblocks, to work with jsdoc 28 | 29 | ### 2.0.1 30 | 31 | ##### Fixes 32 | 33 | - Improving driver support checking - see #18 34 | 35 | ##### General / Improvements 36 | 37 | - Gulpfile now uses ES6 goodness via babel :) 38 | - Removing needless bower dev dependencies from project 39 | 40 | ### 2.0.0 41 | 42 | ##### Breaking Changes 43 | 44 | - Changed the way config is set via `lockerProvider` e.g. 45 | ```js 46 | lockerProvider.defaults({ 47 | driver: 'session', 48 | namespace: 'myApp', 49 | separator: '.', 50 | eventsEnabled: true, 51 | extend: {} 52 | }); 53 | ``` 54 | 55 | ##### General 56 | 57 | - Added ability to extend locker at the config stage 58 | - Added `keys()` method to return an array of keys that exist within the current driver/namespace 59 | - Reduced size of minified file by removing *now* unnecessary functions 60 | - Adding third default parameter to `put()` method 61 | - Hugely refactored and simplified Gulp build process 62 | - Added [jscs](http://jscs.info/) to enforce coding style 63 | - Namespaces can now contain the separator without any issues 64 | - Lots of micro optimisations 65 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default { 4 | paths: { 5 | output: 'dist', 6 | vendor: [ 7 | 'node_modules/angular/angular.js', 8 | 'node_modules/angular-mocks/angular-mocks.js' 9 | ], 10 | scripts: [ 11 | 'src/angular-locker.js' 12 | ], 13 | test: [ 14 | 'test/mock/storageMock.js', 15 | 'test/spec/**/*.js' 16 | ], 17 | versions: [ 18 | './bower.json', 19 | './package.json' 20 | ], 21 | karma: 'test/karma.conf.js', 22 | gitdown: { 23 | src: '.gitdown/README.md', 24 | dest: 'README.md', 25 | glob: '.gitdown/**/*.md' 26 | } 27 | }, 28 | banner: '/*! <%= pkg.name %> v<%= pkg.version %> | (c) ${new Date().getFullYear()} <%= pkg.author %> | <%= pkg.homepage %> */\n' 29 | }; 30 | -------------------------------------------------------------------------------- /dist/angular-locker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * angular-locker 3 | * 4 | * A simple & configurable abstraction for local/session storage in angular projects. 5 | * 6 | * @link https://github.com/tymondesigns/angular-locker 7 | * @author Sean Tymon @tymondesigns 8 | * @license MIT License, http://www.opensource.org/licenses/MIT 9 | */ 10 | 11 | (function (root, factory) { 12 | if (typeof define === 'function' && define.amd) { 13 | define(function () { 14 | return factory(root.angular || (window && window.angular)); 15 | }); 16 | } else if (typeof exports === 'object') { 17 | module.exports = factory(root.angular || (window && window.angular)); 18 | } else { 19 | factory(root.angular); 20 | } 21 | })(this, function (angular) { 22 | 23 | 'use strict'; 24 | 25 | return angular.module('angular-locker', []) 26 | 27 | .provider('locker', function () { 28 | 29 | /** 30 | * If value is a function then execute, otherwise return 31 | * 32 | * @private 33 | * 34 | * @param {Mixed} value The value to execute or return 35 | * @param {Mixed} param The parameter to pass to function if applicable 36 | * 37 | * @return {Mixed} 38 | */ 39 | var _value = function (value, param) { 40 | return angular.isFunction(value) ? value(param) : value; 41 | }; 42 | 43 | /** 44 | * Determine whether a value is defined and not null 45 | * 46 | * @private 47 | * 48 | * @param {Mixed} value The value to check 49 | * 50 | * @return {Boolean} 51 | */ 52 | var _defined = function (value) { 53 | return angular.isDefined(value) && value !== null; 54 | }; 55 | 56 | /** 57 | * Trigger an error 58 | * 59 | * @private 60 | * @throws {Error} 61 | * 62 | * @param {String} msg The error message 63 | */ 64 | var _error = function (msg) { 65 | throw new Error('[angular-locker] ' + msg); 66 | }; 67 | 68 | /** 69 | * Set the defaults 70 | * 71 | * @private 72 | * 73 | * @type {Object} 74 | */ 75 | var defaults = { 76 | driver: 'local', 77 | namespace: 'locker', 78 | eventsEnabled: true, 79 | separator: '.', 80 | extend: {} 81 | }; 82 | 83 | return { 84 | 85 | /** 86 | * Allow the defaults to be specified via the `lockerProvider` 87 | * 88 | * @param {Object} value The defaults to override 89 | */ 90 | defaults: function (value) { 91 | if (! _defined(value)) return defaults; 92 | 93 | angular.forEach(value, function (val, key) { 94 | if (defaults.hasOwnProperty(key)) defaults[key] = val; 95 | }); 96 | }, 97 | 98 | /** 99 | * The locker service 100 | */ 101 | $get: ['$window', '$rootScope', '$parse', function ($window, $rootScope, $parse) { 102 | 103 | /** 104 | * Define the Locker class 105 | * 106 | * @public 107 | * @constructor 108 | * 109 | * @param {Object} options The config options to initialize with 110 | */ 111 | function Locker (options) { 112 | 113 | /** 114 | * The config options 115 | * 116 | * @private 117 | * 118 | * @type {Object} 119 | */ 120 | this._options = options; 121 | 122 | /** 123 | * Out of the box drivers 124 | * 125 | * @private 126 | * 127 | * @type {Object} 128 | */ 129 | this._registeredDrivers = angular.extend({ 130 | local: $window.localStorage, 131 | session: $window.sessionStorage 132 | }, options.extend); 133 | 134 | /** 135 | * Get the Storage instance from the key 136 | * 137 | * @private 138 | * 139 | * @param {String} driver The storage driver identifier 140 | * 141 | * @return {Storage} 142 | */ 143 | this._resolveDriver = function (driver) { 144 | if (! this._registeredDrivers.hasOwnProperty(driver)) { 145 | _error('The driver "' + driver + '" was not found.'); 146 | } 147 | 148 | return this._registeredDrivers[driver]; 149 | }; 150 | 151 | /** 152 | * The driver instance 153 | * 154 | * @private 155 | * 156 | * @type {Storage} 157 | */ 158 | this._driver = this._resolveDriver(options.driver); 159 | 160 | /** 161 | * The namespace value 162 | * 163 | * @private 164 | * 165 | * @type {String} 166 | */ 167 | this._namespace = options.namespace; 168 | 169 | /** 170 | * Separates the namespace from the keys 171 | * 172 | * @private 173 | * 174 | * @type {String} 175 | */ 176 | this._separator = options.separator; 177 | 178 | /** 179 | * Store the watchers here so we can un-register them later 180 | * 181 | * @private 182 | * 183 | * @type {Object} 184 | */ 185 | this._watchers = {}; 186 | 187 | /** 188 | * Check browser support 189 | * 190 | * @private 191 | * @see github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js#L38-L47 192 | * 193 | * @param {String} driver The driver to check support with 194 | * 195 | * @return {Boolean} 196 | */ 197 | this._checkSupport = function (driver) { 198 | if (! _defined(this._supported)) { 199 | var l = 'l'; 200 | try { 201 | this._resolveDriver(driver || options.driver).setItem(l, l); 202 | this._resolveDriver(driver || options.driver).removeItem(l); 203 | this._supported = true; 204 | } catch (e) { 205 | this._supported = false; 206 | } 207 | } 208 | 209 | return this._supported; 210 | }; 211 | 212 | /** 213 | * Build the storage key from the namspace 214 | * 215 | * @private 216 | * 217 | * @param {String} key The key to build the prefix with 218 | * 219 | * @return {String} 220 | */ 221 | this._getPrefix = function (key) { 222 | if (! this._namespace) return key; 223 | 224 | return this._namespace + this._separator + key; 225 | }; 226 | 227 | /** 228 | * Try to encode value as json, or just return the value upon failure 229 | * 230 | * @private 231 | * 232 | * @param {Mixed} value The value to serialize 233 | * 234 | * @return {Mixed} 235 | */ 236 | this._serialize = function (value) { 237 | try { 238 | return angular.toJson(value); 239 | } catch (e) { 240 | return value; 241 | } 242 | }; 243 | 244 | /** 245 | * Try to parse value as json, if it fails then it probably isn't json 246 | * so just return it 247 | * 248 | * @private 249 | * 250 | * @param {String} value The value to unserialize 251 | * 252 | * @return {Mixed} 253 | */ 254 | this._unserialize = function (value) { 255 | try { 256 | return angular.fromJson(value); 257 | } catch (e) { 258 | return value; 259 | } 260 | }; 261 | 262 | /** 263 | * Trigger an event 264 | * 265 | * @private 266 | * 267 | * @param {String} name The name of the event to trigger 268 | * @param {Object} payload The data to pass along with event 269 | */ 270 | this._event = function (name, payload) { 271 | if (this._options.eventsEnabled) { 272 | $rootScope.$emit('locker.' + name, angular.extend(payload, { 273 | driver: this._options.driver, 274 | namespace: this._namespace, 275 | })); 276 | } 277 | }; 278 | 279 | /** 280 | * Add to storage 281 | * 282 | * @private 283 | * @throws {Error} if browser support fails 284 | * 285 | * @param {String} key The key to add 286 | * @param {Mixed} value The value to add 287 | */ 288 | this._setItem = function (key, value) { 289 | if (! this._checkSupport()) { 290 | _error('The browser does not support the "' + options.driver + '" driver'); 291 | } 292 | 293 | try { 294 | var oldVal = this._getItem(key); 295 | this._driver.setItem(this._getPrefix(key), this._serialize(value)); 296 | if (this._exists(key) && ! angular.equals(oldVal, value)) { 297 | this._event('item.updated', { key: key, oldValue: oldVal, newValue: value }); 298 | } else { 299 | this._event('item.added', { key: key, value: value }); 300 | } 301 | } catch (e) { 302 | if (['QUOTA_EXCEEDED_ERR', 303 | 'NS_ERROR_DOM_QUOTA_REACHED', 304 | 'QuotaExceededError'].indexOf(e.name) !== -1) { 305 | _error('The browser storage quota has been exceeded'); 306 | } else { 307 | _error('Could not add item with key "' + key + '"'); 308 | } 309 | } 310 | }; 311 | 312 | /** 313 | * Get from storage 314 | * 315 | * @private 316 | * @throws {Error} if browser support fails 317 | * 318 | * @param {String} key The key to get 319 | * 320 | * @return {Mixed} 321 | */ 322 | this._getItem = function (key) { 323 | if (! this._checkSupport()) { 324 | _error('The browser does not support the "' + options.driver + '" driver'); 325 | } 326 | 327 | return this._unserialize(this._driver.getItem(this._getPrefix(key))); 328 | }; 329 | 330 | /** 331 | * Exists in storage 332 | * 333 | * @private 334 | * @throws {Error} if browser support fails 335 | * 336 | * @param {String} key The key to check for existence 337 | * 338 | * @return {Boolean} 339 | */ 340 | this._exists = function (key) { 341 | if (! this._checkSupport()) { 342 | _error('The browser does not support the "' + options.driver + '" driver'); 343 | } 344 | 345 | return this._driver.hasOwnProperty(this._getPrefix(_value(key))) || !! this._getItem(key); 346 | }; 347 | 348 | /** 349 | * Remove from storage 350 | * 351 | * @private 352 | * @throws {Error} if browser support fails 353 | * 354 | * @param {String} key The key to remove 355 | * 356 | * @return {Boolean} 357 | */ 358 | this._removeItem = function (key) { 359 | if (! this._checkSupport()) { 360 | _error('The browser does not support the "' + options.driver + '" driver'); 361 | } 362 | 363 | if (! this._exists(key)) return false; 364 | 365 | this._driver.removeItem(this._getPrefix(key)); 366 | this._event('item.forgotten', { key: key }); 367 | 368 | return true; 369 | }; 370 | } 371 | 372 | /** 373 | * Define the public api 374 | * 375 | * @public 376 | * 377 | * @type {Object} 378 | */ 379 | Locker.prototype = { 380 | 381 | /** 382 | * Add a new item to storage (even if it already exists) 383 | * 384 | * @public 385 | * 386 | * @param {Mixed} key The key to add 387 | * @param {Mixed} value The value to add 388 | * @param {Mixed} def The default to pass to function if doesn't already exist 389 | * 390 | * @return {Locker|Boolean} 391 | */ 392 | put: function (key, value, def) { 393 | if (! _defined(key)) return false; 394 | key = _value(key); 395 | 396 | if (angular.isObject(key)) { 397 | angular.forEach(key, function (value, key) { 398 | this._setItem(key, _defined(value) ? value : def); 399 | }, this); 400 | } else { 401 | if (! _defined(value)) return false; 402 | var val = this._getItem(key); 403 | this._setItem(key, _value(value, _defined(val) ? val : def)); 404 | } 405 | 406 | return this; 407 | }, 408 | 409 | /** 410 | * Add an item to storage if it doesn't already exist 411 | * 412 | * @public 413 | * 414 | * @param {Mixed} key The key to add 415 | * @param {Mixed} value The value to add 416 | * @param {Mixed} def The default to pass to function if doesn't already exist 417 | * 418 | * @return {Boolean} 419 | */ 420 | add: function (key, value, def) { 421 | if (! this.has(key)) { 422 | this.put(key, value, def); 423 | return true; 424 | } 425 | 426 | return false; 427 | }, 428 | 429 | /** 430 | * Retrieve the specified item from storage 431 | * 432 | * @public 433 | * 434 | * @param {String|Array} key The key to get 435 | * @param {Mixed} def The default value if it does not exist 436 | * 437 | * @return {Mixed} 438 | */ 439 | get: function (key, def) { 440 | if (angular.isArray(key)) { 441 | var items = {}; 442 | angular.forEach(key, function (k) { 443 | if (this.has(k)) items[k] = this._getItem(k); 444 | }, this); 445 | 446 | return items; 447 | } 448 | 449 | if (! this.has(key)) return arguments.length === 2 ? def : void 0; 450 | 451 | return this._getItem(key); 452 | }, 453 | 454 | /** 455 | * Determine whether the item exists in storage 456 | * 457 | * @public 458 | * 459 | * @param {String|Function} key - The key to remove 460 | * 461 | * @return {Boolean} 462 | */ 463 | has: function (key) { 464 | return this._exists(key); 465 | }, 466 | 467 | /** 468 | * Remove specified item(s) from storage 469 | * 470 | * @public 471 | * 472 | * @param {String|Array} key The key or array of keys to remove 473 | * 474 | * @return {Object} 475 | */ 476 | forget: function (key) { 477 | key = _value(key); 478 | 479 | if (angular.isArray(key)) { 480 | key.map(this._removeItem, this); 481 | } else { 482 | this._removeItem(key); 483 | } 484 | 485 | return this; 486 | }, 487 | 488 | /** 489 | * Retrieve the specified item from storage and then remove it 490 | * 491 | * @public 492 | * 493 | * @param {String|Array} key The key to pull from storage 494 | * @param {Mixed} def The default value if it does not exist 495 | * 496 | * @return {Mixed} 497 | */ 498 | pull: function (key, def) { 499 | var value = this.get(key, def); 500 | this.forget(key); 501 | 502 | return value; 503 | }, 504 | 505 | /** 506 | * Return all items in storage within the current namespace/driver 507 | * 508 | * @public 509 | * 510 | * @return {Object} 511 | */ 512 | all: function () { 513 | var items = {}; 514 | angular.forEach(this._driver, function (value, key) { 515 | if (this._namespace) { 516 | var prefix = this._namespace + this._separator; 517 | if (key.indexOf(prefix) === 0) key = key.substring(prefix.length); 518 | } 519 | if (this.has(key)) items[key] = this.get(key); 520 | }, this); 521 | 522 | return items; 523 | }, 524 | 525 | /** 526 | * Get the storage keys as an array 527 | * 528 | * @public 529 | * 530 | * @return {Array} 531 | */ 532 | keys: function () { 533 | return Object.keys(this.all()); 534 | }, 535 | 536 | /** 537 | * Remove all items set within the current namespace/driver 538 | * 539 | * @public 540 | * 541 | * @return {Locker} 542 | */ 543 | clean: function () { 544 | return this.forget(this.keys()); 545 | }, 546 | 547 | /** 548 | * Empty the current storage driver completely. careful now. 549 | * 550 | * @public 551 | * 552 | * @return {Locker} 553 | */ 554 | empty: function () { 555 | this._driver.clear(); 556 | 557 | return this; 558 | }, 559 | 560 | /** 561 | * Get the total number of items within the current namespace 562 | * 563 | * @public 564 | * 565 | * @return {Integer} 566 | */ 567 | count: function () { 568 | return this.keys().length; 569 | }, 570 | 571 | /** 572 | * Bind a storage key to a $scope property 573 | * 574 | * @public 575 | * 576 | * @param {Object} $scope The angular $scope object 577 | * @param {String} key The key in storage to bind to 578 | * @param {Mixed} def The default value to initially bind 579 | * 580 | * @return {Locker} 581 | */ 582 | bind: function ($scope, key, def) { 583 | if (! _defined( $scope.$eval(key) )) { 584 | $parse(key).assign($scope, this.get(key, def)); 585 | this.add(key, def); 586 | } 587 | 588 | var self = this; 589 | this._watchers[key + $scope.$id] = $scope.$watch(key, function (newVal) { 590 | self.put(key, newVal); 591 | }, angular.isObject($scope[key])); 592 | 593 | return this; 594 | }, 595 | 596 | /** 597 | * Unbind a storage key from a $scope property 598 | * 599 | * @public 600 | * 601 | * @param {Object} $scope The angular $scope object 602 | * @param {String} key The key to remove from bindings 603 | * 604 | * @return {Locker} 605 | */ 606 | unbind: function ($scope, key) { 607 | $parse(key).assign($scope, void 0); 608 | this.forget(key); 609 | 610 | var watchId = key + $scope.$id; 611 | 612 | if (this._watchers[watchId]) { 613 | // execute the de-registration function 614 | this._watchers[watchId](); 615 | delete this._watchers[watchId]; 616 | } 617 | 618 | return this; 619 | }, 620 | 621 | /** 622 | * Set the storage driver on a new instance to enable overriding defaults 623 | * 624 | * @public 625 | * 626 | * @param {String} driver The driver to switch to 627 | * 628 | * @return {Locker} 629 | */ 630 | driver: function (driver) { 631 | return this.instance(angular.extend(this._options, { driver: driver })); 632 | }, 633 | 634 | /** 635 | * Get the currently set driver 636 | * 637 | * @public 638 | * 639 | * @return {Storage} 640 | */ 641 | getDriver: function () { 642 | return this._driver; 643 | }, 644 | 645 | /** 646 | * Set the namespace on a new instance to enable overriding defaults 647 | * 648 | * @public 649 | * 650 | * @param {String} namespace The namespace to switch to 651 | * 652 | * @return {Locker} 653 | */ 654 | namespace: function (namespace) { 655 | return this.instance(angular.extend(this._options, { namespace: namespace })); 656 | }, 657 | 658 | /** 659 | * Get the currently set namespace 660 | * 661 | * @public 662 | * 663 | * @return {String} 664 | */ 665 | getNamespace: function () { 666 | return this._namespace; 667 | }, 668 | 669 | /** 670 | * Check browser support 671 | * 672 | * @public 673 | * @see github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js#L38-L47 674 | * 675 | * @param {String} driver The driver to check support with 676 | * 677 | * @return {Boolean} 678 | */ 679 | supported: function (driver) { 680 | return this._checkSupport(driver); 681 | }, 682 | 683 | /** 684 | * Get a new instance of Locker 685 | * 686 | * @public 687 | * 688 | * @param {Object} options The config options to instantiate with 689 | * 690 | * @return {Locker} 691 | */ 692 | instance: function (options) { 693 | return new Locker(options); 694 | } 695 | }; 696 | 697 | // return the default instance 698 | return new Locker(defaults); 699 | }] 700 | }; 701 | 702 | }).name; // export module name for the likes of Browserify and Webpack 703 | 704 | }); 705 | -------------------------------------------------------------------------------- /dist/angular-locker.min.js: -------------------------------------------------------------------------------- 1 | /*! angular-locker v2.0.5 | (c) 2017 @tymondesigns | https://github.com/tymondesigns/angular-locker */ 2 | !function(t,e){"function"==typeof define&&define.amd?define(function(){return e(t.angular||window&&window.angular)}):"object"==typeof exports?module.exports=e(t.angular||window&&window.angular):e(t.angular)}(this,function(t){"use strict";return t.module("angular-locker",[]).provider("locker",function(){var e=function(e,r){return t.isFunction(e)?e(r):e},r=function(e){return t.isDefined(e)&&null!==e},i=function(t){throw new Error("[angular-locker] "+t)},s={driver:"local",namespace:"locker",eventsEnabled:!0,separator:".",extend:{}};return{defaults:function(e){return r(e)?void t.forEach(e,function(t,e){s.hasOwnProperty(e)&&(s[e]=t)}):s},$get:["$window","$rootScope","$parse",function(n,o,h){function u(s){this._options=s,this._registeredDrivers=t.extend({local:n.localStorage,session:n.sessionStorage},s.extend),this._resolveDriver=function(t){return this._registeredDrivers.hasOwnProperty(t)||i('The driver "'+t+'" was not found.'),this._registeredDrivers[t]},this._driver=this._resolveDriver(s.driver),this._namespace=s.namespace,this._separator=s.separator,this._watchers={},this._checkSupport=function(t){if(!r(this._supported)){var e="l";try{this._resolveDriver(t||s.driver).setItem(e,e),this._resolveDriver(t||s.driver).removeItem(e),this._supported=!0}catch(i){this._supported=!1}}return this._supported},this._getPrefix=function(t){return this._namespace?this._namespace+this._separator+t:t},this._serialize=function(e){try{return t.toJson(e)}catch(r){return e}},this._unserialize=function(e){try{return t.fromJson(e)}catch(r){return e}},this._event=function(e,r){this._options.eventsEnabled&&o.$emit("locker."+e,t.extend(r,{driver:this._options.driver,namespace:this._namespace}))},this._setItem=function(e,r){this._checkSupport()||i('The browser does not support the "'+s.driver+'" driver');try{var n=this._getItem(e);this._driver.setItem(this._getPrefix(e),this._serialize(r)),this._exists(e)&&!t.equals(n,r)?this._event("item.updated",{key:e,oldValue:n,newValue:r}):this._event("item.added",{key:e,value:r})}catch(o){i(-1!==["QUOTA_EXCEEDED_ERR","NS_ERROR_DOM_QUOTA_REACHED","QuotaExceededError"].indexOf(o.name)?"The browser storage quota has been exceeded":'Could not add item with key "'+e+'"')}},this._getItem=function(t){return this._checkSupport()||i('The browser does not support the "'+s.driver+'" driver'),this._unserialize(this._driver.getItem(this._getPrefix(t)))},this._exists=function(t){return this._checkSupport()||i('The browser does not support the "'+s.driver+'" driver'),this._driver.hasOwnProperty(this._getPrefix(e(t)))||!!this._getItem(t)},this._removeItem=function(t){return this._checkSupport()||i('The browser does not support the "'+s.driver+'" driver'),this._exists(t)?(this._driver.removeItem(this._getPrefix(t)),this._event("item.forgotten",{key:t}),!0):!1}}return u.prototype={put:function(i,s,n){if(!r(i))return!1;if(i=e(i),t.isObject(i))t.forEach(i,function(t,e){this._setItem(e,r(t)?t:n)},this);else{if(!r(s))return!1;var o=this._getItem(i);this._setItem(i,e(s,r(o)?o:n))}return this},add:function(t,e,r){return this.has(t)?!1:(this.put(t,e,r),!0)},get:function(e,r){if(t.isArray(e)){var i={};return t.forEach(e,function(t){this.has(t)&&(i[t]=this._getItem(t))},this),i}return this.has(e)?this._getItem(e):2===arguments.length?r:void 0},has:function(t){return this._exists(t)},forget:function(r){return r=e(r),t.isArray(r)?r.map(this._removeItem,this):this._removeItem(r),this},pull:function(t,e){var r=this.get(t,e);return this.forget(t),r},all:function(){var e={};return t.forEach(this._driver,function(t,r){if(this._namespace){var i=this._namespace+this._separator;0===r.indexOf(i)&&(r=r.substring(i.length))}this.has(r)&&(e[r]=this.get(r))},this),e},keys:function(){return Object.keys(this.all())},clean:function(){return this.forget(this.keys())},empty:function(){return this._driver.clear(),this},count:function(){return this.keys().length},bind:function(e,i,s){r(e.$eval(i))||(h(i).assign(e,this.get(i,s)),this.add(i,s));var n=this;return this._watchers[i+e.$id]=e.$watch(i,function(t){n.put(i,t)},t.isObject(e[i])),this},unbind:function(t,e){h(e).assign(t,void 0),this.forget(e);var r=e+t.$id;return this._watchers[r]&&(this._watchers[r](),delete this._watchers[r]),this},driver:function(e){return this.instance(t.extend(this._options,{driver:e}))},getDriver:function(){return this._driver},namespace:function(e){return this.instance(t.extend(this._options,{namespace:e}))},getNamespace:function(){return this._namespace},supported:function(t){return this._checkSupport(t)},instance:function(t){return new u(t)}},new u(s)}]}}).name}); 3 | //# sourceMappingURL=angular-locker.min.js.map 4 | -------------------------------------------------------------------------------- /dist/angular-locker.min.js.map: -------------------------------------------------------------------------------- 1 | /*! angular-locker v2.0.5 | (c) 2017 @tymondesigns | https://github.com/tymondesigns/angular-locker */ 2 | {"version":3,"sources":["angular-locker.min.js"],"names":["root","factory","define","amd","angular","window","exports","module","this","provider","_value","value","param","isFunction","_defined","isDefined","_error","msg","Error","defaults","driver","namespace","eventsEnabled","separator","extend","forEach","val","key","hasOwnProperty","$get","$window","$rootScope","$parse","Locker","options","_options","_registeredDrivers","local","localStorage","session","sessionStorage","_resolveDriver","_driver","_namespace","_separator","_watchers","_checkSupport","_supported","l","setItem","removeItem","e","_getPrefix","_serialize","toJson","_unserialize","fromJson","_event","name","payload","$emit","_setItem","oldVal","_getItem","_exists","equals","oldValue","newValue","indexOf","getItem","_removeItem","prototype","put","def","isObject","add","has","get","isArray","items","k","arguments","length","forget","map","pull","all","prefix","substring","keys","Object","clean","empty","clear","count","bind","$scope","$eval","assign","self","$id","$watch","newVal","unbind","watchId","instance","getDriver","getNamespace","supported"],"mappings":"CAUA,SAAWA,EAAMC,GACS,kBAAXC,SAAyBA,OAAOC,IACvCD,OAAO,WACH,MAAOD,GAAQD,EAAKI,SAAYC,QAAUA,OAAOD,WAE3B,gBAAZE,SACdC,OAAOD,QAAUL,EAAQD,EAAKI,SAAYC,QAAUA,OAAOD,SAE3DH,EAAQD,EAAKI,UAElBI,KAAM,SAAUJ,GAEf,YAEA,OAAOA,GAAQG,OAAO,qBAErBE,SAAS,SAAU,WAYhB,GAAIC,GAAS,SAAUC,EAAOC,GAC1B,MAAOR,GAAQS,WAAWF,GAASA,EAAMC,GAASD,GAYlDG,EAAW,SAAUH,GACrB,MAAOP,GAAQW,UAAUJ,IAAoB,OAAVA,GAWnCK,EAAS,SAAUC,GACnB,KAAM,IAAIC,OAAM,oBAAsBD,IAUtCE,GACAC,OAAQ,QACRC,UAAW,SACXC,eAAe,EACfC,UAAW,IACXC,UAGJ,QAOIL,SAAU,SAAUR,GAChB,MAAMG,GAASH,OAEfP,GAAQqB,QAAQd,EAAO,SAAUe,EAAKC,GAC9BR,EAASS,eAAeD,KAAMR,EAASQ,GAAOD,KAHxBP,GAUlCU,MAAO,UAAW,aAAc,SAAU,SAAUC,EAASC,EAAYC,GAUrE,QAASC,GAAQC,GASb1B,KAAK2B,SAAWD,EAShB1B,KAAK4B,mBAAqBhC,EAAQoB,QAC9Ba,MAAOP,EAAQQ,aACfC,QAAST,EAAQU,gBAClBN,EAAQV,QAWXhB,KAAKiC,eAAiB,SAAUrB,GAK5B,MAJMZ,MAAK4B,mBAAmBR,eAAeR,IACzCJ,EAAO,eAAiBI,EAAS,oBAG9BZ,KAAK4B,mBAAmBhB,IAUnCZ,KAAKkC,QAAUlC,KAAKiC,eAAeP,EAAQd,QAS3CZ,KAAKmC,WAAaT,EAAQb,UAS1Bb,KAAKoC,WAAaV,EAAQX,UAS1Bf,KAAKqC,aAYLrC,KAAKsC,cAAgB,SAAU1B,GAC3B,IAAMN,EAASN,KAAKuC,YAAa,CAC7B,GAAIC,GAAI,GACR,KACIxC,KAAKiC,eAAerB,GAAUc,EAAQd,QAAQ6B,QAAQD,EAAGA,GACzDxC,KAAKiC,eAAerB,GAAUc,EAAQd,QAAQ8B,WAAWF,GACzDxC,KAAKuC,YAAa,EACpB,MAAOI,GACL3C,KAAKuC,YAAa,GAI1B,MAAOvC,MAAKuC,YAYhBvC,KAAK4C,WAAa,SAAUzB,GACxB,MAAMnB,MAAKmC,WAEJnC,KAAKmC,WAAanC,KAAKoC,WAAajB,EAFbA,GAclCnB,KAAK6C,WAAa,SAAU1C,GACxB,IACI,MAAOP,GAAQkD,OAAO3C,GACxB,MAAOwC,GACL,MAAOxC,KAcfH,KAAK+C,aAAe,SAAU5C,GAC1B,IACI,MAAOP,GAAQoD,SAAS7C,GAC1B,MAAOwC,GACL,MAAOxC,KAYfH,KAAKiD,OAAS,SAAUC,EAAMC,GACtBnD,KAAK2B,SAASb,eACdS,EAAW6B,MAAM,UAAYF,EAAMtD,EAAQoB,OAAOmC,GAC9CvC,OAAQZ,KAAK2B,SAASf,OACtBC,UAAWb,KAAKmC,eAc5BnC,KAAKqD,SAAW,SAAUlC,EAAKhB,GACrBH,KAAKsC,iBACP9B,EAAO,qCAAuCkB,EAAQd,OAAS,WAGnE,KACI,GAAI0C,GAAStD,KAAKuD,SAASpC,EAC3BnB,MAAKkC,QAAQO,QAAQzC,KAAK4C,WAAWzB,GAAMnB,KAAK6C,WAAW1C,IACvDH,KAAKwD,QAAQrC,KAAUvB,EAAQ6D,OAAOH,EAAQnD,GAC9CH,KAAKiD,OAAO,gBAAkB9B,IAAKA,EAAKuC,SAAUJ,EAAQK,SAAUxD,IAEpEH,KAAKiD,OAAO,cAAgB9B,IAAKA,EAAKhB,MAAOA,IAEnD,MAAOwC,GAIDnC,EAD0C,MAFzC,qBACD,6BACA,sBAAsBoD,QAAQjB,EAAEO,MACzB,8CAEA,gCAAkC/B,EAAM,OAe3DnB,KAAKuD,SAAW,SAAUpC,GAKtB,MAJMnB,MAAKsC,iBACP9B,EAAO,qCAAuCkB,EAAQd,OAAS,YAG5DZ,KAAK+C,aAAa/C,KAAKkC,QAAQ2B,QAAQ7D,KAAK4C,WAAWzB,MAalEnB,KAAKwD,QAAU,SAAUrC,GAKrB,MAJMnB,MAAKsC,iBACP9B,EAAO,qCAAuCkB,EAAQd,OAAS,YAG5DZ,KAAKkC,QAAQd,eAAepB,KAAK4C,WAAW1C,EAAOiB,QAAanB,KAAKuD,SAASpC,IAazFnB,KAAK8D,YAAc,SAAU3C,GAKzB,MAJMnB,MAAKsC,iBACP9B,EAAO,qCAAuCkB,EAAQd,OAAS,YAG7DZ,KAAKwD,QAAQrC,IAEnBnB,KAAKkC,QAAQQ,WAAW1C,KAAK4C,WAAWzB,IACxCnB,KAAKiD,OAAO,kBAAoB9B,IAAKA,KAE9B,IALyB,GA+UxC,MA/TAM,GAAOsC,WAaHC,IAAK,SAAU7C,EAAKhB,EAAO8D,GACvB,IAAM3D,EAASa,GAAM,OAAO,CAG5B,IAFAA,EAAMjB,EAAOiB,GAETvB,EAAQsE,SAAS/C,GACjBvB,EAAQqB,QAAQE,EAAK,SAAUhB,EAAOgB,GAClCnB,KAAKqD,SAASlC,EAAKb,EAASH,GAASA,EAAQ8D,IAC9CjE,UACA,CACH,IAAMM,EAASH,GAAQ,OAAO,CAC9B,IAAIe,GAAMlB,KAAKuD,SAASpC,EACxBnB,MAAKqD,SAASlC,EAAKjB,EAAOC,EAAOG,EAASY,GAAOA,EAAM+C,IAG3D,MAAOjE,OAcXmE,IAAK,SAAUhD,EAAKhB,EAAO8D,GACvB,MAAMjE,MAAKoE,IAAIjD,IAKR,GAJHnB,KAAKgE,IAAI7C,EAAKhB,EAAO8D,IACd,IAgBfI,IAAK,SAAUlD,EAAK8C,GAChB,GAAIrE,EAAQ0E,QAAQnD,GAAM,CACtB,GAAIoD,KAKJ,OAJA3E,GAAQqB,QAAQE,EAAK,SAAUqD,GACvBxE,KAAKoE,IAAII,KAAID,EAAMC,GAAKxE,KAAKuD,SAASiB,KAC3CxE,MAEIuE,EAGX,MAAMvE,MAAKoE,IAAIjD,GAERnB,KAAKuD,SAASpC,GAF4B,IAArBsD,UAAUC,OAAeT,EAAM,QAc/DG,IAAK,SAAUjD,GACX,MAAOnB,MAAKwD,QAAQrC,IAYxBwD,OAAQ,SAAUxD,GASd,MARAA,GAAMjB,EAAOiB,GAETvB,EAAQ0E,QAAQnD,GAChBA,EAAIyD,IAAI5E,KAAK8D,YAAa9D,MAE1BA,KAAK8D,YAAY3C,GAGdnB,MAaX6E,KAAM,SAAU1D,EAAK8C,GACjB,GAAI9D,GAAQH,KAAKqE,IAAIlD,EAAK8C,EAG1B,OAFAjE,MAAK2E,OAAOxD,GAELhB,GAUX2E,IAAK,WACD,GAAIP,KASJ,OARA3E,GAAQqB,QAAQjB,KAAKkC,QAAS,SAAU/B,EAAOgB,GAC3C,GAAInB,KAAKmC,WAAY,CACjB,GAAI4C,GAAS/E,KAAKmC,WAAanC,KAAKoC,UACR,KAAxBjB,EAAIyC,QAAQmB,KAAe5D,EAAMA,EAAI6D,UAAUD,EAAOL,SAE1D1E,KAAKoE,IAAIjD,KAAMoD,EAAMpD,GAAOnB,KAAKqE,IAAIlD,KAC1CnB,MAEIuE,GAUXU,KAAM,WACF,MAAOC,QAAOD,KAAKjF,KAAK8E,QAU5BK,MAAO,WACH,MAAOnF,MAAK2E,OAAO3E,KAAKiF,SAU5BG,MAAO,WAGH,MAFApF,MAAKkC,QAAQmD,QAENrF,MAUXsF,MAAO,WACH,MAAOtF,MAAKiF,OAAOP,QAcvBa,KAAM,SAAUC,EAAQrE,EAAK8C,GACnB3D,EAAUkF,EAAOC,MAAMtE,MACzBK,EAAOL,GAAKuE,OAAOF,EAAQxF,KAAKqE,IAAIlD,EAAK8C,IACzCjE,KAAKmE,IAAIhD,EAAK8C,GAGlB,IAAI0B,GAAO3F,IAKX,OAJAA,MAAKqC,UAAUlB,EAAMqE,EAAOI,KAAOJ,EAAOK,OAAO1E,EAAK,SAAU2E,GAC5DH,EAAK3B,IAAI7C,EAAK2E,IACflG,EAAQsE,SAASsB,EAAOrE,KAEpBnB,MAaX+F,OAAQ,SAAUP,EAAQrE,GACtBK,EAAOL,GAAKuE,OAAOF,EAAQ,QAC3BxF,KAAK2E,OAAOxD,EAEZ,IAAI6E,GAAU7E,EAAMqE,EAAOI,GAQ3B,OANI5F,MAAKqC,UAAU2D,KAEfhG,KAAKqC,UAAU2D,WACRhG,MAAKqC,UAAU2D,IAGnBhG,MAYXY,OAAQ,SAAUA,GACd,MAAOZ,MAAKiG,SAASrG,EAAQoB,OAAOhB,KAAK2B,UAAYf,OAAQA,MAUjEsF,UAAW,WACP,MAAOlG,MAAKkC,SAYhBrB,UAAW,SAAUA,GACjB,MAAOb,MAAKiG,SAASrG,EAAQoB,OAAOhB,KAAK2B,UAAYd,UAAWA,MAUpEsF,aAAc,WACV,MAAOnG,MAAKmC,YAahBiE,UAAW,SAAUxF,GACjB,MAAOZ,MAAKsC,cAAc1B,IAY9BqF,SAAU,SAAUvE,GAChB,MAAO,IAAID,GAAOC,KAKnB,GAAID,GAAOd,QAI3BuC","file":"angular-locker.min.js","sourcesContent":["/**\n * angular-locker\n *\n * A simple & configurable abstraction for local/session storage in angular projects.\n *\n * @link https://github.com/tymondesigns/angular-locker\n * @author Sean Tymon @tymondesigns\n * @license MIT License, http://www.opensource.org/licenses/MIT\n */\n\n(function (root, factory) {\n if (typeof define === 'function' && define.amd) {\n define(function () {\n return factory(root.angular || (window && window.angular));\n });\n } else if (typeof exports === 'object') {\n module.exports = factory(root.angular || (window && window.angular));\n } else {\n factory(root.angular);\n }\n})(this, function (angular) {\n\n 'use strict';\n\n return angular.module('angular-locker', [])\n\n .provider('locker', function () {\n\n /**\n * If value is a function then execute, otherwise return\n *\n * @private\n *\n * @param {Mixed} value The value to execute or return\n * @param {Mixed} param The parameter to pass to function if applicable\n *\n * @return {Mixed}\n */\n var _value = function (value, param) {\n return angular.isFunction(value) ? value(param) : value;\n };\n\n /**\n * Determine whether a value is defined and not null\n *\n * @private\n *\n * @param {Mixed} value The value to check\n *\n * @return {Boolean}\n */\n var _defined = function (value) {\n return angular.isDefined(value) && value !== null;\n };\n\n /**\n * Trigger an error\n *\n * @private\n * @throws {Error}\n *\n * @param {String} msg The error message\n */\n var _error = function (msg) {\n throw new Error('[angular-locker] ' + msg);\n };\n\n /**\n * Set the defaults\n *\n * @private\n *\n * @type {Object}\n */\n var defaults = {\n driver: 'local',\n namespace: 'locker',\n eventsEnabled: true,\n separator: '.',\n extend: {}\n };\n\n return {\n\n /**\n * Allow the defaults to be specified via the `lockerProvider`\n *\n * @param {Object} value The defaults to override\n */\n defaults: function (value) {\n if (! _defined(value)) return defaults;\n\n angular.forEach(value, function (val, key) {\n if (defaults.hasOwnProperty(key)) defaults[key] = val;\n });\n },\n\n /**\n * The locker service\n */\n $get: ['$window', '$rootScope', '$parse', function ($window, $rootScope, $parse) {\n\n /**\n * Define the Locker class\n *\n * @public\n * @constructor\n *\n * @param {Object} options The config options to initialize with\n */\n function Locker (options) {\n\n /**\n * The config options\n *\n * @private\n *\n * @type {Object}\n */\n this._options = options;\n\n /**\n * Out of the box drivers\n *\n * @private\n *\n * @type {Object}\n */\n this._registeredDrivers = angular.extend({\n local: $window.localStorage,\n session: $window.sessionStorage\n }, options.extend);\n\n /**\n * Get the Storage instance from the key\n *\n * @private\n *\n * @param {String} driver The storage driver identifier\n *\n * @return {Storage}\n */\n this._resolveDriver = function (driver) {\n if (! this._registeredDrivers.hasOwnProperty(driver)) {\n _error('The driver \"' + driver + '\" was not found.');\n }\n\n return this._registeredDrivers[driver];\n };\n\n /**\n * The driver instance\n *\n * @private\n *\n * @type {Storage}\n */\n this._driver = this._resolveDriver(options.driver);\n\n /**\n * The namespace value\n *\n * @private\n *\n * @type {String}\n */\n this._namespace = options.namespace;\n\n /**\n * Separates the namespace from the keys\n *\n * @private\n *\n * @type {String}\n */\n this._separator = options.separator;\n\n /**\n * Store the watchers here so we can un-register them later\n *\n * @private\n *\n * @type {Object}\n */\n this._watchers = {};\n\n /**\n * Check browser support\n *\n * @private\n * @see github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js#L38-L47\n *\n * @param {String} driver The driver to check support with\n *\n * @return {Boolean}\n */\n this._checkSupport = function (driver) {\n if (! _defined(this._supported)) {\n var l = 'l';\n try {\n this._resolveDriver(driver || options.driver).setItem(l, l);\n this._resolveDriver(driver || options.driver).removeItem(l);\n this._supported = true;\n } catch (e) {\n this._supported = false;\n }\n }\n\n return this._supported;\n };\n\n /**\n * Build the storage key from the namspace\n *\n * @private\n *\n * @param {String} key The key to build the prefix with\n *\n * @return {String}\n */\n this._getPrefix = function (key) {\n if (! this._namespace) return key;\n\n return this._namespace + this._separator + key;\n };\n\n /**\n * Try to encode value as json, or just return the value upon failure\n *\n * @private\n *\n * @param {Mixed} value The value to serialize\n *\n * @return {Mixed}\n */\n this._serialize = function (value) {\n try {\n return angular.toJson(value);\n } catch (e) {\n return value;\n }\n };\n\n /**\n * Try to parse value as json, if it fails then it probably isn't json\n * so just return it\n *\n * @private\n *\n * @param {String} value The value to unserialize\n *\n * @return {Mixed}\n */\n this._unserialize = function (value) {\n try {\n return angular.fromJson(value);\n } catch (e) {\n return value;\n }\n };\n\n /**\n * Trigger an event\n *\n * @private\n *\n * @param {String} name The name of the event to trigger\n * @param {Object} payload The data to pass along with event\n */\n this._event = function (name, payload) {\n if (this._options.eventsEnabled) {\n $rootScope.$emit('locker.' + name, angular.extend(payload, {\n driver: this._options.driver,\n namespace: this._namespace,\n }));\n }\n };\n\n /**\n * Add to storage\n *\n * @private\n * @throws {Error} if browser support fails\n *\n * @param {String} key The key to add\n * @param {Mixed} value The value to add\n */\n this._setItem = function (key, value) {\n if (! this._checkSupport()) {\n _error('The browser does not support the \"' + options.driver + '\" driver');\n }\n\n try {\n var oldVal = this._getItem(key);\n this._driver.setItem(this._getPrefix(key), this._serialize(value));\n if (this._exists(key) && ! angular.equals(oldVal, value)) {\n this._event('item.updated', { key: key, oldValue: oldVal, newValue: value });\n } else {\n this._event('item.added', { key: key, value: value });\n }\n } catch (e) {\n if (['QUOTA_EXCEEDED_ERR',\n 'NS_ERROR_DOM_QUOTA_REACHED',\n 'QuotaExceededError'].indexOf(e.name) !== -1) {\n _error('The browser storage quota has been exceeded');\n } else {\n _error('Could not add item with key \"' + key + '\"');\n }\n }\n };\n\n /**\n * Get from storage\n *\n * @private\n * @throws {Error} if browser support fails\n *\n * @param {String} key The key to get\n *\n * @return {Mixed}\n */\n this._getItem = function (key) {\n if (! this._checkSupport()) {\n _error('The browser does not support the \"' + options.driver + '\" driver');\n }\n\n return this._unserialize(this._driver.getItem(this._getPrefix(key)));\n };\n\n /**\n * Exists in storage\n *\n * @private\n * @throws {Error} if browser support fails\n *\n * @param {String} key The key to check for existence\n *\n * @return {Boolean}\n */\n this._exists = function (key) {\n if (! this._checkSupport()) {\n _error('The browser does not support the \"' + options.driver + '\" driver');\n }\n\n return this._driver.hasOwnProperty(this._getPrefix(_value(key))) || !! this._getItem(key);\n };\n\n /**\n * Remove from storage\n *\n * @private\n * @throws {Error} if browser support fails\n *\n * @param {String} key The key to remove\n *\n * @return {Boolean}\n */\n this._removeItem = function (key) {\n if (! this._checkSupport()) {\n _error('The browser does not support the \"' + options.driver + '\" driver');\n }\n\n if (! this._exists(key)) return false;\n\n this._driver.removeItem(this._getPrefix(key));\n this._event('item.forgotten', { key: key });\n\n return true;\n };\n }\n\n /**\n * Define the public api\n *\n * @public\n *\n * @type {Object}\n */\n Locker.prototype = {\n\n /**\n * Add a new item to storage (even if it already exists)\n *\n * @public\n *\n * @param {Mixed} key The key to add\n * @param {Mixed} value The value to add\n * @param {Mixed} def The default to pass to function if doesn't already exist\n *\n * @return {Locker|Boolean}\n */\n put: function (key, value, def) {\n if (! _defined(key)) return false;\n key = _value(key);\n\n if (angular.isObject(key)) {\n angular.forEach(key, function (value, key) {\n this._setItem(key, _defined(value) ? value : def);\n }, this);\n } else {\n if (! _defined(value)) return false;\n var val = this._getItem(key);\n this._setItem(key, _value(value, _defined(val) ? val : def));\n }\n\n return this;\n },\n\n /**\n * Add an item to storage if it doesn't already exist\n *\n * @public\n *\n * @param {Mixed} key The key to add\n * @param {Mixed} value The value to add\n * @param {Mixed} def The default to pass to function if doesn't already exist\n *\n * @return {Boolean}\n */\n add: function (key, value, def) {\n if (! this.has(key)) {\n this.put(key, value, def);\n return true;\n }\n\n return false;\n },\n\n /**\n * Retrieve the specified item from storage\n *\n * @public\n *\n * @param {String|Array} key The key to get\n * @param {Mixed} def The default value if it does not exist\n *\n * @return {Mixed}\n */\n get: function (key, def) {\n if (angular.isArray(key)) {\n var items = {};\n angular.forEach(key, function (k) {\n if (this.has(k)) items[k] = this._getItem(k);\n }, this);\n\n return items;\n }\n\n if (! this.has(key)) return arguments.length === 2 ? def : void 0;\n\n return this._getItem(key);\n },\n\n /**\n * Determine whether the item exists in storage\n *\n * @public\n *\n * @param {String|Function} key - The key to remove\n *\n * @return {Boolean}\n */\n has: function (key) {\n return this._exists(key);\n },\n\n /**\n * Remove specified item(s) from storage\n *\n * @public\n *\n * @param {String|Array} key The key or array of keys to remove\n *\n * @return {Object}\n */\n forget: function (key) {\n key = _value(key);\n\n if (angular.isArray(key)) {\n key.map(this._removeItem, this);\n } else {\n this._removeItem(key);\n }\n\n return this;\n },\n\n /**\n * Retrieve the specified item from storage and then remove it\n *\n * @public\n *\n * @param {String|Array} key The key to pull from storage\n * @param {Mixed} def The default value if it does not exist\n *\n * @return {Mixed}\n */\n pull: function (key, def) {\n var value = this.get(key, def);\n this.forget(key);\n\n return value;\n },\n\n /**\n * Return all items in storage within the current namespace/driver\n *\n * @public\n *\n * @return {Object}\n */\n all: function () {\n var items = {};\n angular.forEach(this._driver, function (value, key) {\n if (this._namespace) {\n var prefix = this._namespace + this._separator;\n if (key.indexOf(prefix) === 0) key = key.substring(prefix.length);\n }\n if (this.has(key)) items[key] = this.get(key);\n }, this);\n\n return items;\n },\n\n /**\n * Get the storage keys as an array\n *\n * @public\n *\n * @return {Array}\n */\n keys: function () {\n return Object.keys(this.all());\n },\n\n /**\n * Remove all items set within the current namespace/driver\n *\n * @public\n *\n * @return {Locker}\n */\n clean: function () {\n return this.forget(this.keys());\n },\n\n /**\n * Empty the current storage driver completely. careful now.\n *\n * @public\n *\n * @return {Locker}\n */\n empty: function () {\n this._driver.clear();\n\n return this;\n },\n\n /**\n * Get the total number of items within the current namespace\n *\n * @public\n *\n * @return {Integer}\n */\n count: function () {\n return this.keys().length;\n },\n\n /**\n * Bind a storage key to a $scope property\n *\n * @public\n *\n * @param {Object} $scope The angular $scope object\n * @param {String} key The key in storage to bind to\n * @param {Mixed} def The default value to initially bind\n *\n * @return {Locker}\n */\n bind: function ($scope, key, def) {\n if (! _defined( $scope.$eval(key) )) {\n $parse(key).assign($scope, this.get(key, def));\n this.add(key, def);\n }\n\n var self = this;\n this._watchers[key + $scope.$id] = $scope.$watch(key, function (newVal) {\n self.put(key, newVal);\n }, angular.isObject($scope[key]));\n\n return this;\n },\n\n /**\n * Unbind a storage key from a $scope property\n *\n * @public\n *\n * @param {Object} $scope The angular $scope object\n * @param {String} key The key to remove from bindings\n *\n * @return {Locker}\n */\n unbind: function ($scope, key) {\n $parse(key).assign($scope, void 0);\n this.forget(key);\n\n var watchId = key + $scope.$id;\n\n if (this._watchers[watchId]) {\n // execute the de-registration function\n this._watchers[watchId]();\n delete this._watchers[watchId];\n }\n\n return this;\n },\n\n /**\n * Set the storage driver on a new instance to enable overriding defaults\n *\n * @public\n *\n * @param {String} driver The driver to switch to\n *\n * @return {Locker}\n */\n driver: function (driver) {\n return this.instance(angular.extend(this._options, { driver: driver }));\n },\n\n /**\n * Get the currently set driver\n *\n * @public\n *\n * @return {Storage}\n */\n getDriver: function () {\n return this._driver;\n },\n\n /**\n * Set the namespace on a new instance to enable overriding defaults\n *\n * @public\n *\n * @param {String} namespace The namespace to switch to\n *\n * @return {Locker}\n */\n namespace: function (namespace) {\n return this.instance(angular.extend(this._options, { namespace: namespace }));\n },\n\n /**\n * Get the currently set namespace\n *\n * @public\n *\n * @return {String}\n */\n getNamespace: function () {\n return this._namespace;\n },\n\n /**\n * Check browser support\n *\n * @public\n * @see github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js#L38-L47\n *\n * @param {String} driver The driver to check support with\n *\n * @return {Boolean}\n */\n supported: function (driver) {\n return this._checkSupport(driver);\n },\n\n /**\n * Get a new instance of Locker\n *\n * @public\n *\n * @param {Object} options The config options to instantiate with\n *\n * @return {Locker}\n */\n instance: function (options) {\n return new Locker(options);\n }\n };\n\n // return the default instance\n return new Locker(defaults);\n }]\n };\n\n }).name; // export module name for the likes of Browserify and Webpack\n\n});\n"],"sourceRoot":"/source/"} -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import {task as fizzy} from 'fizzy'; 3 | import pkg from './package.json'; 4 | import {paths, banner} from './config'; 5 | import runSequence from 'run-sequence'; 6 | 7 | // Lint the JS 8 | gulp.task('lint', fizzy('lint', { src: paths.scripts })); 9 | 10 | // Check the coding style 11 | gulp.task('jscs', fizzy('jscs', { src: paths.scripts[0] })); 12 | 13 | // Remove the output folder 14 | gulp.task('clean', fizzy('clean', { src: paths.output })); 15 | 16 | // Build the output folder 17 | gulp.task('scripts', ['clean'], fizzy('scripts', { 18 | src: paths.scripts, 19 | dest: paths.output, 20 | header: [banner, { pkg }], 21 | size: false 22 | })); 23 | 24 | // Run the tests 25 | gulp.task('test', fizzy('test', { 26 | src: paths.vendor 27 | .concat(['./node_modules/phantomjs-polyfill/bind-polyfill.js']) 28 | .concat(paths.scripts, paths.test), 29 | karmaConfigFile: paths.karma 30 | })); 31 | 32 | // Build the readme 33 | gulp.task('gitdown', fizzy('gitdown', { 34 | src: paths.gitdown.src, 35 | dest: paths.gitdown.dest 36 | })); 37 | 38 | // Define the build tasks 39 | gulp.task('build', (cb) => { 40 | runSequence(['lint', 'jscs', 'scripts', 'test'], 'gitdown', cb); 41 | }); 42 | 43 | // Increment versions 44 | gulp.task('version', fizzy('version', { 45 | src: paths.versions, 46 | currentVersion: pkg.version 47 | })); 48 | 49 | // release a new version 50 | gulp.task('release', (cb) => { 51 | runSequence('version', 'build', cb); 52 | }); 53 | 54 | // Watch for changes 55 | gulp.task('watch', () => { 56 | gulp.watch(paths.scripts, ['lint', 'jscs']); 57 | gulp.watch(paths.gitdown.glob, ['gitdown']); 58 | }); 59 | 60 | gulp.task('default', ['build']); 61 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "opts": { 3 | "destination": "./docs/" 4 | }, 5 | "templates": { 6 | "cleverLinks" : false, 7 | "monospaceLinks" : false, 8 | "outputSourceFiles" : false, 9 | "outputSourcePath" : false, 10 | "collapseSymbols" : false, 11 | "inverseNav" : true 12 | }, 13 | "opts": { 14 | "encoding": "utf8", 15 | "private": false 16 | } 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-locker", 3 | "version": "2.0.5", 4 | "description": "A simple & configurable abstraction for local/session storage in angular projects", 5 | "author": "@tymondesigns", 6 | "license": "MIT", 7 | "homepage": "https://github.com/tymondesigns/angular-locker", 8 | "keywords": [ 9 | "angular-locker", 10 | "angular", 11 | "locker", 12 | "storage", 13 | "localStorage", 14 | "sessionStorage", 15 | "session", 16 | "local" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "http://github.com/tymondesigns/angular-locker.git" 21 | }, 22 | "dependencies": { 23 | "angular": ">=1.2.0 <2.0.0" 24 | }, 25 | "devDependencies": { 26 | "angular-mocks": ">=1.2.0 <2.0.0", 27 | "babel-core": "^5.7.4", 28 | "fizzy": "^0.3.3", 29 | "gulp": "~3.9.0", 30 | "karma": "^0.12.16", 31 | "karma-babel-preprocessor": "^5.2.2", 32 | "karma-coverage": "~0.2.6", 33 | "karma-jasmine": "~0.2.0", 34 | "karma-notify-reporter": "^0.1.1", 35 | "karma-phantomjs-launcher": "^0.1.4", 36 | "karma-sauce-launcher": "^0.2.10", 37 | "karma-spec-reporter": "0.0.13", 38 | "phantomjs-polyfill": "0.0.2", 39 | "run-sequence": "^1.1.3" 40 | }, 41 | "main": "src/angular-locker.js", 42 | "scripts": { 43 | "test": "gulp test" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/angular-locker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * angular-locker 3 | * 4 | * A simple & configurable abstraction for local/session storage in angular projects. 5 | * 6 | * @link https://github.com/tymondesigns/angular-locker 7 | * @author Sean Tymon @tymondesigns 8 | * @license MIT License, http://www.opensource.org/licenses/MIT 9 | */ 10 | 11 | (function (root, factory) { 12 | if (typeof define === 'function' && define.amd) { 13 | define(function () { 14 | return factory(root.angular || (window && window.angular)); 15 | }); 16 | } else if (typeof exports === 'object') { 17 | module.exports = factory(root.angular || (window && window.angular)); 18 | } else { 19 | factory(root.angular); 20 | } 21 | })(this, function (angular) { 22 | 23 | 'use strict'; 24 | 25 | return angular.module('angular-locker', []) 26 | 27 | .provider('locker', function () { 28 | 29 | /** 30 | * If value is a function then execute, otherwise return 31 | * 32 | * @private 33 | * 34 | * @param {Mixed} value The value to execute or return 35 | * @param {Mixed} param The parameter to pass to function if applicable 36 | * 37 | * @return {Mixed} 38 | */ 39 | var _value = function (value, param) { 40 | return angular.isFunction(value) ? value(param) : value; 41 | }; 42 | 43 | /** 44 | * Determine whether a value is defined and not null 45 | * 46 | * @private 47 | * 48 | * @param {Mixed} value The value to check 49 | * 50 | * @return {Boolean} 51 | */ 52 | var _defined = function (value) { 53 | return angular.isDefined(value) && value !== null; 54 | }; 55 | 56 | /** 57 | * Trigger an error 58 | * 59 | * @private 60 | * @throws {Error} 61 | * 62 | * @param {String} msg The error message 63 | */ 64 | var _error = function (msg) { 65 | throw new Error('[angular-locker] ' + msg); 66 | }; 67 | 68 | /** 69 | * Set the defaults 70 | * 71 | * @private 72 | * 73 | * @type {Object} 74 | */ 75 | var defaults = { 76 | driver: 'local', 77 | namespace: 'locker', 78 | eventsEnabled: true, 79 | separator: '.', 80 | extend: {} 81 | }; 82 | 83 | return { 84 | 85 | /** 86 | * Allow the defaults to be specified via the `lockerProvider` 87 | * 88 | * @param {Object} value The defaults to override 89 | */ 90 | defaults: function (value) { 91 | if (! _defined(value)) return defaults; 92 | 93 | angular.forEach(value, function (val, key) { 94 | if (defaults.hasOwnProperty(key)) defaults[key] = val; 95 | }); 96 | }, 97 | 98 | /** 99 | * The locker service 100 | */ 101 | $get: ['$window', '$rootScope', '$parse', function ($window, $rootScope, $parse) { 102 | 103 | /** 104 | * Define the Locker class 105 | * 106 | * @public 107 | * @constructor 108 | * 109 | * @param {Object} options The config options to initialize with 110 | */ 111 | function Locker (options) { 112 | 113 | /** 114 | * The config options 115 | * 116 | * @private 117 | * 118 | * @type {Object} 119 | */ 120 | this._options = options; 121 | 122 | /** 123 | * Out of the box drivers 124 | * 125 | * @private 126 | * 127 | * @type {Object} 128 | */ 129 | this._registeredDrivers = angular.extend({ 130 | local: $window.localStorage, 131 | session: $window.sessionStorage 132 | }, options.extend); 133 | 134 | /** 135 | * Get the Storage instance from the key 136 | * 137 | * @private 138 | * 139 | * @param {String} driver The storage driver identifier 140 | * 141 | * @return {Storage} 142 | */ 143 | this._resolveDriver = function (driver) { 144 | if (! this._registeredDrivers.hasOwnProperty(driver)) { 145 | _error('The driver "' + driver + '" was not found.'); 146 | } 147 | 148 | return this._registeredDrivers[driver]; 149 | }; 150 | 151 | /** 152 | * The driver instance 153 | * 154 | * @private 155 | * 156 | * @type {Storage} 157 | */ 158 | this._driver = this._resolveDriver(options.driver); 159 | 160 | /** 161 | * The namespace value 162 | * 163 | * @private 164 | * 165 | * @type {String} 166 | */ 167 | this._namespace = options.namespace; 168 | 169 | /** 170 | * Separates the namespace from the keys 171 | * 172 | * @private 173 | * 174 | * @type {String} 175 | */ 176 | this._separator = options.separator; 177 | 178 | /** 179 | * Store the watchers here so we can un-register them later 180 | * 181 | * @private 182 | * 183 | * @type {Object} 184 | */ 185 | this._watchers = {}; 186 | 187 | /** 188 | * Check browser support 189 | * 190 | * @private 191 | * @see github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js#L38-L47 192 | * 193 | * @param {String} driver The driver to check support with 194 | * 195 | * @return {Boolean} 196 | */ 197 | this._checkSupport = function (driver) { 198 | if (! _defined(this._supported)) { 199 | var l = 'l'; 200 | try { 201 | this._resolveDriver(driver || options.driver).setItem(l, l); 202 | this._resolveDriver(driver || options.driver).removeItem(l); 203 | this._supported = true; 204 | } catch (e) { 205 | this._supported = false; 206 | } 207 | } 208 | 209 | return this._supported; 210 | }; 211 | 212 | /** 213 | * Build the storage key from the namspace 214 | * 215 | * @private 216 | * 217 | * @param {String} key The key to build the prefix with 218 | * 219 | * @return {String} 220 | */ 221 | this._getPrefix = function (key) { 222 | if (! this._namespace) return key; 223 | 224 | return this._namespace + this._separator + key; 225 | }; 226 | 227 | /** 228 | * Try to encode value as json, or just return the value upon failure 229 | * 230 | * @private 231 | * 232 | * @param {Mixed} value The value to serialize 233 | * 234 | * @return {Mixed} 235 | */ 236 | this._serialize = function (value) { 237 | try { 238 | return angular.toJson(value); 239 | } catch (e) { 240 | return value; 241 | } 242 | }; 243 | 244 | /** 245 | * Try to parse value as json, if it fails then it probably isn't json 246 | * so just return it 247 | * 248 | * @private 249 | * 250 | * @param {String} value The value to unserialize 251 | * 252 | * @return {Mixed} 253 | */ 254 | this._unserialize = function (value) { 255 | try { 256 | return angular.fromJson(value); 257 | } catch (e) { 258 | return value; 259 | } 260 | }; 261 | 262 | /** 263 | * Trigger an event 264 | * 265 | * @private 266 | * 267 | * @param {String} name The name of the event to trigger 268 | * @param {Object} payload The data to pass along with event 269 | */ 270 | this._event = function (name, payload) { 271 | if (this._options.eventsEnabled) { 272 | $rootScope.$emit('locker.' + name, angular.extend(payload, { 273 | driver: this._options.driver, 274 | namespace: this._namespace, 275 | })); 276 | } 277 | }; 278 | 279 | /** 280 | * Add to storage 281 | * 282 | * @private 283 | * @throws {Error} if browser support fails 284 | * 285 | * @param {String} key The key to add 286 | * @param {Mixed} value The value to add 287 | */ 288 | this._setItem = function (key, value) { 289 | if (! this._checkSupport()) { 290 | _error('The browser does not support the "' + options.driver + '" driver'); 291 | } 292 | 293 | try { 294 | var oldVal = this._getItem(key); 295 | this._driver.setItem(this._getPrefix(key), this._serialize(value)); 296 | if (this._exists(key) && ! angular.equals(oldVal, value)) { 297 | this._event('item.updated', { key: key, oldValue: oldVal, newValue: value }); 298 | } else { 299 | this._event('item.added', { key: key, value: value }); 300 | } 301 | } catch (e) { 302 | if (['QUOTA_EXCEEDED_ERR', 303 | 'NS_ERROR_DOM_QUOTA_REACHED', 304 | 'QuotaExceededError'].indexOf(e.name) !== -1) { 305 | _error('The browser storage quota has been exceeded'); 306 | } else { 307 | _error('Could not add item with key "' + key + '"'); 308 | } 309 | } 310 | }; 311 | 312 | /** 313 | * Get from storage 314 | * 315 | * @private 316 | * @throws {Error} if browser support fails 317 | * 318 | * @param {String} key The key to get 319 | * 320 | * @return {Mixed} 321 | */ 322 | this._getItem = function (key) { 323 | if (! this._checkSupport()) { 324 | _error('The browser does not support the "' + options.driver + '" driver'); 325 | } 326 | 327 | return this._unserialize(this._driver.getItem(this._getPrefix(key))); 328 | }; 329 | 330 | /** 331 | * Exists in storage 332 | * 333 | * @private 334 | * @throws {Error} if browser support fails 335 | * 336 | * @param {String} key The key to check for existence 337 | * 338 | * @return {Boolean} 339 | */ 340 | this._exists = function (key) { 341 | if (! this._checkSupport()) { 342 | _error('The browser does not support the "' + options.driver + '" driver'); 343 | } 344 | 345 | return this._driver.hasOwnProperty(this._getPrefix(_value(key))) || !! this._getItem(key); 346 | }; 347 | 348 | /** 349 | * Remove from storage 350 | * 351 | * @private 352 | * @throws {Error} if browser support fails 353 | * 354 | * @param {String} key The key to remove 355 | * 356 | * @return {Boolean} 357 | */ 358 | this._removeItem = function (key) { 359 | if (! this._checkSupport()) { 360 | _error('The browser does not support the "' + options.driver + '" driver'); 361 | } 362 | 363 | if (! this._exists(key)) return false; 364 | 365 | this._driver.removeItem(this._getPrefix(key)); 366 | this._event('item.forgotten', { key: key }); 367 | 368 | return true; 369 | }; 370 | } 371 | 372 | /** 373 | * Define the public api 374 | * 375 | * @public 376 | * 377 | * @type {Object} 378 | */ 379 | Locker.prototype = { 380 | 381 | /** 382 | * Add a new item to storage (even if it already exists) 383 | * 384 | * @public 385 | * 386 | * @param {Mixed} key The key to add 387 | * @param {Mixed} value The value to add 388 | * @param {Mixed} def The default to pass to function if doesn't already exist 389 | * 390 | * @return {Locker|Boolean} 391 | */ 392 | put: function (key, value, def) { 393 | if (! _defined(key)) return false; 394 | key = _value(key); 395 | 396 | if (angular.isObject(key)) { 397 | angular.forEach(key, function (value, key) { 398 | this._setItem(key, _defined(value) ? value : def); 399 | }, this); 400 | } else { 401 | if (! _defined(value)) return false; 402 | var val = this._getItem(key); 403 | this._setItem(key, _value(value, _defined(val) ? val : def)); 404 | } 405 | 406 | return this; 407 | }, 408 | 409 | /** 410 | * Add an item to storage if it doesn't already exist 411 | * 412 | * @public 413 | * 414 | * @param {Mixed} key The key to add 415 | * @param {Mixed} value The value to add 416 | * @param {Mixed} def The default to pass to function if doesn't already exist 417 | * 418 | * @return {Boolean} 419 | */ 420 | add: function (key, value, def) { 421 | if (! this.has(key)) { 422 | this.put(key, value, def); 423 | return true; 424 | } 425 | 426 | return false; 427 | }, 428 | 429 | /** 430 | * Retrieve the specified item from storage 431 | * 432 | * @public 433 | * 434 | * @param {String|Array} key The key to get 435 | * @param {Mixed} def The default value if it does not exist 436 | * 437 | * @return {Mixed} 438 | */ 439 | get: function (key, def) { 440 | if (angular.isArray(key)) { 441 | var items = {}; 442 | angular.forEach(key, function (k) { 443 | if (this.has(k)) items[k] = this._getItem(k); 444 | }, this); 445 | 446 | return items; 447 | } 448 | 449 | if (! this.has(key)) return arguments.length === 2 ? def : void 0; 450 | 451 | return this._getItem(key); 452 | }, 453 | 454 | /** 455 | * Determine whether the item exists in storage 456 | * 457 | * @public 458 | * 459 | * @param {String|Function} key - The key to remove 460 | * 461 | * @return {Boolean} 462 | */ 463 | has: function (key) { 464 | return this._exists(key); 465 | }, 466 | 467 | /** 468 | * Remove specified item(s) from storage 469 | * 470 | * @public 471 | * 472 | * @param {String|Array} key The key or array of keys to remove 473 | * 474 | * @return {Object} 475 | */ 476 | forget: function (key) { 477 | key = _value(key); 478 | 479 | if (angular.isArray(key)) { 480 | key.map(this._removeItem, this); 481 | } else { 482 | this._removeItem(key); 483 | } 484 | 485 | return this; 486 | }, 487 | 488 | /** 489 | * Retrieve the specified item from storage and then remove it 490 | * 491 | * @public 492 | * 493 | * @param {String|Array} key The key to pull from storage 494 | * @param {Mixed} def The default value if it does not exist 495 | * 496 | * @return {Mixed} 497 | */ 498 | pull: function (key, def) { 499 | var value = this.get(key, def); 500 | this.forget(key); 501 | 502 | return value; 503 | }, 504 | 505 | /** 506 | * Return all items in storage within the current namespace/driver 507 | * 508 | * @public 509 | * 510 | * @return {Object} 511 | */ 512 | all: function () { 513 | var items = {}; 514 | angular.forEach(this._driver, function (value, key) { 515 | if (this._namespace) { 516 | var prefix = this._namespace + this._separator; 517 | if (key.indexOf(prefix) === 0) key = key.substring(prefix.length); 518 | } 519 | if (this.has(key)) items[key] = this.get(key); 520 | }, this); 521 | 522 | return items; 523 | }, 524 | 525 | /** 526 | * Get the storage keys as an array 527 | * 528 | * @public 529 | * 530 | * @return {Array} 531 | */ 532 | keys: function () { 533 | return Object.keys(this.all()); 534 | }, 535 | 536 | /** 537 | * Remove all items set within the current namespace/driver 538 | * 539 | * @public 540 | * 541 | * @return {Locker} 542 | */ 543 | clean: function () { 544 | return this.forget(this.keys()); 545 | }, 546 | 547 | /** 548 | * Empty the current storage driver completely. careful now. 549 | * 550 | * @public 551 | * 552 | * @return {Locker} 553 | */ 554 | empty: function () { 555 | this._driver.clear(); 556 | 557 | return this; 558 | }, 559 | 560 | /** 561 | * Get the total number of items within the current namespace 562 | * 563 | * @public 564 | * 565 | * @return {Integer} 566 | */ 567 | count: function () { 568 | return this.keys().length; 569 | }, 570 | 571 | /** 572 | * Bind a storage key to a $scope property 573 | * 574 | * @public 575 | * 576 | * @param {Object} $scope The angular $scope object 577 | * @param {String} key The key in storage to bind to 578 | * @param {Mixed} def The default value to initially bind 579 | * 580 | * @return {Locker} 581 | */ 582 | bind: function ($scope, key, def) { 583 | if (! _defined( $scope.$eval(key) )) { 584 | $parse(key).assign($scope, this.get(key, def)); 585 | this.add(key, def); 586 | } 587 | 588 | var self = this; 589 | this._watchers[key + $scope.$id] = $scope.$watch(key, function (newVal) { 590 | self.put(key, newVal); 591 | }, angular.isObject($scope[key])); 592 | 593 | return this; 594 | }, 595 | 596 | /** 597 | * Unbind a storage key from a $scope property 598 | * 599 | * @public 600 | * 601 | * @param {Object} $scope The angular $scope object 602 | * @param {String} key The key to remove from bindings 603 | * 604 | * @return {Locker} 605 | */ 606 | unbind: function ($scope, key) { 607 | $parse(key).assign($scope, void 0); 608 | this.forget(key); 609 | 610 | var watchId = key + $scope.$id; 611 | 612 | if (this._watchers[watchId]) { 613 | // execute the de-registration function 614 | this._watchers[watchId](); 615 | delete this._watchers[watchId]; 616 | } 617 | 618 | return this; 619 | }, 620 | 621 | /** 622 | * Set the storage driver on a new instance to enable overriding defaults 623 | * 624 | * @public 625 | * 626 | * @param {String} driver The driver to switch to 627 | * 628 | * @return {Locker} 629 | */ 630 | driver: function (driver) { 631 | return this.instance(angular.extend(this._options, { driver: driver })); 632 | }, 633 | 634 | /** 635 | * Get the currently set driver 636 | * 637 | * @public 638 | * 639 | * @return {Storage} 640 | */ 641 | getDriver: function () { 642 | return this._driver; 643 | }, 644 | 645 | /** 646 | * Set the namespace on a new instance to enable overriding defaults 647 | * 648 | * @public 649 | * 650 | * @param {String} namespace The namespace to switch to 651 | * 652 | * @return {Locker} 653 | */ 654 | namespace: function (namespace) { 655 | return this.instance(angular.extend(this._options, { namespace: namespace })); 656 | }, 657 | 658 | /** 659 | * Get the currently set namespace 660 | * 661 | * @public 662 | * 663 | * @return {String} 664 | */ 665 | getNamespace: function () { 666 | return this._namespace; 667 | }, 668 | 669 | /** 670 | * Check browser support 671 | * 672 | * @public 673 | * @see github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js#L38-L47 674 | * 675 | * @param {String} driver The driver to check support with 676 | * 677 | * @return {Boolean} 678 | */ 679 | supported: function (driver) { 680 | return this._checkSupport(driver); 681 | }, 682 | 683 | /** 684 | * Get a new instance of Locker 685 | * 686 | * @public 687 | * 688 | * @param {Object} options The config options to instantiate with 689 | * 690 | * @return {Locker} 691 | */ 692 | instance: function (options) { 693 | return new Locker(options); 694 | } 695 | }; 696 | 697 | // return the default instance 698 | return new Locker(defaults); 699 | }] 700 | }; 701 | 702 | }).name; // export module name for the likes of Browserify and Webpack 703 | 704 | }); 705 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | 3 | // if (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) { 4 | // console.log('Make sure the SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are set.'); 5 | // process.exit(1); 6 | // } 7 | 8 | var customLaunchers = { 9 | // sl_chrome: { 10 | // base: 'SauceLabs', 11 | // browserName: 'chrome', 12 | // platform: 'Windows 7', 13 | // version: '38' 14 | // }, 15 | // sl_firefox: { 16 | // base: 'SauceLabs', 17 | // browserName: 'firefox', 18 | // version: '33', 19 | // platform: 'Windows 7' 20 | // }, 21 | // sl_ios_safari: { 22 | // base: 'SauceLabs', 23 | // browserName: 'iphone', 24 | // platform: 'OS X 10.9', 25 | // version: '7.1' 26 | // }, 27 | // sl_mac_safari: { 28 | // base: 'SauceLabs', 29 | // browserName: 'safari', 30 | // platform: 'OS X 10.10', 31 | // version: '8' 32 | // }, 33 | // sl_ie_11: { 34 | // base: 'SauceLabs', 35 | // browserName: 'internet explorer', 36 | // platform: 'Windows 8.1', 37 | // version: '11' 38 | // }, 39 | // sl_ie_9: { 40 | // base: 'SauceLabs', 41 | // browserName: 'internet explorer', 42 | // platform: 'Windows 7', 43 | // version: '9' 44 | // }, 45 | // sl_opera_12: { 46 | // base: 'SauceLabs', 47 | // browserName: 'opera', 48 | // platform: 'Windows 7', 49 | // version: '12' 50 | // } 51 | }; 52 | 53 | config.set({ 54 | basePath: '', 55 | autoWatch: true, 56 | frameworks: ['jasmine'], 57 | // logLevel: config.LOG_DEBUG, 58 | preprocessors: { 59 | '../src/*.js': ['coverage'], 60 | '../test/**/*.js': ['babel'] 61 | }, 62 | sauceLabs: { 63 | testName: 'angular-locker', 64 | recordScreenshots: false, 65 | tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER 66 | }, 67 | customLaunchers: customLaunchers, 68 | browsers: ['PhantomJS'].concat(Object.keys(customLaunchers)), 69 | reporters: ['spec', 'coverage', 'notify', 'saucelabs'], 70 | singleRun: true, 71 | captureTimeout: 120000, 72 | coverageReporter: { 73 | type: 'lcov', 74 | dir: 'coverage/', 75 | subdir: function(browser) { 76 | return 'lcov'; 77 | } 78 | } 79 | }); 80 | }; 81 | -------------------------------------------------------------------------------- /test/mock/storageMock.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | function storageMock() { 5 | let store = {}; 6 | 7 | Object.defineProperties(store, { 8 | setItem: { 9 | value: (key, value) => { 10 | store[key] = value || ''; 11 | }, 12 | enumerable: false, 13 | writable: true 14 | }, 15 | getItem: { 16 | value: (key) => store[key], 17 | enumerable: false, 18 | writable: true 19 | }, 20 | removeItem: { 21 | value: (key) => { 22 | delete store[key]; 23 | }, 24 | enumerable: false, 25 | writable: true 26 | }, 27 | length: { 28 | get: () => Object.keys(store).length, 29 | enumerable: false 30 | }, 31 | clear: { 32 | value: () => { 33 | store = {}; 34 | }, 35 | enumerable: false, 36 | writable: true 37 | } 38 | }); 39 | 40 | return store; 41 | } -------------------------------------------------------------------------------- /test/spec/angular-locker.spec.js: -------------------------------------------------------------------------------- 1 | describe('angular-locker', () => { 2 | 3 | beforeEach(module('angular-locker', ($provide) => { 4 | $provide.value('$window', { 5 | localStorage: storageMock(), 6 | sessionStorage: storageMock() 7 | }); 8 | })); 9 | 10 | describe('lockerProvider', () => { 11 | 12 | it('should be defined', () => { 13 | module((lockerProvider) => { 14 | expect(lockerProvider).toBeDefined(); 15 | }); 16 | }); 17 | 18 | it('should set a default storage driver', () => { 19 | module((lockerProvider) => { 20 | expect(lockerProvider.defaults.driver).toEqual('local'); 21 | 22 | lockerProvider.defaults({ driver: 'session'}); 23 | 24 | expect(lockerProvider.defaults.driver).toEqual('session'); 25 | expect(lockerProvider.defaults.namespace).toEqual('locker'); 26 | }); 27 | }); 28 | 29 | it('should set a default storage driver via function', () => { 30 | module((lockerProvider) => { 31 | expect( lockerProvider.defaults.driver ).toEqual('local'); 32 | 33 | lockerProvider.defaults({ 34 | driver: () => { 35 | var shouldUseSession = true; 36 | if (shouldUseSession) return 'session'; 37 | } 38 | }); 39 | 40 | expect(lockerProvider.defaults.driver).toEqual('session'); 41 | }); 42 | }); 43 | 44 | it('should throw an error if storage driver that does not exist is used', () => { 45 | module((lockerProvider) => { 46 | expect(() => { 47 | locker.driver('somethingNotExpected'); 48 | }).toThrowError(); 49 | 50 | expect( locker._driver ).toEqual($window.localStorage); 51 | }); 52 | }); 53 | 54 | it('should set a default namespace', () => { 55 | module((lockerProvider) => { 56 | expect( lockerProvider.defaults.namespace ).toEqual('locker'); 57 | 58 | lockerProvider.defaults.namespace = 'myApp.foo'; 59 | expect( lockerProvider.defaults.namespace ).toEqual('myApp'); 60 | 61 | lockerProvider.defaults.namespace = ''; 62 | expect( lockerProvider.defaults.namespace ).toEqual(''); 63 | 64 | lockerProvider.defaults.namespace = false; 65 | expect( lockerProvider.defaults.namespace ).toEqual(false); 66 | }); 67 | }); 68 | 69 | it('should set a default namespace via function', () => { 70 | module((lockerProvider) => { 71 | expect( lockerProvider.defaults.namespace ).toEqual('locker'); 72 | lockerProvider.defaults.namespace = () => { 73 | var arr = ['myApp', 'coolApp', 'somethingElse']; 74 | return arr[1]; 75 | }; 76 | expect( lockerProvider.defaults.namespace ).toEqual('coolApp'); 77 | }); 78 | }); 79 | 80 | it('should set a default separator', () => { 81 | module((lockerProvider) => { 82 | expect( lockerProvider.defaults.separator ).toEqual('.'); 83 | lockerProvider.defaults.separator = '-'; 84 | expect( lockerProvider.defaults.separator ).toEqual('-'); 85 | lockerProvider.defaults.separator = ''; 86 | expect( lockerProvider.defaults.separator ).toEqual(''); 87 | 88 | lockerProvider.defaults.separator = false; 89 | expect( lockerProvider.defaults.separator ).toEqual(false); 90 | }); 91 | }); 92 | 93 | it('should set a default separator via function', () => { 94 | module((lockerProvider) => { 95 | expect( lockerProvider.defaults.separator ).toEqual('.'); 96 | lockerProvider.defaults.separator = () => { 97 | var arr = ['.', '-', '!']; 98 | return arr[1]; 99 | }; 100 | expect( lockerProvider.defaults.separator ).toEqual('-'); 101 | }); 102 | }); 103 | 104 | it('should throw an error when setting a driver that is not registered', inject((locker) => { 105 | expect(() => { 106 | locker.driver('foo'); 107 | }).toThrowError(); 108 | })); 109 | }); 110 | 111 | describe('lockerService', () => { 112 | 113 | describe('adding items to locker', () => { 114 | 115 | it('should put a string into the locker', inject((locker) => { 116 | var str = 'someVal'; 117 | locker.put('someKey', str); 118 | 119 | expect( locker.get('someKey') ).toEqual(str); 120 | 121 | locker.driver('session').put('sessionKey1', 1); 122 | locker.driver('session').put('sessionKey2', 2); 123 | locker.driver('local').put('localKey1', 1); 124 | locker.driver('local').put('localKey2', 2); 125 | 126 | expect(locker.driver('session').get('sessionKey2')).toEqual(2); 127 | 128 | })); 129 | 130 | it('should put a boolean into the locker', inject((locker) => { 131 | locker.put('someKey', false); 132 | locker.put('someKey1', true); 133 | 134 | expect( locker.get('someKey') ).toEqual(false); 135 | expect( locker.get('someKey1') ).toEqual(true); 136 | })); 137 | 138 | it('should put an object into the locker', inject((locker) => { 139 | var obj = { 140 | foo: 'bar', 141 | bar: 'baz', 142 | baz: { 143 | foo: true, 144 | bar: false, 145 | baz: 12.34 146 | } 147 | }; 148 | 149 | locker.put('objectKey', obj); 150 | 151 | var result = locker.get('objectKey'); 152 | 153 | expect( result ).toEqual(obj); 154 | expect( result.baz.bar ).toBeFalsy(); 155 | })); 156 | 157 | it('should put an array into the locker', inject((locker) => { 158 | var arr1 = ['foo', 123.456, true, { foo: 'bar' }]; 159 | var arr2 = ['foo', 'bar', 'baz']; 160 | 161 | locker.put('arrayKey1', arr1); 162 | locker.put('arrayKey2', arr2); 163 | 164 | var result1 = locker.get('arrayKey1'); 165 | var result2 = locker.get('arrayKey2'); 166 | 167 | expect( result1 ).toEqual(arr1); 168 | expect( result2 ).toEqual(arr2); 169 | 170 | expect( result1[3].foo ).toEqual('bar'); 171 | expect( result2[0] ).toEqual('foo'); 172 | })); 173 | 174 | it('should put a key value object into the locker via first param', inject((locker) => { 175 | var obj = { 176 | foo: 'bar', 177 | bar: 'baz', 178 | baz: { 179 | foo: 'baz' 180 | }, 181 | bob: { 182 | lorem: true 183 | } 184 | }; 185 | 186 | locker.put(obj); 187 | 188 | expect( locker.get('foo') ).toEqual('bar'); 189 | expect( locker.get('baz') ).toEqual({ foo: 'baz' }); 190 | expect( locker.get('bob').lorem ).toBeTruthy(); 191 | })); 192 | 193 | describe('when passing a function as the second param', () => { 194 | 195 | it('should put an item into the locker', inject((locker) => { 196 | 197 | locker.put('fnKey', () => 12 * 12); 198 | 199 | expect( locker.get('fnKey') ).toEqual(144); 200 | })); 201 | 202 | it('should pass the current value of the item to the function', inject((locker) => { 203 | 204 | locker.put('fnKey', ['foo', 'bar']); 205 | 206 | locker.put('fnKey', (param) => { 207 | param.push('baz'); 208 | return param; 209 | }); 210 | 211 | expect( locker.get('fnKey') ).toEqual(['foo', 'bar', 'baz']); 212 | })); 213 | 214 | it('should pass undefined to the function if the current item value does not exist', inject((locker) => { 215 | var value = null; 216 | 217 | expect(locker.get('fnKey')).not.toBeDefined(); 218 | 219 | locker.put('fnKey', (param) => { 220 | value = param; 221 | return 2; 222 | }); 223 | 224 | expect(locker.get('fnKey')).toEqual(2); 225 | expect(value).not.toBeDefined(); 226 | })); 227 | }); 228 | 229 | it('should pass the default value to the function if the current item value does not exist and a default was specified', inject((locker) => { 230 | var value = null; 231 | 232 | expect(locker.get('fnKey')).not.toBeDefined(); 233 | 234 | locker.put('fnKey', (param) => { 235 | value = param; 236 | return 2; 237 | }, 1); 238 | 239 | expect(locker.get('fnKey')).toEqual(2); 240 | expect(value).toBe(1); 241 | })); 242 | 243 | it('should put an item into the locker when passing a function as first param', inject((locker) => { 244 | 245 | locker.put(() => { 246 | return { 247 | someKey: ['some', 'array'], 248 | anotherKey: { foo: 'bar', baz: true } 249 | }; 250 | }); 251 | 252 | expect( locker.get('someKey') ).toBeDefined(); 253 | expect( locker.get('anotherKey') ).toBeDefined(); 254 | 255 | expect( angular.isArray(locker.get('someKey')) ).toBeTruthy(); 256 | expect( angular.isObject(locker.get('anotherKey')) ).toBeTruthy(); 257 | })); 258 | 259 | it('should put an item into the locker if it doesn\'t already exist', inject((locker) => { 260 | 261 | locker.put('foo', 'loremipsumdolorsitamet'); 262 | var added = locker.add('foo', ['foo', 'bar', 'baz']); 263 | 264 | locker.put('bar', 'foobarbazbob'); 265 | var added2 = locker.add('bar1', 'foobazbob'); 266 | 267 | expect( added ).toBeFalsy(); 268 | expect( added2 ).toBeTruthy(); 269 | 270 | expect( locker.get('foo') ).toEqual('loremipsumdolorsitamet'); 271 | expect( locker.get('bar1') ).toEqual('foobazbob'); 272 | })); 273 | 274 | it('should put an item into the locker in a different namespace', inject((locker) => { 275 | locker.put('foo', 'defaultNamespace'); 276 | locker.namespace('someOtherNamespace').put('foo', 'newNamespace'); 277 | locker.namespace(false).put('noNamespace', [true]); 278 | 279 | expect( locker.get('foo') ).toEqual('defaultNamespace'); 280 | expect( locker.namespace('someOtherNamespace').get('foo') ).toEqual('newNamespace'); 281 | expect( locker.namespace(false).get('noNamespace') ).toEqual([true]); 282 | })); 283 | 284 | it('should return false if key/value params are missing', inject((locker) => { 285 | 286 | var result1 = locker.put('aKey'); 287 | var result2 = locker.put(null, 'aVal'); 288 | 289 | expect( result1 && result2 ).toBeFalsy(); 290 | })); 291 | 292 | it('should fail silently if value cannot be serialized and unserialized', inject((locker) => { 293 | 294 | spyOn(angular, 'toJson').and.throwError(new Error()); 295 | spyOn(angular, 'fromJson').and.throwError(new Error()); 296 | 297 | var result = locker.put('foo', ['bar', 'baz']).get('foo'); 298 | 299 | expect( result ).toBeDefined(); 300 | })); 301 | 302 | it('should catch the error when the browser reports storage is full', inject(($window, locker) => { 303 | 304 | spyOn(locker, '_checkSupport').and.returnValue(true); 305 | 306 | var error = new Error(); 307 | error.name = 'QUOTA_EXCEEDED_ERR'; 308 | 309 | spyOn($window.localStorage, 'setItem').and.throwError(error); 310 | 311 | expect(() => { 312 | locker.put('someKey', ['foo']); 313 | }).toThrowError(); 314 | })); 315 | 316 | it('should catch the error when an item couldn\'t be added for some other reason', inject(($window, locker) => { 317 | 318 | spyOn(locker, '_checkSupport').and.returnValue(true); 319 | spyOn($window.localStorage, 'setItem').and.throwError(new Error()); 320 | 321 | expect(() => { 322 | locker.put('someKey', ['foo']); 323 | }).toThrowError(); 324 | })); 325 | 326 | it('should throw an error when adding item and no browser support detected', inject(($window, locker) => { 327 | 328 | spyOn(locker, '_checkSupport').and.returnValue(false); 329 | 330 | expect(() => { 331 | locker.put('someKey', ['foo']); 332 | }).toThrowError(); 333 | })); 334 | 335 | it('should trigger added event when adding item to locker for the first time', () => { 336 | 337 | module(function(lockerProvider) { 338 | lockerProvider.defaults({ driver: 'session' }); 339 | }); 340 | 341 | inject((locker, $rootScope) => { 342 | spyOn($rootScope, '$emit'); 343 | spyOn(locker, '_exists').and.returnValue(false); 344 | 345 | locker.put('foo', 'bar'); 346 | 347 | expect($rootScope.$emit).toHaveBeenCalledWith('locker.item.added', { 348 | key: 'foo', 349 | value: 'bar', 350 | driver: 'session', 351 | namespace: 'locker' 352 | }); 353 | }); 354 | }); 355 | 356 | it('should not trigger events when events are disabled', () => { 357 | module(function(lockerProvider) { 358 | lockerProvider.defaults({ eventsEnabled: false }); 359 | }); 360 | 361 | inject((locker, $rootScope) => { 362 | spyOn($rootScope, '$emit'); 363 | locker.put('foo', 'bar'); 364 | 365 | expect($rootScope.$emit).not.toHaveBeenCalled(); 366 | }); 367 | }); 368 | 369 | it('should trigger updated event when updating item already in locker', inject((locker, $rootScope) => { 370 | spyOn($rootScope, '$emit'); 371 | 372 | locker.put('foo', 'bar'); 373 | locker.put('foo', 'baz'); 374 | 375 | expect($rootScope.$emit).toHaveBeenCalledWith('locker.item.updated', { 376 | key: 'foo', 377 | oldValue: 'bar', 378 | newValue: 'baz', 379 | driver: 'local', 380 | namespace: 'locker' 381 | }); 382 | })); 383 | }); 384 | 385 | describe('switching drivers/namespaces', () => { 386 | 387 | it('should switch drivers when chained', () => { 388 | module(function(lockerProvider) { 389 | lockerProvider.defaults().driver = 'local'; 390 | }); 391 | 392 | inject((locker) => { 393 | locker.driver('session').put('foo', 'bar'); 394 | expect( locker.get('foo') ).not.toBeDefined(); 395 | }); 396 | }); 397 | 398 | it('should switch namespaces when chained', inject((locker) => { 399 | 400 | locker.namespace('fooBar').put('foo', 'bar'); 401 | 402 | expect( locker.get('foo') ).not.toBeDefined(); 403 | })); 404 | 405 | }); 406 | 407 | describe('retrieving items from locker', () => { 408 | 409 | it('should return specified default value if item not in locker', inject((locker) => { 410 | var obj = { foo: 'bar', bar: 123, baz: true }; 411 | 412 | locker.put('somethingThatDoesExist', 'exists'); 413 | 414 | var result = locker.get('somethingThatDoesExist', 'defaultValue'); 415 | var result2 = locker.get('somethingElseThatDoesntExist', { foo: 'bar', bar: 123, baz: true }); 416 | 417 | var result3 = locker.get('somethingElseThatDoesntExist', false); 418 | var result4 = locker.get('somethingElseThatDoesntExist', ''); 419 | var result5 = locker.get('somethingElseThatDoesntExist', 'NaN'); 420 | var result6 = locker.get('somethingElseThatDoesntExist', null); 421 | var result7 = locker.get('somethingElseThatDoesntExist', 0); 422 | 423 | expect( result3 ).toEqual(false); 424 | expect( result4 ).toEqual(''); 425 | expect( result5 ).toEqual('NaN'); 426 | expect( result6 ).toEqual(null); 427 | expect( result7 ).toEqual(0); 428 | 429 | expect( result ).not.toEqual('defaultValue'); 430 | expect( result2 ).toEqual(obj); 431 | })); 432 | 433 | it('should return an object containing the key/value pairs passed in via array', inject((locker) => { 434 | 435 | locker.put(() => { 436 | return { 437 | something: 'some value', 438 | anotherThing: ['foo', 'bar'], 439 | lorem: true, 440 | foo: null 441 | }; 442 | }); 443 | 444 | var result = locker.get(['something', 'anotherThing']); 445 | 446 | expect( angular.isObject(result) ).toBeTruthy(); 447 | expect( result.something ).toEqual('some value'); 448 | expect( result ).not.toEqual( jasmine.objectContaining({ lorem: true }) ); 449 | 450 | })); 451 | 452 | it('should return a value and then delete the item', inject((locker) => { 453 | var str = 'someVal456'; 454 | locker.put('someKey123', str); 455 | 456 | var value = locker.pull('someKey123'); 457 | 458 | expect( value ).toEqual(str); 459 | expect( locker.get('someKey123') ).not.toBeDefined(); 460 | })); 461 | 462 | it('should return all items within current namespace', inject((locker) => { 463 | 464 | for (var i=0; i<20; i++) { 465 | locker.namespace('foo.bar').put('aKey' + i, 'aVal' + i); 466 | } 467 | 468 | locker.namespace('foo.bar').put('something.foo.bar', ['someValue']); 469 | 470 | var all = locker.namespace('foo.bar').all(); 471 | var none = locker.namespace('something').all(); 472 | 473 | expect( angular.isObject(all) && angular.isObject(none) ).toBeTruthy(); 474 | expect( Object.keys(none).length ).toEqual(0); 475 | 476 | expect( all ).toEqual(jasmine.objectContaining({ 'aKey12': 'aVal12' })); 477 | 478 | expect( Object.keys(all) ).toContain('aKey12'); 479 | expect( Object.keys(all) ).toContain('something.foo.bar'); 480 | expect( Object.keys(all).length ).toEqual(21); 481 | })); 482 | 483 | it('should count the items within current namespace', inject((locker) => { 484 | for (var i=0; i<20; i++) { 485 | locker.put('aKey' + i, 'aVal' + i); 486 | } 487 | 488 | locker.put('something.foo.bar', ['someValue']); 489 | 490 | expect( locker.count() ).toEqual(21); 491 | expect(locker.namespace('something').count()).toEqual(0); 492 | })); 493 | 494 | it('should throw an error when getting item and no browser support detected', inject(($window, locker) => { 495 | 496 | spyOn(locker, '_checkSupport').and.returnValue(false); 497 | spyOn(locker, 'has').and.returnValue(true); 498 | 499 | expect(() => { 500 | locker.get('someKey'); 501 | }).toThrowError(); 502 | })); 503 | 504 | }); 505 | 506 | describe('removing items from locker', () => { 507 | 508 | it('should remove an item from locker', inject((locker) => { 509 | locker.put('someKey', 'someVal'); 510 | 511 | locker.forget('someKey'); 512 | 513 | expect( locker.get('someKey') ).not.toBeDefined(); 514 | })); 515 | 516 | it('should remove an item from locker when passing a function', inject((locker) => { 517 | locker.put('someKey', 'someVal'); 518 | 519 | locker.forget(() => 'someKey'); 520 | 521 | expect( locker.get('someKey') ).not.toBeDefined(); 522 | })); 523 | 524 | it('should remove multiple items from locker when passing a function', inject((locker) => { 525 | locker.put(() => { 526 | return { 527 | 'something': 'some value', 528 | 'anotherThing': ['foo', 'bar'], 529 | 'lorem': true 530 | }; 531 | }); 532 | 533 | locker.forget(() => ['something', 'anotherThing']); 534 | 535 | expect( locker.get('something') ).not.toBeDefined(); 536 | expect( locker.get('anotherThing') ).not.toBeDefined(); 537 | expect( locker.get('lorem') ).toBeTruthy(); 538 | })); 539 | 540 | it('should remove multiple items from locker by passing an array', inject((locker) => { 541 | 542 | locker.put('objectKey', {foo: 'bar'}); 543 | locker.put('arrayKey', ['foo', 'bar']); 544 | locker.put('foo', 'bar'); 545 | 546 | locker.forget(['objectKey', 'arrayKey1', 'foo']); 547 | 548 | expect( locker.get('objectKey') ).not.toBeDefined(); 549 | expect( locker.get('arrayKey1') ).not.toBeDefined(); 550 | expect( locker.get('foo') ).not.toBeDefined(); 551 | })); 552 | 553 | it('should remove all items within a namespace', inject((locker) => { 554 | 555 | locker.put('foo', 'bar'); 556 | 557 | locker.namespace('otherNamespace').put('fooOther', 'barOther'); 558 | 559 | locker.clean(); 560 | 561 | expect( locker.namespace('otherNamespace').get('fooOther') ).toEqual('barOther'); 562 | expect( locker.get('foo') ).not.toBeDefined(); 563 | })); 564 | 565 | it('should empty the locker', inject((locker) => { 566 | 567 | locker.put('anotherKey', { someObj: true, foo: 'barbaz' }); 568 | 569 | locker.empty(); 570 | 571 | expect( locker.get('anotherKey') ).not.toBeDefined(); 572 | 573 | })); 574 | 575 | it('should throw an error when removing item and no browser support detected', inject(($window, locker) => { 576 | 577 | spyOn(locker, '_checkSupport').and.returnValue(false); 578 | 579 | expect(() => { 580 | locker.forget('someKey'); 581 | }).toThrowError(); 582 | })); 583 | 584 | it('should trigger forgotten event when removing item from locker', inject((locker, $rootScope) => { 585 | spyOn($rootScope, '$emit'); 586 | 587 | locker.put('foo', 'bar'); 588 | 589 | locker.forget('foo'); 590 | 591 | expect($rootScope.$emit).toHaveBeenCalledWith('locker.item.forgotten', { 592 | key: 'foo', 593 | driver: 'local', 594 | namespace: 'locker' 595 | }); 596 | })); 597 | 598 | }); 599 | 600 | describe('checking existence in locker', () => { 601 | 602 | it('should determine whether an item exists in locker', inject((locker) => { 603 | locker.put('randKey', Math.random()); 604 | 605 | expect( locker.has('randKey') ).toBeTruthy(); 606 | expect( locker.has('loremipsumdolorsitamet') ).toBeFalsy(); 607 | })); 608 | 609 | it('should determine whether an item exists in locker when passing a function', inject((locker) => { 610 | locker.put('randKey', Math.random()); 611 | 612 | var result = locker.has(() => 'randKey'); 613 | 614 | expect(result).toBeTruthy(); 615 | expect( locker.has('loremipsumdolorsitamet') ).toBeFalsy(); 616 | })); 617 | 618 | it('should determine whether an item exists in locker within another namespace', inject((locker) => { 619 | locker.namespace('differentNs').put('randKeyNs', Math.random()); 620 | 621 | expect( locker.namespace('differentNs').has('randKeyNs') ).toBeTruthy(); 622 | expect( locker.namespace('loremipsumdolorsitamet').has('randKeyNs') ).toBeFalsy(); 623 | })); 624 | 625 | it('should throw an error when checking has item and no browser support detected', inject(($window, locker) => { 626 | 627 | spyOn(locker, '_checkSupport').and.returnValue(false); 628 | 629 | expect(() => { 630 | locker.has('someKey'); 631 | }).toThrowError(); 632 | })); 633 | 634 | it('should check an item exists when hasOwnProperty does not work', inject((locker) => { 635 | locker.namespace('differentNs').put('randKeyNs', Math.random()); 636 | spyOn(locker._driver, 'hasOwnProperty').and.returnValue(void 0); 637 | 638 | expect( locker.namespace('differentNs').has('randKeyNs') ).toBeTruthy(); 639 | expect( locker.namespace('loremipsumdolorsitamet').has('randKeyNs') ).toBeFalsy(); 640 | })); 641 | 642 | }); 643 | 644 | describe('checking browser support', () => { 645 | 646 | it('should bind a variable to the scope', inject((locker, $rootScope) => { 647 | locker.bind($rootScope, 'foo'); 648 | 649 | $rootScope.foo = ['bar', 'baz']; 650 | $rootScope.$apply(); 651 | 652 | expect(locker.get('foo')).toEqual(['bar', 'baz']); 653 | 654 | $rootScope.foo = 123; 655 | $rootScope.$apply(); 656 | 657 | expect(locker.get('foo')).toEqual(123); 658 | expect(Object.keys(locker._watchers).length).toEqual(1); 659 | })); 660 | 661 | it('should bind a variable to the scope with a default', inject((locker, $rootScope) => { 662 | locker.bind($rootScope, 'foo', 'defaultVal'); 663 | 664 | expect($rootScope.foo).toEqual('defaultVal'); 665 | expect(locker.get('foo')).toEqual('defaultVal'); 666 | })); 667 | 668 | it('should unbind a variable from the scope', inject((locker, $rootScope) => { 669 | locker.bind($rootScope, 'foo'); 670 | locker.bind($rootScope, 'bar'); 671 | 672 | $rootScope.foo = ['bar', 'baz']; 673 | $rootScope.$apply(); 674 | 675 | expect(locker.get('foo')).toEqual(['bar', 'baz']); 676 | 677 | locker.unbind($rootScope, 'foo'); 678 | 679 | expect($rootScope.foo).toBeUndefined(); 680 | expect(Object.keys(locker._watchers).length).toEqual(1); 681 | })); 682 | 683 | }); 684 | 685 | describe('checking browser support', () => { 686 | 687 | it('should return true if storage is supported', inject(($window, locker) => { 688 | 689 | expect( locker.supported() ).toBeTruthy(); 690 | 691 | })); 692 | 693 | it('should return false if storage is not supported', inject(($window, locker) => { 694 | 695 | spyOn($window.localStorage, 'setItem').and.throwError(new Error()); 696 | 697 | expect( locker.supported() ).toBeFalsy(); 698 | 699 | })); 700 | 701 | }); 702 | 703 | describe('misc', () => { 704 | 705 | it('should get the currently set namespace', inject((locker) => { 706 | expect( locker.getNamespace() ).toEqual('locker'); 707 | expect( locker.namespace('foo').getNamespace() ).toEqual('foo'); 708 | })); 709 | 710 | it('should get the currently set driver', inject(($window, locker) => { 711 | expect( locker.getDriver() ).toEqual($window.localStorage); 712 | expect( locker.driver('session').getDriver() ).toEqual($window.sessionStorage); 713 | })); 714 | 715 | }); 716 | 717 | }); 718 | }); 719 | --------------------------------------------------------------------------------