Working with Turbolinks
May 1st 2020
This is an excerpt from the Working with Turbolinks chapter in my book Playbook Thirty-nine -- A guide to shipping interactive web apps with minimal tooling.
In this chapter I talk about the challenges of working with Turbolinks in Rails applications, and some techniques on how to mitigate those issues. This section has been shortened from the actual chapter.
____________________
Third-party Javascript plugins make assumptions about how the page cycle unfolds as the page loads. But Turbolinks takes over completely and has its own way of dealing with the page. Sometimes this can conflict, and when that happens, it can really make you pull your hair out when it comes down to troubleshooting.
When these issues happen, most developers just remove Turbolinks. If you’re one of those developers, I don’t blame you at all. But it doesn’t have to be that way. The Turbolinks API is mature and offers us quite a few options to ameliorate these issues.
So what does one of these issues look like? If you’ve ever seen the issue of “doubling up” JS driven elements, it’s because of Turbolinks.
The problem is easily replicable by visiting a page, pressing the back button, then returning to the aforementioned page. But there’s hope!
To start, the most important principle to remember is that instead of instantiating a JS plugin on document load, we have to run under turbolinks:load. For example:
function initRangePicker(){
$('#range-picker').flatpickr()
}
$(document).on('turbolinks:load',function(){
initRangePicker()
});
Actions like click events should be delegated to the window or document instead of being run under turbolinks:load.
Another helpful tip is that Javascript plugins should have their instantiation call inside a method. The reason is that we’ll often need to re-instantiate plugins after content has been loaded to the page dynamically.
This is common when you’re doing things like loading a date picker in a Bootstrap modal, or adding a view partial to a page with jQuery from a .js.erb file. In this case we can call functions from our application.js file and they will now work within .js.erb files.
Here is an example from Mavenseed: I built a drag-and-drop page builder that only uses Rails and a bit of jQuery. The page builder has panels that slide in, and in these panels we load the widget editing form on the page (we call them “leaves"). This provides a better experience, but there are challenges with loading content after the Document Object Model (DOM) has already loaded.
Elements dynamically driven by Javascript—like a select dropdown or color picker—wouldn’t get initialized after being added to the page. We can mitigate this problem by calling our Javascript method which re-instantiates the plugin.
$('.js--widget-settings').html("<%= j render partial:'builder/templates_widgets/form', locals:{templates_widget: @templates_widget} %>")
initSelect()
initColorPicker()
To fix the doubling up of JS elements, or to fix issues with disappearing JS-driven elements, we have to break them down before the page reloads. We can do this by hooking into turbolinks:before-cache and destroying the plugin before the page reloads.
This example is from Flatpickr, one of my favorite date-picker Javascript libraries:
function initRangePicker(){
$('#range-picker').flatpickr()
}
$(document).on('turbolinks:load',function(){
initRangePicker()
}).on('turbolinks:before-cache', function(){
// Tear Down Flat Picker
$('#range-picker').flatpickr('destroy')
})
But what about plugins without a method to destroy themselves?
____________________
Read more about working with Turbolinks in my new book Playbook Thirty-nine, out now!