I'm using the below to render model initial counts on load.
consumers.py:
from channels.generic.websocket import WebsocketConsumer
from django.template.loader import render_to_string
from myapp.models import Model1, Model2, Model3, Model4
class DashboardHeaderConsumer(WebsocketConsumer):
def update_dashboard_counts(self):
stats = [
{'name': 'Model1', 'value': Model1.objects.count()},
{'name': 'Model2', 'value': Model2.objects.count()},
{'name': 'Model3', 'value': Model3.objects.count()},
{'name': 'Model4', 'value': Model4.objects.count()},
]
html = render_to_string('dashboard/header-stats.html', {'stats': stats})
self.send(text_data=html)
def connect(self):
self.accept()
self.update_dashboard_counts()
header-stats.html
:
{% for stat in stats %}
<div class="col-sm-6 col-xl-3">
<div class="dashboard-stat rounded d-flex align-items-center justify-content-between p-4">
<div class="ms-3">
<p class="mb-2">{{ stat.name }}</p>
<h6 class="mb-0">{{ stat.value }}</h6>
</div>
</div>
</div>
{% endfor %}
my-template.html
:
{% load static %}
{% block styles %}
<link rel="stylesheet" href="{% static 'css/dashboard.css' %}">
{% endblock %}
<div class="container-fluid pt-4 px-4">
<div class="row g-2 mb-2 stats-wrapper"
hx-ext="ws"
ws-connect="/ws/dashboard/header/"
hx-target=".stats-wrapper"
hx-swap="innerHTML"
>
</div>
<div class="active-tasks-scroll-container">
<div class="row flex-nowrap g-2">
</div>
</div>
</div>
I'm expecting the counts to show up. However, despite the message is received and can be seen in under the networks websocket request, the dom is empty. This is what gets rendered:
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<title>App Title</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/index.css" rel="stylesheet">
<script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/htmx.min.js"></script>
<script src="/static/js/htmx-ext-ws%402.0.2"></script>
<style> .htmx-indicator {
opacity: 0
}
.htmx-request .htmx-indicator {
opacity: 1;
transition: opacity 200ms ease-in;
}
.htmx-request.htmx-indicator {
opacity: 1;
transition: opacity 200ms ease-in;
} </style>
</head>
<body>
<div class="content-wrapper" id="content-wrapper">
<div class="container-fluid pt-4 px-4">
<div class="row g-2 mb-2 stats-wrapper" hx-ext="ws" ws-connect="/ws/dashboard/header/"
hx-target=".stats-wrapper" hx-swap="innerHTML">
</div>
<div class="active-tasks-scroll-container">
<div class="row flex-nowrap g-2">
</div>
</div>
</div>
</div>
</body>
</html>
I tried everything from sending the result as json, adding an id, adding hx-swap-oob="beforeend"
, setting hx-receive
, ... nothing is working.
I think you're sending raw HTML from the Django Channels consumer
self.send(text_data=html)
but the HTML doesn’t contain the .stats-wrapper div, so HTMX has no idea what part of the DOM to replace.
The swap target is looking for .stats-wrapper
, but that’s not included in the response.
So, to fix that update your header-stats.html template like this:
<div class="row g-2 mb-2 stats-wrapper">
{% for stat in stats %}
<div class="col-sm-6 col-xl-3">
<div class="dashboard-stat rounded d-flex align-items-center justify-content-between p-4">
<div class="ms-3">
<p class="mb-2">{{ stat.name }}</p>
<h6 class="mb-0">{{ stat.value }}</h6>
</div>
</div>
</div>
{% endfor %}
</div>
and if you don't want to include .stats-wrapper
in your response (and only send the inner content), then make the response out-of-band:
<div class="row g-2 mb-2 stats-wrapper" hx-swap-oob="true">
{% for stat in stats %}
...
{% endfor %}
</div>
This way, HTMX will find the matching .stats-wrapper
in the DOM and replace it, even though it's not directly targeted.