Revolutionize Django Admin: Give it a SPA like look-and-feel with Hotwire/TURBO

Django Viewflow
5 min readMay 16, 2023

Django Admin has long been a reliable tool for managing databases and performing administrative tasks. However, compared to modern SPA applications, its traditional style can make the user interaction feel less dynamic. This article will demonstrate how you can enhance Django Admin by integrating Hotwire/Turbo, utilizing a modern no-JavaScript, HTML-over-the-wire approach.

Beyond Django Admin, you can use the same approach, to breathe new life into your any legacy Django application. Offering users a more dynamic and interactive experience while leveraging the simplicity and familiarity of server-side rendering.

Turbo, a part of the Hotwire suite developed by Basecamp, is a set of tools designed to simplify and enhance web application development. It accelerates navigation by eliminating full-page reloads (Turbo Drive), allows page decomposition into independent contexts for scoped navigation and lazy loading (Turbo Frames), delivers page changes over WebSocket or SSE using HTML and CRUD actions (Turbo Streams). This is accomplished by primarily sending HTML over the network, reducing the reliance on custom JavaScript, and makes it easier for integration with traditional frameworks like Django, that renders HTML on the server side

In this article, we will cover the simple integration of Turbo Drive, to enhance the user experience in your Django Admin or legacy Django application. We will later explore advanced techniques, such as Turbo Frames, for partial interactivity and improved page updates.

Check out the article’s source code at Viewflow Cookbook Repository

1. Integrating Hotwire/Turbo

To quickly integrate Hotwire/Turbo into your Django Admin or legacy Django application, follow these steps:

  • Create a admin/base_site.html file in your custom templates folder.
  • Add the following code to base_site.html:
{% extends "admin/base_site.html" %}
{% block extrahead %}
{{ block.super }}
<script type="module" src="https://cdn.skypack.dev/@hotwired/turbo">
</script>
{% endblock %}
  • Make sure your Django project’s TEMPLATES setting includes the folder where base_site.html resides.
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / 'templates'],
...
},
]

For simplicity, we include Hotwire/Turbo from the CDN. However, it’s crucial to consider the security implications when including external scripts. To take a more controlled and secure approach, it is recommended to install Turbo using npm. Simply run npm install @hotwired/turbo in your project's directory, and then copy the installed script to your static files directory.

This simple setup enables Turbo’s functionality. GET and POST requests are handled through Turbo, while link clicks and form submissions utilize fetch() requests, replacing the page body without re-rendering styles and scripts. This significantly improves page loading speed and enhances the user experience.

2. Fixing legacy JavaScript to work seamlessly with Hotwire/Turbo

As you quickly would realize, the admin interface JavaScript custom interactions are no longer initializes on a page change. This is because the legacy code assumes initialization should happen on window load or document DOMContentLoaded events. However, with Hotwire/Turbo, our page loads only once, and subsequent page changes emit a turbo:render event instead.

To address this issue, let’s apply a simple hack. We’ll replace all standard addEventListener hooks with our custom ones that also subscribe to the turbo:load event. Here’s the code:

const originalAddEventListener = window.addEventListener;

window.addEventListener = function(event, listener, options) {
if (event === 'load') {
originalAddEventListener.call(this, 'turbo:render', listener, options);
}

originalAddEventListener.call(this, event, listener, options);
};


const originalDocAddEventListener = document.addEventListener;

document.addEventListener = function(event, listener, options) {
if (event === 'DOMContentLoaded') {
originalDocAddEventListener.call(this, 'turbo:render', listener, options);
}

originalDocAddEventListener.call(this, event, listener, options);
};

Save this code in a file called fixElementListeners.js inside one of your static directories, such as admin/js/. To include it early in the Django admin base page, add the following lines:

{% extends "admin/base_site.html" %}
{% load static %}

{% block dark-mode-vars %}
<script src="{% static "admin/js/fixEventListeners.js" %}"></script>
{{ block.super }}
{% endblock %}

