tymondesigns/angular-locker

View on GitHub
src/angular-locker.js

Summary

Maintainability
D
2 days
Test Coverage
/**
 * angular-locker
 *
 * A simple & configurable abstraction for local/session storage in angular projects.
 *
 * @link https://github.com/tymondesigns/angular-locker
 * @author Sean Tymon @tymondesigns
 * @license MIT License, http://www.opensource.org/licenses/MIT
 */

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        define(function () {
            return factory(root.angular || (window && window.angular));
        });
    } else if (typeof exports === 'object') {
        module.exports = factory(root.angular || (window && window.angular));
    } else {
        factory(root.angular);
    }
})(this, function (angular) {

    'use strict';

    return angular.module('angular-locker', [])

    .provider('locker', function () {

        /**
         * If value is a function then execute, otherwise return
         *
         * @private
         *
         * @param  {Mixed}  value  The value to execute or return
         * @param  {Mixed}  param  The parameter to pass to function if applicable
         *
         * @return {Mixed}
         */
        var _value = function (value, param) {
            return angular.isFunction(value) ? value(param) : value;
        };

        /**
         * Determine whether a value is defined and not null
         *
         * @private
         *
         * @param  {Mixed}  value  The value to check
         *
         * @return {Boolean}
         */
        var _defined = function (value) {
            return angular.isDefined(value) && value !== null;
        };

        /**
         * Trigger an error
         *
         * @private
         * @throws {Error}
         *
         * @param  {String}  msg  The error message
         */
        var _error = function (msg) {
            throw new Error('[angular-locker] ' + msg);
        };

        /**
         * Set the defaults
         *
         * @private
         *
         * @type {Object}
         */
        var defaults = {
            driver: 'local',
            namespace: 'locker',
            eventsEnabled: true,
            separator: '.',
            extend: {}
        };

        return {

            /**
             * Allow the defaults to be specified via the `lockerProvider`
             *
             * @param {Object}  value  The defaults to override
             */
            defaults: function (value) {
                if (! _defined(value)) return defaults;

                angular.forEach(value, function (val, key) {
                    if (defaults.hasOwnProperty(key)) defaults[key] = val;
                });
            },

            /**
             * The locker service
             */
            $get: ['$window', '$rootScope', '$parse', function ($window, $rootScope, $parse) {

                /**
                 * Define the Locker class
                 *
                 * @public
                 * @constructor
                 *
                 * @param {Object}  options  The config options to initialize with
                 */
                function Locker (options) {

                    /**
                     * The config options
                     *
                     * @private
                     *
                     * @type {Object}
                     */
                    this._options = options;

                    /**
                     * Out of the box drivers
                     *
                     * @private
                     *
                     * @type {Object}
                     */
                    this._registeredDrivers = angular.extend({
                        local: $window.localStorage,
                        session: $window.sessionStorage
                    }, options.extend);

                    /**
                     * Get the Storage instance from the key
                     *
                     * @private
                     *
                     * @param  {String}  driver  The storage driver identifier
                     *
                     * @return {Storage}
                     */
                    this._resolveDriver = function (driver) {
                        if (! this._registeredDrivers.hasOwnProperty(driver)) {
                            _error('The driver "' + driver + '" was not found.');
                        }

                        return this._registeredDrivers[driver];
                    };

                    /**
                     * The driver instance
                     *
                     * @private
                     *
                     * @type {Storage}
                     */
                    this._driver = this._resolveDriver(options.driver);

                    /**
                     * The namespace value
                     *
                     * @private
                     *
                     * @type {String}
                     */
                    this._namespace = options.namespace;

                    /**
                     * Separates the namespace from the keys
                     *
                     * @private
                     *
                     * @type {String}
                     */
                    this._separator = options.separator;

                    /**
                     * Store the watchers here so we can un-register them later
                     *
                     * @private
                     *
                     * @type {Object}
                     */
                    this._watchers = {};

                    /**
                     * Check browser support
                     *
                     * @private
                     * @see github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js#L38-L47
                     *
                     * @param  {String}  driver  The driver to check support with
                     *
                     * @return {Boolean}
                     */
                    this._checkSupport = function (driver) {
                        if (! _defined(this._supported)) {
                            var l = 'l';
                            try {
                                this._resolveDriver(driver || options.driver).setItem(l, l);
                                this._resolveDriver(driver || options.driver).removeItem(l);
                                this._supported = true;
                            } catch (e) {
                                this._supported = false;
                            }
                        }

                        return this._supported;
                    };

                    /**
                     * Build the storage key from the namspace
                     *
                     * @private
                     *
                     * @param  {String}  key  The key to build the prefix with
                     *
                     * @return {String}
                     */
                    this._getPrefix = function (key) {
                        if (! this._namespace) return key;

                        return this._namespace + this._separator + key;
                    };

                    /**
                     * Try to encode value as json, or just return the value upon failure
                     *
                     * @private
                     *
                     * @param  {Mixed}  value  The value to serialize
                     *
                     * @return {Mixed}
                     */
                    this._serialize = function (value) {
                        try {
                            return angular.toJson(value);
                        } catch (e) {
                            return value;
                        }
                    };

                    /**
                     * Try to parse value as json, if it fails then it probably isn't json
                     * so just return it
                     *
                     * @private
                     *
                     * @param  {String}  value  The value to unserialize
                     *
                     * @return {Mixed}
                     */
                    this._unserialize = function (value) {
                        try {
                            return angular.fromJson(value);
                        } catch (e) {
                            return value;
                        }
                    };

                    /**
                     * Trigger an event
                     *
                     * @private
                     *
                     * @param  {String}  name     The name of the event to trigger
                     * @param  {Object}  payload  The data to pass along with event
                     */
                    this._event = function (name, payload) {
                        if (this._options.eventsEnabled) {
                            $rootScope.$emit('locker.' + name, angular.extend(payload, {
                                driver: this._options.driver,
                                namespace: this._namespace,
                            }));
                        }
                    };

                    /**
                     * Add to storage
                     *
                     * @private
                     * @throws {Error}   if browser support fails
                     *
                     * @param  {String}  key    The key to add
                     * @param  {Mixed}   value  The value to add
                     */
                    this._setItem = function (key, value) {
                        if (! this._checkSupport()) {
                            _error('The browser does not support the "' + options.driver + '" driver');
                        }

                        try {
                            var oldVal = this._getItem(key);
                            this._driver.setItem(this._getPrefix(key), this._serialize(value));
                            if (this._exists(key) && ! angular.equals(oldVal, value)) {
                                this._event('item.updated', { key: key, oldValue: oldVal, newValue: value });
                            } else {
                                this._event('item.added', { key: key, value: value });
                            }
                        } catch (e) {
                            if (['QUOTA_EXCEEDED_ERR',
                                'NS_ERROR_DOM_QUOTA_REACHED',
                                'QuotaExceededError'].indexOf(e.name) !== -1) {
                                _error('The browser storage quota has been exceeded');
                            } else {
                                _error('Could not add item with key "' + key + '"');
                            }
                        }
                    };

                    /**
                     * Get from storage
                     *
                     * @private
                     * @throws {Error}   if browser support fails
                     *
                     * @param  {String}  key  The key to get
                     *
                     * @return {Mixed}
                     */
                    this._getItem = function (key) {
                        if (! this._checkSupport()) {
                            _error('The browser does not support the "' + options.driver + '" driver');
                        }

                        return this._unserialize(this._driver.getItem(this._getPrefix(key)));
                    };

                    /**
                     * Exists in storage
                     *
                     * @private
                     * @throws {Error}   if browser support fails
                     *
                     * @param  {String}  key  The key to check for existence
                     *
                     * @return {Boolean}
                     */
                    this._exists = function (key) {
                        if (! this._checkSupport()) {
                            _error('The browser does not support the "' + options.driver + '" driver');
                        }

                        return this._driver.hasOwnProperty(this._getPrefix(_value(key))) || !! this._getItem(key);
                    };

                    /**
                     * Remove from storage
                     *
                     * @private
                     * @throws {Error}   if browser support fails
                     *
                     * @param  {String}  key  The key to remove
                     *
                     * @return {Boolean}
                     */
                    this._removeItem = function (key) {
                        if (! this._checkSupport()) {
                            _error('The browser does not support the "' + options.driver + '" driver');
                        }

                        if (! this._exists(key)) return false;

                        this._driver.removeItem(this._getPrefix(key));
                        this._event('item.forgotten', { key: key });

                        return true;
                    };
                }

                /**
                 * Define the public api
                 *
                 * @public
                 *
                 * @type {Object}
                 */
                Locker.prototype = {

                    /**
                     * Add a new item to storage (even if it already exists)
                     *
                     * @public
                     *
                     * @param  {Mixed}  key    The key to add
                     * @param  {Mixed}  value  The value to add
                     * @param  {Mixed}  def    The default to pass to function if doesn't already exist
                     *
                     * @return {Locker|Boolean}
                     */
                    put: function (key, value, def) {
                        if (! _defined(key)) return false;
                        key = _value(key);

                        if (angular.isObject(key)) {
                            angular.forEach(key, function (value, key) {
                                this._setItem(key, _defined(value) ? value : def);
                            }, this);
                        } else {
                            if (! _defined(value)) return false;
                            var val = this._getItem(key);
                            this._setItem(key, _value(value, _defined(val) ? val : def));
                        }

                        return this;
                    },

                    /**
                     * Add an item to storage if it doesn't already exist
                     *
                     * @public
                     *
                     * @param  {Mixed}  key    The key to add
                     * @param  {Mixed}  value  The value to add
                     * @param  {Mixed}  def    The default to pass to function if doesn't already exist
                     *
                     * @return {Boolean}
                     */
                    add: function (key, value, def) {
                        if (! this.has(key)) {
                            this.put(key, value, def);
                            return true;
                        }

                        return false;
                    },

                    /**
                     * Retrieve the specified item from storage
                     *
                     * @public
                     *
                     * @param  {String|Array}  key  The key to get
                     * @param  {Mixed}         def  The default value if it does not exist
                     *
                     * @return {Mixed}
                     */
                    get: function (key, def) {
                        if (angular.isArray(key)) {
                            var items = {};
                            angular.forEach(key, function (k) {
                                if (this.has(k)) items[k] = this._getItem(k);
                            }, this);

                            return items;
                        }

                        if (! this.has(key)) return arguments.length === 2 ? def : void 0;

                        return this._getItem(key);
                    },

                    /**
                     * Determine whether the item exists in storage
                     *
                     * @public
                     *
                     * @param  {String|Function}  key  -  The key to remove
                     *
                     * @return {Boolean}
                     */
                    has: function (key) {
                        return this._exists(key);
                    },

                    /**
                     * Remove specified item(s) from storage
                     *
                     * @public
                     *
                     * @param  {String|Array}  key  The key or array of keys to remove
                     *
                     * @return {Object}
                     */
                    forget: function (key) {
                        key = _value(key);

                        if (angular.isArray(key)) {
                            key.map(this._removeItem, this);
                        } else {
                            this._removeItem(key);
                        }

                        return this;
                    },

                    /**
                     * Retrieve the specified item from storage and then remove it
                     *
                     * @public
                     *
                     * @param  {String|Array}  key  The key to pull from storage
                     * @param  {Mixed}         def  The default value if it does not exist
                     *
                     * @return {Mixed}
                     */
                    pull: function (key, def) {
                        var value = this.get(key, def);
                        this.forget(key);

                        return value;
                    },

                    /**
                     * Return all items in storage within the current namespace/driver
                     *
                     * @public
                     *
                     * @return {Object}
                     */
                    all: function () {
                        var items = {};
                        angular.forEach(this._driver, function (value, key) {
                            if (this._namespace) {
                                var prefix = this._namespace + this._separator;
                                if (key.indexOf(prefix) === 0) key = key.substring(prefix.length);
                            }
                            if (this.has(key)) items[key] = this.get(key);
                        }, this);

                        return items;
                    },

                    /**
                     * Get the storage keys as an array
                     *
                     * @public
                     *
                     * @return {Array}
                     */
                    keys: function () {
                        return Object.keys(this.all());
                    },

                    /**
                     * Remove all items set within the current namespace/driver
                     *
                     * @public
                     *
                     * @return {Locker}
                     */
                    clean: function () {
                        return this.forget(this.keys());
                    },

                    /**
                     * Empty the current storage driver completely. careful now.
                     *
                     * @public
                     *
                     * @return {Locker}
                     */
                    empty: function () {
                        this._driver.clear();

                        return this;
                    },

                    /**
                     * Get the total number of items within the current namespace
                     *
                     * @public
                     *
                     * @return {Integer}
                     */
                    count: function () {
                        return this.keys().length;
                    },

                    /**
                     * Bind a storage key to a $scope property
                     *
                     * @public
                     *
                     * @param  {Object}  $scope  The angular $scope object
                     * @param  {String}  key     The key in storage to bind to
                     * @param  {Mixed}   def     The default value to initially bind
                     *
                     * @return {Locker}
                     */
                    bind: function ($scope, key, def) {
                        if (! _defined( $scope.$eval(key) )) {
                            $parse(key).assign($scope, this.get(key, def));
                            this.add(key, def);
                        }

                        var self = this;
                        this._watchers[key + $scope.$id] = $scope.$watch(key, function (newVal) {
                            self.put(key, newVal);
                        }, angular.isObject($scope[key]));

                        return this;
                    },

                    /**
                     * Unbind a storage key from a $scope property
                     *
                     * @public
                     *
                     * @param  {Object}  $scope  The angular $scope object
                     * @param  {String}  key     The key to remove from bindings
                     *
                     * @return {Locker}
                     */
                    unbind: function ($scope, key) {
                        $parse(key).assign($scope, void 0);
                        this.forget(key);

                        var watchId = key + $scope.$id;

                        if (this._watchers[watchId]) {
                            // execute the de-registration function
                            this._watchers[watchId]();
                            delete this._watchers[watchId];
                        }

                        return this;
                    },

                    /**
                     * Set the storage driver on a new instance to enable overriding defaults
                     *
                     * @public
                     *
                     * @param  {String}  driver  The driver to switch to
                     *
                     * @return {Locker}
                     */
                    driver: function (driver) {
                        return this.instance(angular.extend(this._options, { driver: driver }));
                    },

                    /**
                     * Get the currently set driver
                     *
                     * @public
                     *
                     * @return {Storage}
                     */
                    getDriver: function () {
                        return this._driver;
                    },

                    /**
                     * Set the namespace on a new instance to enable overriding defaults
                     *
                     * @public
                     *
                     * @param  {String}  namespace  The namespace to switch to
                     *
                     * @return {Locker}
                     */
                    namespace: function (namespace) {
                        return this.instance(angular.extend(this._options, { namespace: namespace }));
                    },

                    /**
                     * Get the currently set namespace
                     *
                     * @public
                     *
                     * @return {String}
                     */
                    getNamespace: function () {
                        return this._namespace;
                    },

                    /**
                     * Check browser support
                     *
                     * @public
                     * @see github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js#L38-L47
                     *
                     * @param  {String}  driver  The driver to check support with
                     *
                     * @return {Boolean}
                     */
                    supported: function (driver) {
                        return this._checkSupport(driver);
                    },

                    /**
                     * Get a new instance of Locker
                     *
                     * @public
                     *
                     * @param  {Object}  options  The config options to instantiate with
                     *
                     * @return {Locker}
                     */
                    instance: function (options) {
                        return new Locker(options);
                    }
                };

                // return the default instance
                return new Locker(defaults);
            }]
        };

    }).name; // export module name for the likes of Browserify and Webpack

});