TurboNav Integration Guide
This guide covers integrating djust LiveViews with TurboNav for SPA-style navigation in Django projects.
What is TurboNav?
TurboNav intercepts link clicks and form submissions, fetches the new page via AJAX, and swaps the <main> element's innerHTML — avoiding full page reloads. This gives users a faster, app-like navigation experience while keeping server-rendered HTML as the source of truth.
djust works with TurboNav out of the box. When TurboNav swaps page content, djust automatically tears down old WebSocket connections and reinitializes LiveViews found in the new content.
How It Works
- User clicks a link
- TurboNav fetches the target page via
fetch() - TurboNav swaps
<main>innerHTML with the new page content - TurboNav runs
loadPageScripts()for any<script data-turbo-track="reload">tags - TurboNav fires a
turbo:loadevent - djust listens for
turbo:load, disconnects any existing WebSocket, and reinitializes LiveViews in the new DOM
Setup
1. Add TurboNav to Your Base Template
Include the TurboNav script in your base template's <head>:
<head>
<script src="{% static 'turbo.js' %}" data-turbo-track="reload"></script>
<script src="{% static 'djust/client.js' %}" data-turbo-track="reload"></script>
</head>
The data-turbo-track="reload" attribute tells TurboNav's loadPageScripts() to manage these scripts across navigations.
2. Configure the Swap Target
TurboNav swaps the contents of <main>. Structure your base template accordingly:
<body>
<nav><!-- persistent navigation --></nav>
<main>
{% block content %}{% endblock %}
</main>
<footer><!-- persistent footer --></footer>
</body>
Elements outside <main> (nav, footer, sidebars) persist across navigations.
3. Place LiveViews Inside Main
Your LiveView templates render inside {% block content %}, so they naturally land inside <main>:
{% block content %}
<div dj-view="{{ view_id }}">
<!-- LiveView content -->
</div>
{% endblock %}
How djust Handles Navigation
When TurboNav fires turbo:load, djust runs reinitLiveViewForTurboNav() which:
- Disconnects the existing WebSocket — closes the old connection cleanly
- Resets client VDOM version — prevents stale diff conflicts
- Clears lazy hydration state — resets
dj-lazytracking - Stamps root attributes — auto-applies
data-djust-rooton new content - Scans for LiveView containers — finds all
[dj-view]elements in the new DOM - Opens a new WebSocket — connects to the server for the new page's LiveViews
- Re-binds event handlers — attaches
dj-click,dj-submit, etc. to new elements - Cleans up poll intervals — stops old
dj-polltimers before creating new ones
Initialization Order
djust guards against race conditions between DOMContentLoaded and turbo:load:
- If
turbo:loadfires before djust has initialized (first page load), reinit is deferred - If
turbo:loadfires after initialization, reinit runs immediately - Duplicate WebSocket connections are prevented by disconnecting before reconnecting
Inline Scripts
TurboNav swaps innerHTML, which means inline <script> tags in the new content are not executed automatically by the browser. TurboNav's loadPageScripts() handles <script data-turbo-track="reload"> tags, but other inline scripts require explicit execution.
If you need inline scripts in a LiveView page:
<!-- Option 1: Use data-turbo-track -->
<script data-turbo-track="reload">
// This will be executed by loadPageScripts
</script>
<!-- Option 2: Move logic to a djust event handler (preferred) -->
<button dj-click="do_something">Click me</button>
Prefer moving client-side logic into djust event handlers when possible.
Known Caveats
DOMContentLoaded Does Not Fire
After TurboNav swaps content, DOMContentLoaded does not fire again. Any code that depends on this event will not run on subsequent navigations. djust handles this internally by checking document.readyState and using the turbo:load event.
If you have non-djust JavaScript that relies on DOMContentLoaded, listen for turbo:load as well:
function initMyWidget() {
// Widget initialization code
}
document.addEventListener('DOMContentLoaded', initMyWidget);
window.addEventListener('turbo:load', initMyWidget);
Debug Panel State
The djust debug panel does not persist its state across TurboNav navigations. Switching pages resets the panel. This is tracked as a remaining item in the roadmap.
Pages Without LiveViews
If TurboNav navigates to a page with no [dj-view] containers, djust disconnects the WebSocket and does not reconnect. This is the correct behavior — no LiveViews means no WebSocket needed.
Design Decision: Separate Integration
TurboNav is not bundled with djust. It remains a separate library that users opt into. This decision was made because:
- Not all Django projects need SPA-style navigation
- TurboNav adds complexity (script execution, event lifecycle) that not every app benefits from
- Keeping them separate allows each to evolve independently
- djust's
turbo:loadhandler is minimal (~60 lines) and adds no overhead when TurboNav is absent
If TurboNav is not present, the turbo:load event never fires and djust operates normally with full page reloads.
Troubleshooting
LiveView not initializing after navigation: Ensure your LiveView markup is inside <main>. TurboNav only swaps <main> content.
Duplicate WebSocket connections: This was fixed in v0.3.6. Update to the latest version. The fix ensures reinitLiveViewForTurboNav() always disconnects before reconnecting.
Scripts not executing: Add data-turbo-track="reload" to any <script> tags that need to run after navigation.
Triple initialization on first load: Fixed in v0.3.6. The pendingTurboReinit guard prevents djust from reinitializing multiple times when both DOMContentLoaded and turbo:load fire.