Turbolinks and Django

Last revision Oct. 19, 2020
Spanish version click here

What is Turbolinks?

Turbolinks is a gem included in Ruby on Rails that avoids having to reload the CSS and JavaScript files each time you click on a link in your application, making this feel faster and more agile in the browser.

The way it works is as follows: instead of reloading the page when a link is clicked, Turbolinks intercepts the click event, calls the server via AJAX and replaces the body tag in the page with the response.

With Turbolinks your applications will feel almost as if they were made with a modern JavaScript framework such as Angular or React, but without the complexity that this implies. In addition, this maintaining a cache of the last visited pages, allowing those pages to be displayed immediately without making a call to the server.

How do we use it in our Django app?

In comparison with the RoR gem, in Django (so far) we have not found a package that works or integrates into the assets pipeline as the version of Ruby on Rails does. Although, since version 5 of Turbolinks this was completely rewritten using TypeScript to simplify the API, review the philosophical decisions of the past and cut some technical luggage; it can now be used without the need for your application to be written in RoR, making use of the standalone version, which you can install using your preferred JavaScript package manager, as explained in its documentation.

That said, the standalone version is what we're going to use in our Django application, so first let's do, is add Turbolinks to our main template.

{% load static %}
<head>
    <title>Django + Turbolinks</title>
    ...
    <script src="{% static 'js/turbolinks.js' %}></script>
    ...
</head>

You can download it using CDNJS and include it in the assets of your application; if you notice, we have loaded it in the header and not at the end of <body>, this is because if we leave it in the end, it will be loaded in each change of the application.

If you use the template blocks in Django to render certain scripts in certain pages, it is better to avoid or stop using that method and move everything to the header, to ensure the optimal functioning of Turbolinks

*base.html*
<html>
    <head>
        <title>Turbolinks + Django</title>
    </head>
    <body>
        <main>
            {% block main %}
            {% endblock main %}
        </main>
        {% block javascripts %}
        {% endblock javascripts %}
    </body>
</html>
*main.html*
{% extendeds 'base.html' %}
{% load static %}

{% block main %}
    <h1>Hi!</h1>
{% endblock main %}

{% block javascripts %}
<script src="{% static 'js/cool_script.js' %}"></script>
{% endblock javascripts %}

Start up

Now we will see how to combine everything to launch our application with Turbolinks.

Fingerprint

Turbolinks track the URLs of the static files that you have in the header of your document, that's why it is advisable to add a unique identifier to your urls, which is usually generated with each new deploy.

<head>
    <link rel=“stylesheet” href=“/application-258e88d.css” />
  <script src=“/application-cbd3cd4.js”></script>
</head>

You can use Django Pipelines for this process and, in turn, get a better organization of the assets in your application.

Idempotency

If you have read the documentation, you will have noticed the events that Turbolinks adds to your application, one of these is the turbolinks: load, well if you moved all the initializers within this, you will notice that the events are duplicated, you can use this plugin rstacruz/onmount to avoid this kind of errors.

$.onmount(
  ‘.push-button’,
  function() {
    $(this).on('click', doSomething)
  },
  function() {
    $(this).off('click', doSomething)
  }
);

In this way, every time the element exists in the rendered DOM, we add the click event to it, and when it disappears, we deactivate it, a good tip is to invoke onmount every time Turbolinks loads a page.

$(document).on('ready turbolinks:load', function() {
  $.onmount();
});

Async - Defer

Since all the assets must be in the header of the application, you do not want to add more time to the user and damage the experience of this, that is why you must consider what is important to load first and what could wait, for this use the async or defer attributes as appropriate.

Defer-Async

No-Cache

If you have a page that requires you to always have the most recent version of its content, use the following tag to avoid it being added to the Turbolinks cache.

<meta name="turbolinks-cache-control" content="no-cache">

Final

This is how the final app will looks like.

settings.py

