Skip to content

Django

Django owns routing, templates, and form processing. Brio enhances only the marked root. Read the ownership and data boundaries before adding partial-page navigation.

Place the Brio browser bundle at myapp/static/myapp/brio.min.js. Copy widgets.js alongside it, and add counter.js:

import { createWidgetManager } from './widgets.js';
const widgets = createWidgetManager(brio.createApp);
widgets.mountWithin(document);

In your template, render initial_count from a view that supplies a validated integer (for example, {"initial_count": 3}):

{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>A Django page with Brio</title>
<script defer src="{% static 'myapp/brio.min.js' %}"></script>
<script type="module" src="{% static 'myapp/counter.js' %}"></script>
</head>
<body>
<section data-brio-counter data-start="{{ initial_count }}" aria-label="Counter">
<p>Count: <output b-text="$data.count" aria-live="polite">{{ initial_count }}</output></p>
<button type="button" b-on-click="$data.count++">Add one</button>
</section>
</body>
</html>

Django escapes attributes and text. $data.count is a Brio expression, not a Django template variable. Keep these scripts in your base template so partials do not register duplicate listeners.

Add these listeners to the same counter.js, before or after its initial mount. They do not require htmx when it is absent:

document.addEventListener('htmx:beforeCleanupElement', event => {
widgets.disposeWithin(event.detail.elt);
});
document.addEventListener('htmx:load', event => {
widgets.mountWithin(event.detail.elt);
});

beforeCleanupElement releases widgets before a normal swap removes their DOM. htmx:load mounts newly inserted content, including when the new root itself is a counter. Repeated load events do not duplicate mounts.

This covers normal htmx replacement, not arbitrary morph extensions mutating Brio-owned children in place. Configure those extensions around ownership boundaries or dispose the affected region before morphing it. Brio state resets when a region is replaced.

For larger initial data use Django’s json_script, then parse the element’s textContent; do not use safe to insert raw JSON into executable JavaScript. The counter only needs a numeric attribute.

Retain {% csrf_token %} for server forms and follow Django’s CSRF guidance for unsafe fetch requests. Surface server validation and network failures. Brio does not make an optimistic local counter into a persisted server value.