{% block extrahead %}
....

Make sure your Django project’s setting includes the folder where the js file located.

STATICFILES_DIRS = [ BASE_DIR / "static" ]

3. Fix navigation bar initialization

You might notice that everything works smoothly except for the navigation bar. Unlike other parts of Django Admin, the navigation bar is initialized directly inside a <script> tag without waiting for a load event. To resolve this, we need to reinitialize it by copying the entire code inside fixElementListeners.js.

window.addEventListener('turbo:render', function() {
const toggleNavSidebar = document.getElementById('toggle-nav-sidebar');
if (toggleNavSidebar !== null) {
const navSidebar = document.getElementById('nav-sidebar');
const main = document.getElementById('main');
let navSidebarIsOpen = localStorage.getItem('django.admin.navSidebarIsOpen');
if (navSidebarIsOpen === null) {
navSidebarIsOpen = 'true';
}
main.classList.toggle('shifted', navSidebarIsOpen === 'true');
navSidebar.setAttribute('aria-expanded', navSidebarIsOpen);

toggleNavSidebar.addEventListener('click', function() {
if (navSidebarIsOpen === 'true') {
navSidebarIsOpen = 'false';
} else {
navSidebarIsOpen = 'true';
}
localStorage.setItem('django.admin.navSidebarIsOpen', navSidebarIsOpen);
main.classList.toggle('shifted');
navSidebar.setAttribute('aria-expanded', navSidebarIsOpen);
});
}
window.initSidebarQuickFilter();
})

4. Fix jQuery initialization scripts

As with any legacy application, Django Admin relies on jQuery. To ensure proper initialization of jQuery scripts, we need to attach them to the ‘turbo:render’ event. Let’s address this. Start by creating a file called admin/js/jquery.init.js. This file will override the default jQuery initialization in Django Admin, allowing us to make the necessary adjustments.

Inside jquery.init.js, include the following code:

window.django = {jQuery: jQuery.noConflict(true)};

const originalReady = django.jQuery.fn.ready;

django.jQuery.fn.ready = function(fn) {
if (typeof fn === 'function') {
originalReady.call(this, function() {
fn.apply(this, arguments);
});
window.addEventListener('turbo:render', fn);
} else {
originalReady.apply(this, arguments);
}
};

5. Integrate Django form processing with Turbo

To align Django’s form processing with Turbo’s expectations for HTTP return codes, we need to ensure that Turbo Drive receives the correct responses. After a form submission, Turbo Drive expects a redirect response with an HTTP 303 status code to navigate and update the page without reloading. In cases where there are form validation errors, Turbo Drive looks for an HTTP 422 status code.

To make this work in Django, we can create a simple middleware called TurboMiddleware. This middleware checks if the request path starts with '/admin/' and if the method is 'POST'. If these conditions are met, it modifies the response status code accordingly. Successful form submissions will have a 303 status code, while form validation errors will have a 422 status code.

  • In the same directory as settings.pyadd the following code to middleware.pyfile:
def TurboMiddleware(get_response):
def middleware(request):
response = get_response(request)
if request.path.startswith('/admin/') and request.method == 'POST':
if response.status_code == 200:
response.status_code = 422
elif response.status_code == 301:
response.status_code = 303
return response

return middleware

Then locate the MIDDLEWARE setting and add the path to the TurboMiddleware:

MIDDLEWARE = [
...
"your_project_name.middleware.TurboMiddleware",
]

Remember to replace 'your_project_name' with the actual name of your Django project.

6. Conclusion

Alright, now you should have an admin application that updates pages almost instantly and feels like a single-page application for the user. We’ve looked into how to easily integrate Turbo Drive to make the user experience better in Django Admin and older Django applications. With Turbo Drive, we’ve made great progress in making user interactions more dynamic and smooth.

In the next article i’m going to explore Turbo Frames functionality to add partial page updates for inline editing and row based actions in Django Admin. Clap and subscribe to the blog, to get notified!

--

--