uav-router
Version:
Simple hash-based routing for single page apps
497 lines (291 loc) • 8.54 kB
JavaScript
(() => {
let syncPending,
loadPending;
/**
* Convert an object to key=value&key=value notation.
*/
function serialize(obj) {
if (!obj) {
return '';
}
const parts = [];
Object.keys(obj).forEach(key => {
if (obj[key] !== undefined) {
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));
}
});
return parts.join('&');
}
/**
* Convert a serialzed string to an object.
*/
function deserialize(str) {
const obj = {};
const parts = decodeURIComponent(str).split('&');
parts.forEach(part => {
part = part.split('=');
if (part[0]) {
obj[part[0]] = part[1];
}
});
return obj;
}
function canNavigate(retry) {
return !router.canNavigate || router.canNavigate(retry);
}
/**
* Set router.params to reflect the current URL
*/
function syncParams() {
router.params = deserialize(location.hash.substring(1));
}
/**
* Set the URL to reflect router.params
*/
function syncURL() {
const hash = serialize(router.params);
if (!syncPending && location.hash.substring(1) !== hash) {
syncPending = true;
requestAnimationFrame(() => {
location.hash = hash;
});
}
}
/**
* Update router.params and reload the app
* after a change to the URL
*/
const hashchange = e => {
if (syncPending) {
syncPending = false;
} else {
function retry() {
location.href = e.newURL;
}
if (!canNavigate(retry)) {
syncPending = true;
location.hash = serialize(router.params);
return e.preventDefault();
}
syncParams();
router.load();
}
};
/**
* Take either an object or a serialized string,
* and return an object.
*/
function normalize(params) {
if (typeof params === 'string') {
params = deserialize(params);
}
return params || {};
}
function paramsAreDifferent(params) {
const newKeys = Object.keys(params);
const oldKeys = Object.keys(router.params);
if (newKeys.length !== oldKeys.length) {
return true;
}
const keys = newKeys.length > oldKeys.length ? newKeys : oldKeys;
for (let i = 0; i < keys.length; i++) {
if (router.params[keys[i]] !== params[keys[i]]) {
return true;
}
}
}
/**
* Merge the given params with router.params
*/
function mergeParams(params) {
let isDifferent;
params = normalize(params);
Object.keys(params).forEach(key => {
if (router.params[key] !== params[key]) {
isDifferent = true;
router.params[key] = params[key];
}
});
return isDifferent;
}
function replaceURL() {
const hash = '#' + serialize(router.params);
if (location.hash !== hash) {
history.replaceState(undefined, undefined, hash);
}
}
/**
* Remove the given parameters from router.params
*/
function removeParams(params) {
let isDifferent;
params.forEach(param => {
if (router.params[param] !== undefined) {
isDifferent = true;
delete router.params[param];
}
});
return isDifferent;
}
const url = {
/**
* Remove the provided keys from the URL
*/
remove(...params) {
if (removeParams(params)) {
url.set(router.params, true);
}
},
/**
* Remove the provided keys from the URL
* without adding a browser history entry
*/
removeReplace(...params) {
if (removeParams(params)) {
replaceURL();
}
},
/**
* Add the provided keys to the URL
*/
merge(params) {
if (mergeParams(params)) {
syncURL();
}
},
/**
* Update the URL to match the given params
*/
set(params, force) {
params = normalize(params);
if (force || paramsAreDifferent(params)) {
router.params = params;
syncURL();
}
},
/**
* Replace the current URL without adding
* a browser history entry
*/
replace(params) {
params = normalize(params);
if (paramsAreDifferent(params)) {
router.params = params;
replaceURL();
}
},
/**
* Add the given params to the URL without
* adding a browser history entry
*/
mergeReplace(params) {
if (mergeParams(params)) {
replaceURL();
}
}
};
const router = {
params: {},
url,
/**
* Reload the app.
*/
load() {
if (router.app && !loadPending) {
loadPending = true;
requestAnimationFrame(() => {
loadPending = false;
router.app(router.params);
});
}
},
/**
* Register the app with the router, and run it.
*/
init(app) {
/**
* Support URLs that use query strings instead of hash fragments.
*/
if (location.search) {
location.href = location.href.replace('#', '&').replace('?', '#')
} else {
router.app = app;
window.addEventListener('hashchange', hashchange);
syncParams();
router.load();
}
},
/**
* Remove the provided keys from the URL,
* and reload the app.
*/
remove(...params) {
if (canNavigate(() => router.remove(...params))) {
removeParams(params);
syncURL();
router.load();
}
},
/**
* Remove the provided keys from the URL
* without adding a browser history entry
* and reload the app.
*/
removeReplace(...params) {
if (canNavigate(() => router.removeReplace(...params))) {
removeParams(params);
replaceURL();
router.load();
}
},
/**
* Add the provided keys to the URL,
* and reload the app.
*/
merge(params) {
if (canNavigate(() => router.merge(params))) {
mergeParams(params);
syncURL();
router.load();
}
},
/**
* Set the URL to an exact param list,
* and reload the app
*/
set(params) {
if (canNavigate(() => router.set(params))) {
router.params = normalize(params);
syncURL();
router.load();
}
},
/**
* Replace the current URL without adding a
* browser history entry, and reload the app.
*/
replace(params) {
if (canNavigate(() => router.replace(params))) {
router.params = normalize(params);
replaceURL();
router.load();
}
},
/**
* Replace params in the current URL without adding
* a browser history entry, and reload the app.
*/
mergeReplace(params) {
if (canNavigate(() => router.mergeReplace(params))) {
mergeParams(params);
replaceURL();
router.load();
}
}
};
window.uav = window.uav || {};
window.uav.router = router;
if (typeof module !== 'undefined' && module.exports) {
module.exports = router;
}
})();