Created
August 15, 2017 04:54
-
-
Save jcgregorio/fee80c6484d718f7e037916ba323abf6 to your computer and use it in GitHub Desktop.
StateTools
This file contains 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
this.StateTools = this.StateTools || {}; | |
(function(sr) { | |
"use strict"; | |
// A Promise that resolves when DOMContentLoaded has fired. | |
sr.DomReady = new Promise(function(resolve, reject) { | |
if (document.readyState != 'loading') { | |
// If readyState is already past loading then | |
// DOMContentLoaded has already fired, so just resolve. | |
resolve(); | |
} else { | |
document.addEventListener('DOMContentLoaded', resolve); | |
} | |
}); | |
// Namespace for utilities for working with URL query strings. | |
sr.query = {}; | |
// fromObject takes an object and encodes it into a query string. | |
// | |
// The reverse of this function is toObject. | |
sr.query.fromObject = function(o) { | |
var ret = []; | |
Object.keys(o).sort().forEach(function(key) { | |
if (Array.isArray(o[key])) { | |
o[key].forEach(function(value) { | |
ret.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); | |
}) | |
} else if (typeof(o[key]) == 'object') { | |
ret.push(encodeURIComponent(key) + '=' + encodeURIComponent(sr.query.fromObject(o[key]))); | |
} else { | |
ret.push(encodeURIComponent(key) + '=' + encodeURIComponent(o[key])); | |
} | |
}); | |
return ret.join('&'); | |
} | |
// toObject decodes a query string into an object | |
// using the 'target' as a source for hinting on the types | |
// of the values. | |
// | |
// "a=2&b=true" | |
// | |
// decodes to: | |
// | |
// { | |
// a: 2, | |
// b: true, | |
// } | |
// | |
// When given a target of: | |
// | |
// { | |
// a: 1.0, | |
// b: false, | |
// } | |
// | |
// Note that a target of {} would decode | |
// the same query string into: | |
// | |
// { | |
// a: "2", | |
// b: "true", | |
// } | |
// | |
// Only Number, String, Boolean, Object, and Array of String hints are supported. | |
sr.query.toObject = function(s, target) { | |
var target = target || {}; | |
var ret = {}; | |
var vars = s.split("&"); | |
for (var i=0; i<vars.length; i++) { | |
var pair = vars[i].split("=", 2); | |
if (pair.length == 2) { | |
var key = decodeURIComponent(pair[0]); | |
var value = decodeURIComponent(pair[1]); | |
if (target.hasOwnProperty(key)) { | |
switch (typeof(target[key])) { | |
case 'boolean': | |
ret[key] = value=="true"; | |
break; | |
case 'number': | |
ret[key] = Number(value); | |
break; | |
case 'object': // Arrays report as 'object' to typeof. | |
if (Array.isArray(target[key])) { | |
var r = ret[key] || []; | |
r.push(value); | |
ret[key] = r; | |
} else { | |
ret[key] = sr.query.toObject(value, target[key]); | |
} | |
break; | |
case 'string': | |
ret[key] = value; | |
break; | |
default: | |
ret[key] = value; | |
} | |
} else { | |
ret[key] = value; | |
} | |
} | |
} | |
return ret; | |
} | |
// Namespace for utilities for working with Objects. | |
sr.object = {}; | |
// Returns true if a and b are equal, covers Boolean, Number, String and | |
// Arrays and Objects. | |
sr.object.equals = function(a, b) { | |
if (typeof(a) != typeof(b)) { | |
return false | |
} | |
var ta = typeof(a); | |
if (ta == 'string' || ta == 'boolean' || ta == 'number') { | |
return a === b | |
} | |
if (ta == 'object') { | |
if (Array.isArray(ta)) { | |
return JSON.stringify(a) == JSON.stringify(b) | |
} else { | |
return sr.query.fromObject(a) == sr.query.fromObject(b) | |
} | |
} | |
} | |
// Returns an object with only values that are in o that are different | |
// from d. | |
// | |
// Only works shallowly, i.e. only diffs on the attributes of | |
// o and d, and only for the types that sr.object.equals supports. | |
sr.object.getDelta = function (o, d) { | |
var ret = {}; | |
Object.keys(o).forEach(function(key) { | |
if (!sr.object.equals(o[key], d[key])) { | |
ret[key] = o[key]; | |
} | |
}); | |
return ret; | |
}; | |
// Returns a copy of object o with values from delta if they exist. | |
sr.object.applyDelta = function (delta, o) { | |
var ret = {}; | |
Object.keys(o).forEach(function(key) { | |
if (delta.hasOwnProperty(key)) { | |
ret[key] = JSON.parse(JSON.stringify(delta[key])); | |
} else { | |
ret[key] = JSON.parse(JSON.stringify(o[key])); | |
} | |
}); | |
return ret; | |
}; | |
// Track the state of a page and reflect it to and from the URL. | |
// | |
// store - A Redux store. | |
// The state must be on Object and all the values in the Object | |
// must be Number, String, Boolean, Object, or Array of String. | |
// Doesn't handle NaN, null, or undefined. | |
// | |
// dispatch - A function called to update the state in store. | |
sr.urlReflector = function(store, dispatch) { | |
var defaultState = store.getState(); | |
var lastState = store.getState(); | |
store.subscribe(function() { | |
var state = store.getState(); | |
if (Object.keys(sr.object.getDelta(lastState, state)).length > 0) { | |
lastState = state; | |
var q = sr.query.fromObject(sr.object.getDelta(state, defaultState)); | |
window.history.pushState(null, "", window.location.origin + window.location.pathname + "#" + q); | |
} | |
}); | |
// stateChangeFromURL should be called when DOMContentLoaded. | |
var stateChangeFromURL = function() { | |
var delta = sr.query.toObject(window.location.hash.slice(1), defaultState); | |
lastState = sr.object.applyDelta(delta, defaultState); | |
dispatch(lastState); | |
} | |
window.addEventListener('popstate', stateChangeFromURL); | |
StateTools.DomReady.then(stateChangeFromURL); | |
} | |
})(this.StateTools); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment