Skip to content

Instantly share code, notes, and snippets.

@jcgregorio
Created August 15, 2017 04:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jcgregorio/fee80c6484d718f7e037916ba323abf6 to your computer and use it in GitHub Desktop.
Save jcgregorio/fee80c6484d718f7e037916ba323abf6 to your computer and use it in GitHub Desktop.
StateTools
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