Skip to content

Instantly share code, notes, and snippets.

@Raynos
Created August 31, 2014 05:43
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Raynos/adbf7951bee3fdfe1a65 to your computer and use it in GitHub Desktop.
Save Raynos/adbf7951bee3fdfe1a65 to your computer and use it in GitHub Desktop.
Mercury router
module.exports = clickEvent;
function clickEvent(handler, opts) {
opts = opts || {};
return function clickHandler(ev) {
if (!opts.ctrl && ev.ctrlKey) {
return;
}
if (!opts.meta && ev.metaKey) {
return;
}
if (!opts.rightClick && ev.which === 2) {
return;
}
handler();
ev.preventDefault();
};
}
var h = require('mercury').h;
var clickEvent = require('./click-event.js');
var routeAtom = require('./router.js').atom;
module.exports = anchor;
function anchor(props, text) {
var href = props.href;
props.href = '#';
props['ev-click'] = clickEvent(pushState, {
ctrl: false,
meta: false,
rightClick: false
});
return h('a', props, text);
function pushState() {
routeAtom.set(href);
}
}
var routeMap = require('route-map');
module.exports = routeView;
function routeView(defn, args) {
if (args.base) {
defn = Object.keys(defn)
.reduce(function applyBase(acc, str) {
acc[args.base + str] = defn[str];
return acc;
}, {});
}
var match = routeMap(defn);
var res = match(args.route);
if (!res) {
throw new Error('router: no match found');
}
res.params.url = res.url;
return res.fn(res.params);
}
var mercury = require('mercury');
var source = require('geval/source');
var window = require('global/window');
var document = require('global/document');
var atom = Router.atom =
mercury.value(String(document.location.pathname));
/*
var mercury = require('mercury');
var h = require('mercury').h;
var anchor = require('mercury-route/anchor');
var routeView = require('mercury-route/route-view');
var Router = require('mercury-route/router');
function State() {
var state = mercury.struct({
route: Router()
});
return { state: state }
}
mercury.app(document.body, State().state, render);
function render(state) {
return h('div', [
menu(),
routeView({
'/': renderHome,
'/animals': renderAnimals,
'/animals/:id': renderAnimalItem
}, { route: state.route })
])
}
function menu() {
return h('ul', [
h('li', [
anchor({
href: '/'
}, 'Home')
]),
h('li', [
anchor({
href: '/animals'
}, 'Animals')
])
])
}
*/
module.exports = Router;
function Router() {
var inPopState = false;
var popstates = popstate();
popstates(onPopState);
atom(onRouteSet);
return { state: atom };
function onPopState(uri) {
inPopState = true;
atom.set(uri);
}
function onRouteSet(uri) {
if (inPopState) {
inPopState = false;
return;
}
pushHistoryState(uri);
}
}
function pushHistoryState(uri) {
window.history.pushState(undefined, document.title, uri);
}
function popstate() {
return source(function broadcaster(broadcast) {
window.addEventListener('popstate', onPopState);
function onPopState() {
broadcast(String(document.location.pathname));
}
});
}
@kristianmandrup
Copy link

Trying to understand...

routeAtom.set(href); uses mercury.value - ah, to be found in https://github.com/Raynos/observ, so it is a generic immutable observable I guess. I expect that atom(onRouteSet); in Router means it will call onRouteSet(uri) with the href which will in turn call pushHistoryState(uri); ?

Then there is this part which also needs a bit of explanation: what is broadcast ?
window.popstate - https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history, simply triggered when going back in history, which sets document.location.pathname to previous state, which we then broadcast somehow...

broadcast is available from within the context of source from geval at https://github.com/Raynos/geval

function Source(broadcaster) {
    var tuple = Event()
    broadcaster(tuple.broadcast)
    return tuple.listen
}

Which uses the broadcast in Event via tuple

function Event() {
    var listeners = []
    return { broadcast: broadcast, listen: event }
    function broadcast(value) {
      ...
function popstate() {
    return source(function broadcaster(broadcast) {
        window.addEventListener('popstate', onPopState);

        function onPopState() {
            broadcast(String(document.location.pathname));
        }
    });
}

But who is listening? Looks like this logic sets up the onPopState to listen for uri's popped from window.history. Clever!

    var popstates = popstate();

    popstates(onPopState);

    function onPopState(uri) {
        inPopState = true;
        atom.set(uri);
    }

And the actual routing is performed via routeView

function routeView(defn, args) {
    if (args.base) {
        defn = Object.keys(defn)
            .reduce(function applyBase(acc, str) {
                acc[args.base + str] = defn[str];
                return acc;
            }, {});
    }

    // create match function using defn
    var match = routeMap(defn);

    // match route
    var res = match(args.route);
    if (!res) {
        // no route found, we should/could then route/redirect to error route ... 
        throw new Error('router: no match found');
    }

@Raynos
Copy link
Author

Raynos commented Nov 16, 2014

You got it spot on :)

  • the anchor module writes to the state atom to trigger a new uri.
  • the route view module is a pure rendering helper function that takes an uri and calls the correct view based on an url pattern.
  • the router itself listens to popstate and updates the state atom with a new uri, when the state atom is mutated it will write to the HTML5 history api to ensure that back & forward works. And it returns the url state atom, this can be plugged into a larger app and you can use the route view helper on it.

The bit that is missing from this is a component interface.

var RouterComponent = function () {
  return Router().state;
};
RouterComponent.render = function (state, opts) {
  return routeView(opts, { route: state });
};
RouterComponent.anchor = anchor;

@kristianmandrup
Copy link

Super!!! I'm trying to integrate much of this in a new framework dragonslayer and I plan to document improve docs for the mercury API in the process..

I like playing with this stuff!! Happy to see I've got it spot in :) Very interesting approach. I plan to integrate this approach with the Crossroads router which is more feature complete.
You are amazing!!

@kristianmandrup
Copy link

Can you show an example for how to use the RouterComponent as well :)
I would think it could be used as a sort of "base class" for any component that wishes to use the Router?

@twilson63
Copy link

@Raynos - I took this gist and converted it into a repo mercury-router https://github.com/twilson63/mercury-router - gave you the credit - just wanted a npm module. if you want it under your account, I will be happy to transfer. Thanks again for the mercury framework and this gist!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment