Loading States & Background Work
Loading Directives
<!-- Disable button during event -->
<button dj-click="save" dj-loading.disable>Save</button>
<!-- Show spinner during event -->
<div dj-loading.show dj-loading.for="save" style="display:none">Saving...</div>
<!-- Hide element during event -->
<span dj-loading.hide dj-loading.for="save">Ready</span>
<!-- Add CSS class during event -->
<div dj-loading.class="opacity-50" dj-loading.for="save">Content</div>
<!-- Show with specific display value -->
<div dj-loading.show="flex" dj-loading.for="save" style="display:none">...</div>
Scoping Rules
- Implicit:
dj-loading.*on an element withdj-click="event"scopes to that event - Explicit:
dj-loading.for="event_name"scopes to any named event (works on any element)
CSS
- Trigger element gets
djust-loadingclass during loading <body>getsdjust-global-loading
AsyncWorkMixin (Background Work)
from djust import LiveView
from djust.mixins.async_work import AsyncWorkMixin
from djust.decorators import event_handler
class MyView(AsyncWorkMixin, LiveView):
def mount(self, request, **kwargs):
self.loading = False
self.result = ""
@event_handler()
def generate(self, **kwargs):
self.loading = True # Sent to client immediately
self.start_async(self._do_work) # Runs after response sent
def _do_work(self):
"""Runs in background thread. View re-renders when done."""
try:
self.result = slow_api_call()
except Exception as e:
self.error = str(e)
self.loading = False
How async_pending Works
- Handler sets state + calls
start_async(callback) - Server sends patches with
async_pending: true— client keeps loading active - Background callback runs in thread
- When done, server re-renders + sends final patches (no
async_pending) — loading stops
start_async Signature
self.start_async(callback, *args, **kwargs)
callback: Method on the view (receivesselfimplicitly)*args, **kwargs: Forwarded to callback
Error Handling
Always catch exceptions in the callback — uncaught exceptions are logged but leave the client in loading state:
def _do_work(self):
try:
self.result = risky_call()
except Exception as e:
self.error = str(e)
finally:
self.loading = False
Complete Example
class ReportView(AsyncWorkMixin, LiveView):
template_name = "report.html"
def mount(self, request, **kwargs):
self.generating = False
self.report = ""
@event_handler()
def generate_report(self, **kwargs):
self.generating = True
self.start_async(self._generate)
def _generate(self):
self.report = call_api()
self.generating = False
<button dj-click="generate_report" dj-loading.disable>Generate</button>
<div dj-loading.show dj-loading.for="generate_report" style="display:none">
Generating...
</div>
{% if report %}
<div>{{ report }}</div>
{% endif %}
@background Decorator
Simpler syntax — entire handler runs in background:
from djust.decorators import background
class ContentView(LiveView):
@event_handler
@background
def generate_content(self, prompt: str = "", **kwargs):
self.generating = True
self.content = call_llm(prompt)
self.generating = False
vs start_async:
@background: entire handler runs in backgroundstart_async(): update state first, then start background work
Can combine with other decorators:
@event_handler
@debounce(wait=0.5)
@background
def auto_save(self, **kwargs):
self.save_draft()
Task name is function name; cancel via self.cancel_async("generate_content").
Configuration
# settings.py
LIVEVIEW_CONFIG = {
'loading_grouping_classes': ['d-flex', 'flex'], # Container scoping
}
Programmatic API (JS)
window.djust.globalLoadingManager.startLoading('event_name');
window.djust.globalLoadingManager.stopLoading('event_name');
window.djust.globalLoadingManager.pendingEvents.has('event_name');