Created
December 11, 2015 00:35
-
-
Save fmal/5e21dcd6725f9bf6a63e to your computer and use it in GitHub Desktop.
Angular state service
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* State | |
* @ngdoc service | |
* @name State | |
* @description | |
* this service handles all app state. | |
* you can also use it as a basic pub/sub with the publish() and subscribe() functions | |
* this is able to function and synchronize over multiple Tabs. | |
* all State is persistet to localStorage | |
* **#######################################################################** | |
* **## DO NOT SET STATE PROPERTIES WITH LITERALS! USE THE STATE CONSTANTS IN constants.js ## ** | |
* **#######################################################################** | |
* @todo it is still possible to overwrite a new state with an old one. add something like a modifiedAt prop | |
*/ | |
angular.module("appname").service("State", function($log, ACTIONS){ | |
var storageName = "appState"; | |
var _state = {}; //current State | |
var _listeners = {}; | |
/** | |
* load a saved state from localStorage | |
* @memberOf State | |
* @function init | |
*/ | |
this.init = function(){ | |
var state = this.load(); | |
_state = state; | |
$log.debug("State (init): %o",_state); | |
} | |
/** | |
* get the saved state | |
* @return {Object} saved state | |
* @memberOf State | |
* @function load | |
*/ | |
this.load = function(){ | |
var stateJSON = localStorage.getItem(storageName); | |
if(stateJSON == undefined || stateJSON == null){ | |
$log.debug("no state in localStorage") | |
return {} | |
} | |
else{ | |
try{ | |
var data = JSON.parse(stateJSON); | |
//remove null values as they only take up memory | |
for(var i in data){ | |
if(data[i] == null){ | |
delete data[i]; | |
} | |
} | |
return data; | |
$log.debug("loaded state from localStorage") | |
}catch(e){ | |
$log.debug("could not load state") | |
return {} | |
//do nothing and let state be default | |
} | |
} | |
$log.debug("finished state loading") | |
} | |
/** | |
* get the State from localStorage and update the current one | |
* this function is debounced to 200ms | |
* @memberOf State | |
* @function updateFromStorage | |
*/ | |
this.updateFromStorage = _.debounce(function(e){ | |
var changedState = this.load(); | |
var mergedState = _.merge({},_state, changedState) | |
$log.debug("State (old): %o", _state); | |
$log.debug("State (new): %o", changedState); | |
$log.debug("State (merged): %o", mergedState) | |
//check for important changes | |
if(!_state[ACTIONS.FILTERPLZ] || _.isEqual(changedState[ACTIONS.FILTERPLZ].sort(), _state[ACTIONS.FILTERPLZ].sort()) == false){ | |
this.publish(ACTIONS.FILTERPLZ, changedState[ACTIONS.FILTERPLZ]); | |
} | |
//removing sets a key's value to null (because this gets propagated through onstorage) | |
//beacuse of this we need to delete the nulled values (to get the wanted result) | |
for(var i in mergedState){ | |
if(mergedState[i] == null){ | |
delete mergedState[i]; | |
} | |
} | |
_state = mergedState; | |
$log.debug("State: update from localStorage") | |
}.bind(this),200); | |
/** | |
* setter function. persists changes | |
* **this notifies all subscribers** | |
* @param {String} key key | |
* @param {any} data value (has ot be JSON parse-able) | |
* @memberOf State | |
* @function set | |
* @example | |
* State.set(42) //State.get("item") => 42 | |
*/ | |
this.set = function(key, data){ | |
if(typeof key !== "string"){ | |
return Error("key has to be a string") | |
} | |
if(data === undefined){return} | |
_state[key] = data; | |
this.save(); | |
this.publish(key,data); | |
return data; | |
} | |
/** | |
* returns a setter function for the key | |
* **calling the create setter notifies all subscribers** | |
* @param {String} key key | |
* @return {Function} setter | |
* @memberOf State | |
* @function setter | |
* @example | |
* var setter = State.setter("item"); | |
* setter(42) //State.get("item") => 42 | |
*/ | |
this.setter = function(key){ | |
return function(dta){ | |
if(dta === undefined){return} | |
_state[key] = dta; | |
this.save(); | |
this.publish(key,data); | |
return data; | |
}.bind(this) | |
} | |
/** | |
* get an key | |
* @param {String} key key | |
* @return {any} saved value | |
* @memberOf State | |
* @function get | |
* @example | |
* //let item be 42 | |
* var a = State.get("item"); | |
* a // => 42 | |
*/ | |
this.get = function(key){ | |
return _state[key]; | |
} | |
/** | |
* returns a getter function for the key | |
* @param {String} key key | |
* @return {Function} getter | |
* @memberOf State | |
* @function getter | |
* @example | |
* //let item be 42 | |
* var a = State.getter("item"); | |
* a() // => 42 | |
*/ | |
this.getter = function(key){ | |
return function(){ | |
return _state[key]; | |
} | |
} | |
/** | |
* returns a setter/getter for a specific key | |
* ** setting a prop notifies all subscribers** | |
* @param {String} key key to get the setter/getter for | |
* @return {Function} setter/getter. call without params to get the value | |
* @memberOf State | |
* @function prop | |
* @example | |
* var filter = State.prop("filter") | |
* filter() // => 123 | |
* filter(42) // => 42 | |
* filter() // => 42 | |
*/ | |
this.prop = function(key){ | |
return function(data){ | |
if(data === undefined){ | |
//was called as getter | |
return _state[key]; | |
} | |
else{ | |
_state[key] = data; | |
this.save(); | |
this.publish(key,data); | |
return data; | |
} | |
}.bind(this); | |
}.bind(this); | |
/** | |
* remove key from state and persist | |
* @param {String} key key | |
* @memberOf State | |
* @function remove | |
*/ | |
this.remove = function(key){ | |
_state[key] = null; | |
this.save(); | |
} | |
/** | |
* notify all listeners for a key | |
* publish allows you to broadcast data without writing it to the locaStorage. | |
* You can use this as lightweight alternative to angulars event system (because this doesn't run over $rootScope) | |
* @param {String} key key | |
* @param {any} data value (has ot be JSON parse-able) | |
*/ | |
this.publish = function(key, data){ | |
$log.debug("PUB: %s %o", key, data) | |
if(data === undefined){return} | |
if(_listeners[key]){ | |
_listeners[key].map(function(listener){ | |
listener(data); | |
}) | |
} | |
} | |
/** | |
* get notified for all value changes | |
* for the key | |
* @param {String} key key | |
* @param {function} listener callback to be executed on change | |
* @param {bool} immmidiate immidiately call the listener on registration | |
*/ | |
this.subscribe = function(key, listener, immediate){ | |
if(key == undefined){ | |
$log.log(arguments); | |
} | |
$log.debug("SUB: %s", key) | |
if(!_listeners[key]){ | |
_listeners[key] = []; | |
} | |
_listeners[key].push(listener); | |
if(immediate){ | |
listener(this.get(key)); | |
} | |
}; | |
/** | |
* removes a listener from a prop | |
* @param {String} key key | |
* @param {function} listener callback to remove | |
*/ | |
this.unsubscribe = function(key, listener){ | |
$log.debug("UNSUB: %s", key) | |
_listeners[key].splice(_listeners[key].indexOf(listener), 1) | |
} | |
/** | |
* persist the current State | |
* @memberOf State | |
* @function save | |
*/ | |
this.save = function(){ | |
if(_state){ | |
localStorage.setItem(storageName, JSON.stringify(_state)) | |
$log.debug("State: saved"); | |
} | |
else{ | |
$log.debug("State: did not save state because current State is falsey") | |
} | |
} | |
/** | |
* clear all state from the application | |
*/ | |
this.clear = function(){ | |
_state = null; | |
localStorage.removeItem(storageName) | |
} | |
//save state before leaving the page | |
window.addEventListener("beforeunload", this.save, false); | |
//update state if it changes | |
//the storage event is fired if the storage is changed externally (this includes changes made in another tab) | |
window.addEventListener("storage", this.updateFromStorage, false) | |
window.State = this; | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment