Monday, September 16, 2013

First Experience with Rails 4 Turbolinks

My most recently developed Rails application was built with Rails 4, which ships out of the box with a feature called Turbolinks. Here is its description from the Turbolinks github repo:
Turbolinks makes following links in your web application faster. Instead of letting the browser recompile the JavaScript and CSS between each page change, it keeps the current page instance alive and replaces only the body and the title in the head.
In effect, hyperlinks become turbolinks as page loading is perceived to be faster without the reloading of JavaScript and CSS despite having the same network load time for the webpage HTML markup.

In this post, I am sharing some of the gotchas I encountered with this feature, how I dealt with them, and my overall experience with the feature.

Gotchas and Solutions:

1. Style changes were not updating on page navigation

I encountered an issue the first time I tried to leverage the controller-specific CSS stylesheet pattern. For those unfamiliar with the pattern, it is actually about loading an extra CSS file in addition to application.css in order to customize the look of pages belonging to a specific controller. To enable the pattern, one would add a line like the following to their layouts/application.html.erb:

<%= stylesheet_link_tag params[:controller], media: "all" %>

This in effect, loads users.css for the page rendered by users#index or users#show for example.

The benefit of the pattern is simply keeping that CSS maintained separately from other pages so that when the Rails Asset Pipeline builds application.css it does not concatenate many potentially conflicting styles and forcing developers to deal with them with all sorts of complicated hacks. Having a controller-specific CSS loaded per controller page in addition to application.css results in much lighter more maintainable page specific styles.

So, what was the gotcha?

Well, upon updating the controller specific CSS, I noticed that its styles were not reflected when I was navigating between the controller pages until I refreshed the browser page completely. Turns out, that is because I did not have turbolinks tracking enabled, so I had to add this option to resolve the problem: , "data-turbolinks-track" => true, changing the erb to:

<%= stylesheet_link_tag params[:controller], media: "all", "data-turbolinks-track" => true %>

2. On-document-ready JavaScript events were not firing on page navigation

For example, if you have JavaScript code that converts form hints into tooltips like the following code (in CoffeeScript):

$ =>
  $('.hint').tooltip(placement: 'right')

The code above relies on jQuery's shortcut method $ for executing some JavaScript (CoffeeScript) code upon page document load (document ready).

When visiting the form for the first time, the tooltips show up. However, when navigating away to the home page, and navigating back to the form page, the tooltips do not show up anymore.

Turns out that it is expected behavior by Turbolinks' definition. After all, the Turbolinks feature does not reload JavaScript files on page navigation, missing out on the activation of hint tooltips on document load.

Fortunately, the Turbolinks feature offers developers a way to get around this through its own event hooks fired on page navigation. They are detailed in the Turbolinks github repo (highly recommended to visit to learn Turbolinks in depth).

The event hook I used in particular was: "page:load" (2020 update: "turbolinks:load"). Using it alone did not resolve the problem however as it worked for page navigation, but not initial page load (2020 update: the docs claim the new "turbolinks:load" now works for both initial page load and later page visits), so I used it in addition to the original jQuery $ method as follows:

$(document).on "page:load", =>
  $('.hint').tooltip(placement: 'right')

So in essence, the gotcha fix was to wrap all the page document load logic in a JavaScript function, and then hook it into both on-document-ready (jQuery $) (2020 update: the docs claim this is no longer needed with "turbolinks:load") and Turbolinks "page:load" (2020 update: now "turbolinks:load")


Now regarding my overall experience with Turbolinks, I thought it was a very welcome addition to Rails. It worked quite perfectly once I have gone beyond the two gotchas, resulting in an impressively instant page navigation and making local browser testing much faster and more productive. The JS libraries I have used with Turbolinks by the way are the Spectrum Color Picker, Twitter Bootstrap Tooltips, and Fancybox in addition to usage of JS media queries.

In a way, Turbolinks is the perfect embodiment of the Rails philosophy of discovering common patterns, automating their solutions to be as effortless and friction free as possible for everyone to use, and finally offering the ability to customize or disable all-together. By the way, you can opt out of Turbolinks by adding the attribute "data-no-turbolink" to the HTML body element or divs for which you want links to behave regularly. In essence, Turbolinks' implementation satisfies the 80/20 rule, automating the solution for 80% of the cases (not literally) to increase productivity and allowing developers to customize the solution in the remaining special 20% of the cases (not literally) to allow flexibility. Using Turbolinks is also an instance of following Convention over Configuration.

In conclusion, Turbolinks is like driving your first Porsche. The first couple of times you drive the car, you are out of control and do not know why the hell the car is not behaving like a good old Honda, but then you finally get the hang of it, and suddenly find yourself flying in a 911 Turbo. ;)

1 comment:

Christian Gudrian said...

Including a controller specific stylesheet in the head with data-turbolinks-track=true will trigger a full reload of the page everytime the controller changes. In fact the page is loaded twice: once to detect the change and once for the actual reload.

I ended up loading my specific JavaScripts in the views themselves.