Django integration for Fixi.js - a lightweight HTMX alternative for server-side rendering with hypermedia.
- 🎯 Automatic Fixi Detection - Middleware detects
FX-Requestheaders - 🔄 Smart Template Selection - Serve fragments for Fixi requests, full pages otherwise
- 🏗️ View Mixins - Drop-in enhancements for class-based views (
FxResponseMixin,ContextPersistenceMixin,OptimizedQueryMixin) - 🎨 Template Tags - Helpers for Fixi attributes, CSRF, and loading the (vendored) Fixi.js
- 📝 Form Helpers -
FxForm/FxModelFormrender Django forms with Fixi attributes - 🧪 Testing Utilities - A test client that lints every response for what fixi.js would silently ignore, and refuses to guess about redirects
- 🔎 Lint -
dj_fixi.lintknows the six attributes fixi reads and the htmx ones it does not (see Lint) - 🚨 System Checks -
manage.py checkcatches the misconfigurations that would otherwise fail silently (see System checks)
Not on PyPI yet. Install from this repository:
pip install git+https://github.com/codetalcott/dj-fixi.git# settings.py
INSTALLED_APPS = [
...
'dj_fixi',
]
MIDDLEWARE = [
...
'dj_fixi.middleware.FxMiddleware', # optional since 0.3.0
]dj_fixi in INSTALLED_APPS is required: it is what serves the vendored
fixi.js, registers the template tags, and runs the system checks.
The middleware is optional as of 0.3.0. dj-fixi reads the FX-Request header
directly, so fragment selection works without it. Add it anyway if you want
request.is_fx in your own view code, or Vary: FX-Request on responses dj-fixi
does not build itself.
# views.py
from django.views.generic import ListView
from dj_fixi.views import FxView
class ProductListView(FxView, ListView):
model = Product
context_object_name = 'products'
template_name = 'products/list.html'
partial_template = 'products/list_partial.html'Two things the checks will hold you to. dj-fixi classes come first in the base
list, because Django's generic mixins do not call super() in the hooks dj-fixi
overrides. And FxView supplies no HTTP handlers — compose it with a generic
view as above, or subclass FxTemplateView, which provides get(). A subclass
with neither answers 405 forever.
{# products/list.html - Full page #}
{% load fixi_tags %}
<!DOCTYPE html>
<html>
<head>
{% fixi_js %} {# serves the fixi.js vendored with dj-fixi (needs staticfiles) #}
{% fixi_events %} {# optional: enables FX-Trigger events (see below) #}
</head>
<body>
<div id="product-list">
{% include "products/list_partial.html" %}
</div>
</body>
</html>{# products/list_partial.html - Fragment for Fixi requests #}
{% for product in products %}
<div class="product">{{ product.name }}</div>
{% endfor %}Adapted from two of the author's unpublished projects: django-mod, for the
HypermediaView and middleware patterns, and python-modules/crud, for the CRUD
mixins and renderers.
Fixi is deliberately minimal, and this matters for what the server can assume:
- Request header: it sends only
FX-Request: true(plus anything you add viawindow.fixiCfg.headers). It does not send target/swap/trigger headers — those are client-side concerns — so that one header is the only signal the server gets. Read it withdj_fixi.is_fx(request), which works with or withoutFxMiddleware. - Attributes:
fx-action,fx-method,fx-target,fx-swap,fx-trigger. - Default swap is
outerHTML(HTMX defaults toinnerHTML).{% fx_attrs %}follows Fixi here: it omitsfx-swapforouterHTMLand emits it for anything else. - Events:
fx:init,fx:config,fx:before,fx:after,fx:swapped, etc. Fixi core reads no response headers (see "Client-side events" below).
HTMX sends HX-Request/HX-Target/…, reads response headers (HX-Trigger,
HX-Retarget, …), and ships history/indicators/OOB swaps. If you want that richer
server-driven protocol, use HTMX with django-htmx
rather than expecting Fixi to behave the same way.
FxResponseMixin sets an FX-Trigger response header on form success/error (e.g.
{"formSuccess": {"object_id": "7"}}). Fixi core does not read response headers, so
this header does nothing on its own. Enable it one of two ways:
- Shipped shim (zero-config): add
{% fixi_events %}after{% fixi_js %}. It turns the header into a bubblingCustomEventafter the swap, dispatched on the element that made the request if it is still in the document and on<body>otherwise (a delete'souterHTMLswap removes the element that asked). A stringtargetkey in the detail names a selector to dispatch on instead. Listen withdocument.addEventListener("formSuccess", (e) => …). The same file logs to the console the three mistakes fixi swallows: anfx-targetthat matches nothing (fixi swaps into the element itself), a swap spelled so fixi cannot perform it, and a failed request. - moxi.js: if you already use moxi, write the equivalent
on-fx:swappedhandler that readsevt.detail.cfg.response.headers.get('FX-Trigger').
Unsafe Fixi requests (POST/DELETE/…) still need a CSRF token; attach it per request via an
fx:config listener setting the X-CSRFToken header (see the demo's base.html).
fixi.js reads six attributes and ignores everything else, so hx-get written from habit,
fx-swap="outerhtml", fx-trigger="keyup delay:200ms" and an fx-target that matches
nothing all render a 200 and do nothing in the browser. dj_fixi.lint makes them loud on
the surface a test sees:
from dj_fixi.testing import FxTestClient
client = FxTestClient() # every text/html response is linted; errors raise
client.fx_get("/products/") # a redirect here raises too: pass follow=True or FalseEach finding names the element, the line, and the line of fixi.js that explains it, and
dj_fixi.lint.FINDING_IDS lists them all. python manage.py fixi_lint runs the same rules
over template source, for a person at the command line. FxMiddleware logs the same findings under
DEBUG and never raises. {% fx_attrs %} and FxForm refuse the same mistakes at render
time, before there is anything to lint.
dj-fixi's failure modes were almost all silent: a wrong base-class order, a
typo'd template name, or a forgotten setting produced a 200 with the wrong body
and no exception anywhere. Most of those root causes are gone as of 0.3.0. What
remains lives in your project's settings, URLconf, or filesystem, where the
library cannot fix it — so manage.py check reports it at startup instead.
| ID | Level | Fires when |
|---|---|---|
dj_fixi.E101 |
Error | A Django class earlier in the MRO shadows a dj-fixi hook, e.g. class V(ListView, FxView) |
dj_fixi.W102 |
Warning | Same, but your own class is the one that does not call super() |
dj_fixi.E103 |
Error | A routed FxView subclass defines no HTTP handler, so it answers 405 forever |
dj_fixi.E104 |
Error | A routed FxView subclass has no way to name a template |
dj_fixi.W001 |
Warning | FxMiddleware is not installed (advisory) |
dj_fixi.W002 |
Warning | No template engine enables context_processors.request |
dj_fixi.W201 |
Warning | A declared template_name/partial_template resolves to nothing |
dj_fixi.W202 |
Warning | A routed dj-fixi view declares no partial_template |
dj_fixi.W203 |
Warning | A project template uses htmx attributes, which fixi ignores |
dj_fixi.E301 |
Error | staticfiles is installed but fixi.js is unfindable |
dj_fixi.W302 |
Warning | django.contrib.staticfiles is not installed |
Checks only inspect views reachable from your URLconf, and stay silent on a
project that is not using the feature in question — a render_fx-only project
with function-based views triggers none of them. Silence any individually:
SILENCED_SYSTEM_CHECKS = ["dj_fixi.W202"]Django runs system checks on runserver, on migrate, and inside its own test
runner — but not under pytest, which is what most Django projects use. Add
this one test so your suite catches these too:
from dj_fixi.testing import assert_no_fixi_check_issues
def test_dj_fixi_is_configured_correctly():
assert_no_fixi_check_issues()The failure output carries each message and its hint verbatim, so it says what to
change rather than just that something is wrong. fixi_check_messages() returns
the same messages if you want to inspect them, and both honor
SILENCED_SYSTEM_CHECKS.
Why E101 exists. Django's generic view mixins do not call super() in
get_template_names or get_context_data, so anything listed after them in the
MRO is dead code. class V(ListView, FxView) still renders, still returns 200,
and silently serves the full page to every Fixi request with is_fx missing from
the context. Put dj-fixi classes first:
class ProductListView(FxView, ListView): # correct
class ProductListView(ListView, FxView): # dj_fixi.E101dj-fixi deliberately stays small — it's the request/response/template adapter for
Fixi. Declarative, inline-editable tables (server-rendered, with Fixi row swaps) live
in a separate companion package, dj-fixi-tables, which builds on dj-fixi.
That package is not published yet.
Earlier releases shipped two half-finished table systems (a server-rendered
ModelTableand a JSONFxCRUDViewfor a client plugin). Both were removed in favor of the focuseddj-fixi-tablespackage.
- llms.txt - Complete API reference in one file, written for coding agents. Point your agent at it and it should not need to read the source.
- CLAUDE.md - Development guide
MIT. The vendored dj_fixi/static/dj_fixi/fixi.js is Fixi.js, by Big Sky
Software, under the Zero-Clause BSD license.