Reinventing the Turbolinks Wheel

I was reading about Turbolinks and Stimulus recently, and it got me thinking. Turbolinks is a pretty sweet addition to Ruby on Rails, but a lot of my work is still in ColdFusion and in an application framework I’ve been doing a lot of work on.

So how hard could it be to create Turbolinks for a ColdFusion framework? It turns out it really isn’t too difficult as long a few compromises can be made. If it’s just a body element being replaced and jQuery can be leveraged, the challenges are few. JavaScript works equally well, jQuery just makes it a bit more terse.

I’m picky, though. I don’t want things to be complicated, especially in integration. Preferably, jQuery would do all the heavy lifting, and I’d just add something to each anchor tag to make it happen. That way, I could stage the changes over time if there were any caveats.

Oh, this probably doesn’t work on older browsers, but modern Opera, Firefox, IE, Edge and Chrome are all fine.

Step 1: jQuery interception

$('a[data-ajax]').off("click");
$('a[data-ajax]').on("click", function(e){
  history.pushState('navigate', '', e.currentTarget.getAttribute('href').replace(/turbo&?/,""))
  e.preventDefault();
  e.stopPropagation();
  turboLink(e.currentTarget.getAttribute('href'));
});

Turning off the listeners is necessary because when a page is loaded via certain means, the listeners get registered multiple times. This not only breaks the browser back button, but means that each link gets registered exponentially. That was a fun bug to find 2, 4, 8, 16 and 32 times each click.

After that, it’s a matter of registering a new listener to intercept the clicking of any link that has a “data-ajax” attribute. Then a few things need to happen. The first is that before we navigate anywhere, we add an entry in the history since an ajax loading won’t do that for us.

Then we prevent the default behavior, and we stop the event from bubbling up. Stopping propagation isn’t strictly necessary in many cases, but it may be necessary in some so it doesn’t hurt to have it.

Then we call a custom function! I’m not pretending to do anything new, so I call my function “turbolink.” It’s an homage?

Step 2: Getting the HTML

function turboLink(link, scrollUp) {
  if(/\?/i.test(link)){
    link = link.replace(/\?/, "?turbo&");
  } else {
    link = link+"?turbo";
  }
  $.ajax(link)
  .done(function(data) {
    if(scrollUp){
      window.scrollTo(0,0);
    }
    $('.main-content').replaceWith(data);
    linkListeners();
    initScripts();
  });
}

The first thing the function does is add a url parameter. In my Application.cfc, I include 3 files around the page being requested. The “turbo” flag skips including the header, navigation and footer. The regex just makes sure I don’t replace any query strings that are included in the link.

In my code, all the non-(header|nav|footer) content lives in a div with the “main-content” class. The replaceWith() function in jQuery is perfect for that.

I also have some functions that make database changes that get called in the background, and the page can’t be reloaded until the background is complete. For some reason, calling turbolink() in the callback of another ajax function doesn’t register the listeners like calling turbolink() directly. Some day I’ll need to figure that one out, but calling the listener function doesn’t hurt anything for now.

Similarly, if there are any scripts called on document.ready, they won’t get called unless it’s done manually. So they need to be inside a function. In this case, that’s the initScripts() function.

Step 3: Handling History

So now, any link with a “data-ajax” attribute gets called by an ajax function, and all the HTML gets inserted into the main-content div. The browser history even gets an entry when they’re clicked. Unfortunately, hitting the back button won’t actually do anything. So a bit of JavaScript is needed to handle that.

$(window).on("popstate", function(){
  if(/\?/i.test(location.href)){
    turboLink(location.href.replace(/\?/, "?turbo&"));
  } else {
    turboLink(location.href+"?turbo");
  }
})

Because the links have the “turbo” query parameter scrubbed out, it has to be added back in so that header, nav and footer don’t get added a second time.

Beyond that, all that’s needed is to call the linkListeners() and initScripts() functions so that on a non-ajax load, everything happens in the same way.

Step 4: The Background

To handle certain links that make some database changes, but shouldn’t make the user leave the page, like adding a comment, or switching the status of an item or something similar, listeners for those links are registered to anchors with the data-background.

$('a[data-background]').off("click");
$('a[data-background]').on("click", function(e){
  e.preventDefault();
  e.stopPropagation();
  currPage = window.location;
  $.ajax(e.currentTarget.getAttribute('href'))
  .done(turboLink(window.location.href, false));
});

There does some to be some latency issues with the database changes not being fully saved to the database after the page has reloaded. I’m not entirely sure that’s a problem at the JavaScript level. That might take some testing to confirm.

But that’s pretty much it. Updating the page title isn’t too tough if that’s needed. Or whatever element in the head needs updating. That’s more tedious than challenging.

Is it Turbolinks? Nah. But it is simple. And it can fit in pretty much anywhere, and it’s optional at the code level, but doesn’t take much to add.

There are no comments