Skip to content

Instantly share code, notes, and snippets.

@fmal
Created December 11, 2015 00:35
Show Gist options
  • Save fmal/5e21dcd6725f9bf6a63e to your computer and use it in GitHub Desktop.
Save fmal/5e21dcd6725f9bf6a63e to your computer and use it in GitHub Desktop.
Angular state service
/**
* 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