Decorators API Reference
from djust.decorators import (
event_handler,
debounce,
throttle,
optimistic,
cache,
client_state,
loading,
permission_required,
background,
)
@event_handler
Mark a method as callable from the client. Required on all event handlers — djust blocks any unmarked method for security.
@event_handler(params=None, description="", coerce_types=True)
Parameters:
params(list[str], optional) — Explicit list of allowed parameter names. Defaults to auto-extraction from the function signature.description(str) — Human-readable description shown in the debug panel. Defaults to the method docstring.coerce_types(bool, defaultTrue) — Automatically coerce string values fromdata-*attributes to the expected types based on type hints ("5"→5forint).
Usage:
# Simple — no arguments
@event_handler()
def increment(self, **kwargs):
self.count += 1
# With type coercion (item_id="5" → item_id=5)
@event_handler()
def delete(self, item_id: int = 0, **kwargs):
Item.objects.filter(pk=item_id).delete()
# Input/change: parameter must be named 'value'
@event_handler()
def search(self, value: str = "", **kwargs):
self.query = value
# Form submit: named fields arrive as kwargs
@event_handler()
def save_form(self, name="", email="", **kwargs):
User.objects.create(name=name, email=email)
# Disable type coercion to receive raw strings
@event_handler(coerce_types=False)
def raw_handler(self, value: str = "", **kwargs):
# value is always a string, not coerced
pass
Rules:
- Always accept
**kwargs— djust passes extra metadata - Always provide default values for all parameters
valueis the magic parameter name fordj-inputanddj-changeeventsdata-item-idbecomesitem_id(kebab-case → snake_case)
@debounce
Debounce event handler calls on the client side. The handler fires only after the specified delay has elapsed since the last event.
@debounce(wait=0.3, max_wait=None)
Parameters:
wait(float) — Seconds to wait after the last event before firing. Default0.3.max_wait(float | None) — Maximum seconds to wait even if events keep firing. DefaultNone(unlimited).
Usage:
@event_handler()
@debounce(wait=0.5)
def search(self, value: str = "", **kwargs):
"""Fires 500ms after the user stops typing."""
self.results = Product.objects.filter(name__icontains=value)
@event_handler()
@debounce(wait=0.3, max_wait=2.0)
def autosave(self, content: str = "", **kwargs):
"""Fires 300ms after last change, but always fires within 2 seconds."""
self.draft = content
Must be applied inside @event_handler() (closer to the function).
@throttle
Limit how often a handler fires. Useful for scroll, resize, or mouse-move events.
@throttle(interval=0.1, leading=True, trailing=True)
Parameters:
interval(float) — Minimum seconds between calls. Default0.1.leading(bool) — Fire on the first event. DefaultTrue.trailing(bool) — Fire on the last event after the interval. DefaultTrue.
Usage:
@event_handler()
@throttle(interval=0.1)
def on_scroll(self, position: int = 0, **kwargs):
"""Fires at most 10 times/second."""
self.scroll_pos = position
@optimistic
Apply state changes immediately in the UI before the server confirms. If the handler raises, djust rolls back the optimistic update.
@optimistic
No arguments — apply directly.
Usage:
@event_handler()
@optimistic
def toggle_like(self, item_id: int = 0, **kwargs):
"""UI updates instantly; server confirms asynchronously."""
item = next(i for i in self.items if i["id"] == item_id)
item["liked"] = not item["liked"]
@cache
Cache handler responses client-side. The response is stored in the browser indexed by the specified key parameters.
@cache(ttl=60, key_params=None)
Parameters:
ttl(int) — Cache lifetime in seconds. Default60.key_params(list[str] | None) — Parameter names to use as cache key. Default[](caches by handler name only).
Usage:
@event_handler()
@cache(ttl=300, key_params=["value"])
def search(self, value: str = "", **kwargs):
"""Results for "laptop" are cached for 5 minutes."""
self.results = Product.objects.filter(name__icontains=value)[:20]
@client_state
Share state via a client-side pub/sub bus. When specified keys change, other components subscribed to those keys update automatically.
@client_state(keys)
Parameters:
keys(list[str]) — Attribute names to publish after this handler runs.
Usage:
@event_handler()
@client_state(keys=["filter", "sort"])
def update_filter(self, filter: str = "all", **kwargs):
self.filter = filter
# Other components listening for 'filter' update automatically
@loading
Set a boolean attribute to True while the handler is running, False after. Use to show loading spinners or disable buttons.
@loading(attr)
Parameters:
attr(str) — Name of the boolean attribute to set.
Usage:
@event_handler()
@loading("is_saving")
def save(self, **form_data):
"""self.is_saving=True while this runs."""
time.sleep(1)
self.saved = True
<button dj-click="save" {% if is_saving %}disabled{% endif %}>
{% if is_saving %}Saving...{% else %}Save{% endif %}
</button>
@permission_required
Check Django permissions before the handler executes. Returns an error if the user lacks the required permission(s).
@permission_required(perm)
Parameters:
perm(str | list[str]) — Django permission string(s) (e.g.,"myapp.can_delete").
Usage:
@event_handler()
@permission_required("myapp.can_delete")
def delete_item(self, item_id: int = 0, **kwargs):
Item.objects.filter(pk=item_id).delete()
# Require multiple permissions (all must be satisfied)
@event_handler()
@permission_required(["myapp.can_edit", "myapp.can_publish"])
def publish(self, **kwargs):
self.item.published = True
self.item.save()
@background
Run the entire event handler in a background thread after flushing current state. The view re-renders and sends patches when the handler completes.
@background
No arguments — apply directly.
Usage:
from djust.decorators import background
@event_handler
@background
def generate_content(self, prompt: str = "", **kwargs):
"""Entire method runs in background thread."""
self.generating = True
try:
self.content = call_llm(prompt) # Long-running operation
except Exception as e:
self.error = str(e)
finally:
self.generating = False
How it works:
- Current view state is flushed to client (e.g., loading spinner appears)
- Handler executes in background thread
- View re-renders and sends patches when handler completes
- Loading state stops (spinner disappears)
Task naming and cancellation:
The task name is automatically set to the handler's function name. Cancel via self.cancel_async(name):
@event_handler
@background
def long_operation(self, **kwargs):
# Task name is "long_operation"
...
@event_handler
def cancel_operation(self, **kwargs):
self.cancel_async("long_operation")
Combining with other decorators:
@event_handler
@debounce(wait=0.5)
@background
def auto_save(self, **kwargs):
# Debounced and runs in background
self.save_draft()
When to use @background vs start_async():
- Use
@backgroundwhen the entire handler should run in background - Use
self.start_async(callback)when you need to update state before starting background work, or need multiple concurrent tasks with different names
See also: Loading States & Background Work guide
Decorator Composition
Decorators compose — apply multiple to one handler. Order matters: decorators execute from outermost to innermost (top to bottom):
@event_handler() # outermost — registers the handler
@debounce(0.5) # wait for typing to stop
@optimistic # update UI immediately
@cache(ttl=60) # return cached result if available
def search(self, value: str = "", **kwargs):
self.results = Product.objects.filter(name__icontains=value)
Execution order: debounce → optimistic → cache → search()
See Also
- Events guide — event binding in templates
- State Management — higher-level patterns
- LiveView API