PIPELINE = {
  'JAVASCRIPT': {
    'plugins': {
       'source_filenames': (
         'js/plugins/plugin-a.js',
         'js/plugins/plugin-b.js',
       ),
      'extra_context': {'defer': True},
      'output_filename': 'js/plugins.js',
    },
    'editor': {
      'source_filenames': (
        'js/editor/editor-deps.js',
        'js/editor/editor.js',
      ),
      'extra_context': {'async': True},
      'output_filename': 'js/editor.js'
    },
    'app': {
      'source_filenames': (
        'js/application.js',
      ),
      'output_filename': 'js/app.js'
    },
    ...

As you can notice plugins is loaded using defer because I do not need them to be available before the whole document finishes being rendered, in comparison with editor which uses async since I need it to be available before the document ends and it will be initialized within app.

tag head

<head>
  <meta charset="utf-8" />
  <title>{% block title %}{% endblock %}</title>
  ...
  {% stylesheet "theme" %}
  {% javascript "editor" %}
  {% javascript "turbolinks" %}
  {% javascript "app" %}
  {% javascript "plugins" %}
</head>

All the static files of the application are loaded in the header and instead of using the {% static '<path>'%} template tag, we use the builtins Django-Pipelines tags, which add the options to have the fingerprint and ship the minified version in the deploy.

Middleware

# Taken from: https://github.com/dgladkov/django-turbolinks/blob/master/turbolinks/middleware.py#L11  # NOQA
def same_origin(current_uri, redirect_uri):
    a = urlparse(current_uri)
    if not a.scheme:
        return True
    b = urlparse(redirect_uri)
    return (a.scheme, a.hostname, a.port) == (b.scheme, b.hostname, b.port)


class TurbolinksMiddleware:
    ''' Send the `Turbolinks-Location` header in response to a visit
    that was redirected, and Turbolinks will replace the browser’s topmost
    history entry.
    Taken from: https://github.com/viewflow/django-material/blob/v2/material/middleware.py#L38  # NOQA
    Note: This is needed to handle redirects with TurboLinks.
    '''

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        turbolinks_referrer = request.META.get('HTTP_TURBOLINKS_REFERRER')
        is_response_redirect = response.has_header('Location')
        if turbolinks_referrer:
            if is_response_redirect:
                location = response['Location']
                prev_location = request.session.pop(
                    '_turbolinks_redirect_to', None
                )
                if prev_location is not None:
                    # relative subsequent redirect
                    if location.startswith('.'):
                        location = prev_location.split('?')[0] + location
                request.session['_turbolinks_redirect_to'] = location
                # cross domain blocker
                if not same_origin(location, turbolinks_referrer):
                    return HttpResponseForbidden()
            else:
                if request.session.get('_turbolinks_redirect_to'):
                    location = request.session.pop('_turbolinks_redirect_to')
                    response['Turbolinks-Location'] = location
        return response

App.js

$(document).on('turbolinks:load', function() {
  ...
  $.App.init();
  ...
  $.onmount();
});
...
/* Enable plugin a */
$.onmount('.my-plugin', function () {
  $(this).plugin_a();
});

And we finish initializing onmount each time Turbolinks loads a page, so that the plugins and/or events are activated according to the elements that have been rendered.

Conclusions

I hope this post has been useful and save you a few hours of searching the internet, this way has worked for us, to give to our users a better experience in our application, without having to separate the Back-End from the Front-End, or re-write it using modern libraries.

Notes

  • Always check the way in which Turbolinks renders a view that has already been saved in the cache, since it could be that some plugins (tooltips for example) have been initialized.
  • If you have the opportunity and the time, it is better to divide your javascript into chunks and write it in a more modular or modern way.
  • Being B your final point or view, always test it according to these four ways:

    • From A to B.
    • From A to B, then from B to A, and again from A to B.
    • Only B.
    • Only B, then reload B.
    • Leave the performance tests to the end.
    • You can experiment with Stimulus.