loading...

Anchor links in Mithril.js

weirdmayo profile image Daniel Mayovsky ・1 min read

Recently, during the process of making a small website for my friend, I decided to use Mithril.js MVC framework, since it is my favorite and I got to know it by heart. Give me time, and I can do basically everything with that framework at this point. But, recently, I encountered the issue with it's routing technique implementation. I can't link to anchors and make my page teleport/scroll to them, but I fixed it.

How the problem appeared/works.

When you are using links to route to different pages of your app, you hard-wire a component to each route:

m.route( rootElement, "/home", {
    "/home": HomeComponent,
    "/settings": SettingsComponent
}

You can specify your own routing strategy, but by default the end result with make your url look like this: https://website.com/#!/home. The hashbang in the beginning isolates to routes exclusively to a single Mithril applicatoin, in case your website will have more than one Mithril application/use-case of some sort.

Mithril knows that you don't want to bother to write out every a link in your application to /#!/whatever link, so they allow you to do the <a href='/home'> and Mithril will take care of safely routing a user to the route you specify if you pass the oncreate method with a m.route.link function to be called.

The code for the a element will look something like this:

m("a[href='/home']", {
    oncreate: m.route.link
}, "Home");

The problem itself

Now, with that being said, if you have not hardwired a component to a certain route, it will just omit the route call and keep you on the same page you were or teleport you to the default route (the one specified as a second argument in the m.route call).

How do you set up anchors then?
With route parameters.

That looks something like this: https://website.com/home?filter=all

To pass a route parameter, you have to call the m.route.set() function, not do it through the a[href] link. An example of such function would be:

//button component
const Button = {
    view: function(vnode){
        return m("button", {
            onclick: (e)=>{ m.route.set("/home", {
                //your parameters here
                "filter": "all",
        }, "Go Home")
    }
}
//app component
const App = {
    view: function(vnode){
        return m("div.app", [
            m(Button)
        ]);
    }
}

So, as you might have guessed already, you can pass the anchor name into a route parameter.

/* ...button component script... */ {
    onclick: (e)=>{
        m.route.set("/home", {
            "a": "contact-me", // a for Anchor
        }
    }   
}

But just passing the anchor name won't do anything. You have to actually scroll to the anchor. For that we make a function called scrollToAnchor( anchorName ), which we can call during the onclick event;

function scrollToAnchor( anchorName ){
    let is = (el)=>{return el !== undefined && el !== null};
    //if you pass an undefined anchor it will scroll to the top of the body
    let targetEl = is(anchor) ?
        document.querySelector("a[name="+ anchor +"]")
    : document.body;
    let scrollTop = window.pageYOffset || document.documentElement.scrollTop;
    let target = is(targetEl) ? targetEl.getBoundingClientRect().top : 0;
    window.scroll({
        top: target + scrollTop - 70,
        left: 0,
        behavior: "smooth"
    });
}

and now just add a function to an onclick call on the button we made

onclick: (e)=>{
    let anchor = "contact-me";
    m.route.set( "/home", { "a": anchor } );
    scrollToAnchor( anchor );
}

That's it. We solved it.

If you want to keep the same route, but just teleport to anchor, can pass the m.route.get() as a first argument of the m.route.set.

If you want to be fancy, you can make a Button component better with the functionality automated integrated:

const Button = {
    oninit: function(vnode){
        this.ctrl = {
            route: function(route, anchorName){
                if(anchorName){
                    m.route.set(route, {"a": anchorName});
                    scrollToAnchor( anchorName );
                } else {
                    m.route.set( route );
                }
            }
        }
    },
    view: function(vnode){
        return m("button", {
            onclick: (e)=>{
                that.ctrl.route( vnode.attrs.route, vnode.attrs.anchor )
            }, vnode.attrs.text );
    }
}

const App = {
    view: function(vnode){
        return m("div.app", [
            m(Button, {
                route: "home",
                anchor: "contact-me",
                text: "Contact Me"
            })
        ]);
    }
}

Conclusion

It might seem like too much work to do to just make an anchor link work on this framework. But, frankly speaking, most of the frameworks have a similar routing strategy. Since anchor links are mostly used in blogs and tutorial posts, these are static websites that don't need MVC frameworks to be built, so MVC framework routing strategies barely ever include an anchor solution. This kind of "fix" can be implemented anywhere, not just Mithril.js

Posted on by:

weirdmayo profile

Daniel Mayovsky

@weirdmayo

A dude. Preaching Mithril.js to be the best MVC framework.

Discussion

markdown guide