* Home
:PROPERTIES:
:ONE: one-custom-default-home
:CUSTOM_ID: /
:TITLE:
:DESCRIPTION: Build real-time, reactive interfaces with Django using WebSockets: write Python, not JavaScript.
:NAVIGATOR-ACTIVE: home
:END:

** Build real-time SPAs with Python, not JavaScript

Django LiveView is a framework for creating real-time, interactive web applications entirely in Python, inspired by [[https://hexdocs.pm/phoenix_live_view/][Phoenix LiveView]] and [[https://laravel-livewire.com/][Laravel Livewire]]. It is built on top of Django Channels.

Build rich, dynamic user experiences with server-rendered HTML without writing a single line of JavaScript. Perfect for Django developers who want real-time features without the complexity of a separate frontend framework.

** See it in action

Here's a complete example: a button that loads the latest blog article with a single click.

*HTML:*

#+BEGIN_SRC html
<button
    data-liveview-function="load_latest_article"
    data-action="click->page#run">
    Load Latest Article
</button>

<div id="article-container"></div>
#+END_SRC

*Python:*

#+BEGIN_SRC python
from liveview import liveview_handler, send
from django.template.loader import render_to_string

@liveview_handler("load_latest_article")
def load_latest_article(consumer, content):
    # Get the latest article from database
    article = Article.objects.latest('published_at')

    # Render with Django templates
    html = render_to_string('article.html', {
        'article': article
    })

    # Send to frontend
    send(consumer, {
        "target": "#article-container",
        "html": html
    })
#+END_SRC

*Result (after clicking the button):*

#+BEGIN_SRC html
<button
    data-liveview-function="load_latest_article"
    data-action="click->page#run">
    Load Latest Article
</button>

<div id="article-container">
    <article>
        <h2>Understanding Django Channels</h2>
        <p class="meta">Published on Dec 15, 2024 by Jane Doe</p>
        <p>Django Channels extends Django to handle WebSockets,
           long-running connections, and background tasks...</p>
        <a href="/blog/understanding-django-channels/">Read more</a>
    </article>
</div>
#+END_SRC

That's it! No page reload, no API endpoints, no REST, no GraphQL, no frontend framework. The article appears instantly via WebSocket. Just Python and Django templates working together in real-time.

** Key features

- 🎯 **Create SPAs without using APIs**: No REST or GraphQL needed
- 🎨 **Uses Django's template system** to render the frontend (without JavaScript frameworks)
- 🐍 **Logic stays in Python**: No split between backend and frontend
- 🛠️ **Use all of Django's tools**: ORM, forms, authentication, admin, etc.
- ⚡ **Everything is asynchronous by default**: Built on Django Channels
- 📚 **Zero learning curve**: If you know Python and Django, you're ready
- 🔄 **Real-time by design**: All interactions happen over WebSockets
- 🔋 **Batteries included**: JavaScript assets bundled, automatic reconnection with exponential backoff
- 💡 **Type hints** and modern Python (3.10+)
- 📡 **Broadcast support** for multi-user real-time updates
- 🔐 **Middleware system** for authentication and authorization

** Why Django LiveView?

[[https://github.com/tanrax/django-interactive-frameworks-benchmark][Benchmarks]] show Django LiveView delivers the fastest response times among Django interactive frameworks:

#+ATTR_HTML: :class center-block image image--benchmark
[[#/img/benchmark-response-time.webp][Response Time Comparison]]

| Framework    | Technology      | Response Time | Data Transfer | Use Case                             |
|--------------+-----------------+---------------+---------------+--------------------------------------|
| **LiveView** | WebSockets      | **9.35ms**    | 450 bytes     | Real-time dashboards, collaborative apps |
| HTMX         | AJAX            | 16.48ms       | ~37 KB        | Modern UX with minimal JavaScript    |
| Unicorn      | AJAX            | 26.76ms       | ~71 KB        | Complex forms with reactive state    |
| SSR          | POST + redirect | 47.25ms       | ~8.5 KB       | SEO-critical pages, traditional CRUD |

Django LiveView is approximately **43% faster than HTMX** and **80% faster than traditional SSR** through persistent WebSocket connectivity.

*** Technology Comparison

| Feature            | LiveView         | SSR         | HTMX        | Unicorn     |
|--------------------+------------------+-------------+-------------+-------------|
| **Transport**      | WebSocket        | HTTP        | AJAX        | AJAX        |
| **Update Type**    | Real-time        | Full reload | Partial     | Reactive    |
| **Multi-user**     | ✅ Broadcast     | ❌          | ❌          | ❌          |
| **Infrastructure** | Redis + Channels | Django only | Django only | Django only |

*** What makes it different?

The key difference is the connection model:

**Django LiveView** uses a persistent WebSocket connection that stays open between the client and server. This allows bidirectional, real-time communication with minimal latency. Think of it like a phone call: once connected, both sides can talk instantly.

**HTMX** sends a new HTTP request for each user interaction and updates only part of the page. It's like sending text messages: you send a message, wait for a response, then repeat.

**Traditional SSR** reloads the entire page with each interaction through a POST request followed by a redirect. It's like hanging up and calling back every time you want to say something.

#+ATTR_HTML: :class center-block image image--home
[[#/img/step-1.avif][Architecture send]]

#+ATTR_HTML: :class center-block image image--home
[[#/img/step-2.avif][Architecture receive]]

** How does it work?

Let's illustrate with an example: displaying article number 2.

1. [@1] A WebSocket connection (a channel) is established between the client and the server.

2. [@2] JavaScript sends a message via WebSocket to the server (Django).

   #+ATTR_HTML: :class center-block image image--home
   [[#/img/step-3.avif][Send string]]

3. [@3] Django interprets the message and renders the HTML of the article through the template system and the database.

4. [@4] Django sends the HTML to JavaScript via the channel and specifies which selector to embed it in.

   #+ATTR_HTML: :class center-block image image--home
   [[#/img/step-4.avif][Send JSON]]

5. [@5] JavaScript renders the received HTML in the indicated selector.

   #+ATTR_HTML: :class center-block image image--home
   [[#/img/step-5.avif][Place HTML]]

The same process is repeated for each action: clicking a button, submitting a form, navigating, etc.

** Ready to start?

Are you ready to create your first real-time SPA? Let's go to the [[#/quick-start/][Quick start]].

* Install
:PROPERTIES:
:ONE: one-custom-default-doc
:CUSTOM_ID: /docs/install/
:TITLE: Install
:DESCRIPTION: Install Django LiveView.
:NAVIGATOR-ACTIVE: docs
:END:

** Requirements

- Python 3.10+
- Django 4.2+
- Redis (for Channels layer)
- Channels 4.0+

** Installation

Install Django LiveView with pip:

#+BEGIN_SRC sh
pip install django-liveview
#+END_SRC

** Configure Django

Add to your ~settings.py~:

#+BEGIN_SRC python
# settings.py
INSTALLED_APPS = [
    "daphne",  # Must be first for ASGI support
    "channels",
    "liveview",
    # ... your other apps
]

# ASGI configuration
ASGI_APPLICATION = "your_project.asgi.application"

# Configure Channels with Redis
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("127.0.0.1", 6379)],
        },
    },
}
#+END_SRC

** Setup ASGI routing

Create or update ~asgi.py~:

#+BEGIN_SRC python
# asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator
from liveview.routing import get_liveview_urlpatterns

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "your_project.settings")

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AllowedHostsOriginValidator(
        AuthMiddlewareStack(
            URLRouter(
                get_liveview_urlpatterns()
            )
        )
    ),
})
#+END_SRC

** Add JavaScript to your base template

#+BEGIN_SRC html
<!-- templates/base.html -->
{% load static %}
{% load liveview %}
<!DOCTYPE html>
<html lang="en" data-room="{% liveview_room_uuid %}">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}My Site{% endblock %}</title>
</head>
<body data-controller="page">
    {% block content %}{% endblock %}

    <!-- Django LiveView JavaScript -->
    <script src="{% static 'liveview/liveview.min.js' %}" defer></script>
</body>
</html>
#+END_SRC

**Important attributes:**
- ~data-room="{% liveview_room_uuid %}"~ on ~<html>~: unique room ID for each page load
- ~data-controller="page"~ on ~<body>~: activates the Stimulus controller

The ~{% liveview_room_uuid %}~ template tag generates a random UUID for each request, ensuring isolated sessions and preventing room ID enumeration attacks.

**Alternative: User-specific Room ID** (use with caution)
#+BEGIN_SRC html
<html lang="en" data-room="{% if request.user.is_authenticated %}user-{{ request.user.id }}{% else %}{% liveview_room_uuid %}{% endif %}">
#+END_SRC
⚠️ *Warning:* Using predictable IDs like user IDs can be a security risk. An attacker could subscribe to another user's room by guessing their ID. Only use this if you implement proper authorization checks in your handlers.

We strongly recommend that you follow the [[#/quick-start/][Quick start]] to see the installation in action.

* Handlers
:PROPERTIES:
:ONE: one-custom-default-doc
:CUSTOM_ID: /docs/handlers/
:TITLE: Handlers
:DESCRIPTION: LiveView handlers of Django LiveView.
:NAVIGATOR-ACTIVE: docs
:END:

Handlers are Python functions that respond to WebSocket messages from the client. They contain your business logic and can render HTML, update the database, broadcast to multiple users, and more.

** Your First Handler

Let's start with the simplest possible handler. This handler will be called when a button is clicked and will update a section of the page.

Python code:

#+BEGIN_SRC python
from liveview import liveview_handler, send

@liveview_handler("say_hello")
def say_hello(consumer, content):
    send(consumer, {
        "target": "#greeting",
        "html": "<p>Hello, World!</p>"
    })
#+END_SRC

HTML code:

#+BEGIN_SRC html
<div>
    <div id="greeting"></div>
    <button
        data-liveview-function="say_hello"
        data-action="click->page#run">
        Say Hello
    </button>
</div>
#+END_SRC

When you click the button:
1. The frontend sends a WebSocket message with ~function: "say_hello"~
2. Django LiveView calls the ~say_hello~ handler
3. The handler sends back HTML to replace the content of ~#greeting~
4. The page updates instantly without a full reload

** Working with Form Data

Most handlers need to receive data from the user. All form inputs within the same container are automatically sent to your handler.

Python code:

#+BEGIN_SRC python
@liveview_handler("greet_user")
def greet_user(consumer, content):
    # Get the name from form data
    name = content["form"].get("name", "Anonymous")

    send(consumer, {
        "target": "#greeting",
        "html": f"<p>Hello, {name}! Welcome to Django LiveView.</p>"
    })
#+END_SRC

HTML code:

#+BEGIN_SRC html
<div>
    <div id="greeting"></div>
    <input type="text" name="name" placeholder="Enter your name">
    <button
        data-liveview-function="greet_user"
        data-action="click->page#run">
        Greet Me
    </button>
</div>
#+END_SRC

The ~content["form"]~ dictionary contains all input values with their ~name~ attribute as the key.

** Using Templates for Complex HTML

For anything beyond simple strings, use Django templates to render your HTML.

Python code:

#+BEGIN_SRC python
from django.template.loader import render_to_string

@liveview_handler("show_profile")
def show_profile(consumer, content):
    user_id = content["form"].get("user_id")

    # Get user from database
    from .models import User
    user = User.objects.get(id=user_id)

    # Render template with context
    html = render_to_string("profile_card.html", {
        "user": user,
        "is_online": True
    })

    send(consumer, {
        "target": "#profile-container",
        "html": html
    })
#+END_SRC

HTML code:

#+BEGIN_SRC html
<div>
    <div id="profile-container"></div>
    <input type="hidden" name="user_id" value="123">
    <button
        data-liveview-function="show_profile"
        data-action="click->page#run">
        Load Profile
    </button>
</div>
#+END_SRC

Template (~profile_card.html~):

#+BEGIN_SRC html
<div class="profile-card">
    <img src="{{ user.avatar }}" alt="{{ user.name }}">
    <h3>{{ user.name }}</h3>
    <p>{{ user.bio }}</p>
    {% if is_online %}
        <span class="badge">Online</span>
    {% endif %}
</div>
#+END_SRC

** Understanding the Content Parameter

Every handler receives two parameters: ~consumer~ and ~content~. The ~content~ dictionary contains all the information from the client.

#+BEGIN_SRC python
@liveview_handler("example")
def example(consumer, content):
    # content structure:
    # {
    #     "function": "example",           # Handler name
    #     "form": {...},                   # All form inputs
    #     "data": {...},                   # Custom data-data-* attributes
    #     "lang": "en",                    # Current language
    #     "room": "user_123"              # WebSocket room identifier
    # }
    pass
#+END_SRC

** Auto-discovery

Django LiveView automatically discovers handlers in ~liveview_components/~ directories within your installed apps:

#+BEGIN_SRC
my_app/
├── liveview_components/
│   ├── __init__.py
│   ├── users.py
│   ├── posts.py
│   └── comments.py
#+END_SRC

Handlers are loaded on startup with this output:
#+BEGIN_SRC
✓ Imported: my_app.liveview_components.users
✓ Imported: my_app.liveview_components.posts
✓ Imported: my_app.liveview_components.comments
#+END_SRC

** Using Custom Data Attributes

Sometimes you need to send additional data that isn't part of a form. Use ~data-data-*~ attributes for this.

Python code:

#+BEGIN_SRC python
@liveview_handler("delete_comment")
def delete_comment(consumer, content):
    # Access custom data from data-data-* attributes
    comment_id = content["data"]["comment_id"]
    post_id = content["data"]["post_id"]

    # Delete from database
    from .models import Comment
    Comment.objects.filter(id=comment_id).delete()

    send(consumer, {
        "target": f"#comment-{comment_id}",
        "remove": True  # Remove the element from DOM
    })
#+END_SRC

HTML code:

#+BEGIN_SRC html
<div id="comment-123" class="comment">
    <p>This is a comment</p>
    <button
        data-liveview-function="delete_comment"
        data-data-comment-id="123"
        data-data-post-id="456"
        data-action="click->page#run">
        Delete
    </button>
</div>
#+END_SRC

The attribute ~data-data-comment-id~ becomes ~comment_id~ (with underscores, not camelCase) within ~content["data"]~.

**Important**: Django LiveView automatically converts all attribute names from camelCase to snake_case. HTML attributes with hyphens (~data-data-comment-id~) are first converted by the browser to camelCase (~dataCommentId~), which Django LiveView then converts to snake_case (~comment_id~) in your handlers.

** Appending Content to Lists

When building dynamic lists (like infinite scroll or chat messages), you want to add items without replacing the entire list.

Python code:

#+BEGIN_SRC python
@liveview_handler("load_more_posts")
def load_more_posts(consumer, content):
    page = int(content["form"].get("page", 1))

    # Get next page of posts
    from .models import Post
    posts = Post.objects.all()[(page-1)*10:page*10]

    # Render new posts
    html = render_to_string("posts_list.html", {
        "posts": posts
    })

    send(consumer, {
        "target": "#posts-container",
        "html": html,
        "append": True  # Add to the end instead of replacing
    })
#+END_SRC

HTML code:

#+BEGIN_SRC html
<div id="posts-container">
    <!-- Existing posts here -->
</div>

<input type="hidden" name="page" value="2">
<button
    data-liveview-function="load_more_posts"
    data-action="click->page#run">
    Load More
</button>
#+END_SRC

Template (~posts_list.html~):

#+BEGIN_SRC html
{% for post in posts %}
<article class="post">
    <h3>{{ post.title }}</h3>
    <p>{{ post.content }}</p>
</article>
{% endfor %}
#+END_SRC

** Removing Elements from the DOM

Instead of hiding elements with CSS, you can completely remove them from the page.

Python code:

#+BEGIN_SRC python
@liveview_handler("archive_notification")
def archive_notification(consumer, content):
    notification_id = content["data"]["notification_id"]

    # Archive in database
    from .models import Notification
    Notification.objects.filter(id=notification_id).update(archived=True)

    # Remove from page
    send(consumer, {
        "target": f"#notification-{notification_id}",
        "remove": True
    })
#+END_SRC

HTML code:

#+BEGIN_SRC html
<div id="notification-42" class="notification">
    <p>You have a new message</p>
    <button
        data-liveview-function="archive_notification"
        data-data-notification-id="42"
        data-action="click->page#run">
        Dismiss
    </button>
</div>
#+END_SRC

** Updating the URL Without Page Reload

Create SPA-like navigation by updating both content and the browser URL.

Python code:

#+BEGIN_SRC python
@liveview_handler("navigate_to_profile")
def navigate_to_profile(consumer, content):
    user_id = content["data"]["user_id"]

    # Get user data
    from .models import User
    user = User.objects.get(id=user_id)

    # Render profile page
    html = render_to_string("profile_page.html", {
        "user": user
    })

    send(consumer, {
        "target": "#main-content",
        "html": html,
        "url": f"/profile/{user.username}/",  # Update browser URL
        "title": f"{user.name} - Profile"     # Update page title
    })
#+END_SRC

HTML code:

#+BEGIN_SRC html
<div id="main-content">
    <h1>Home Page</h1>
    <button
        data-liveview-function="navigate_to_profile"
        data-data-user-id="123"
        data-action="click->page#run">
        View Profile
    </button>
</div>
#+END_SRC

The browser's back/forward buttons will work correctly, and users can bookmark or share the URL.

** Scrolling to Elements

After updating content, you often want to scroll to a specific element or to the top of the page.

Python code:

#+BEGIN_SRC python
@liveview_handler("show_product_details")
def show_product_details(consumer, content):
    product_id = content["data"]["product_id"]

    from .models import Product
    product = Product.objects.get(id=product_id)

    html = render_to_string("product_details.html", {
        "product": product
    })

    send(consumer, {
        "target": "#product-details",
        "html": html,
        "scroll": "#product-details"  # Smooth scroll to this element
    })

@liveview_handler("search_products")
def search_products(consumer, content):
    query = content["form"].get("q")

    from .models import Product
    products = Product.objects.filter(name__icontains=query)

    html = render_to_string("products_list.html", {
        "products": products
    })

    send(consumer, {
        "target": "#products-list",
        "html": html,
        "scrollTop": True  # Scroll to top of page
    })
#+END_SRC

HTML code:

#+BEGIN_SRC html
<div id="product-details"></div>

<button
    data-liveview-function="show_product_details"
    data-data-product-id="789"
    data-action="click->page#run">
    Show Details
</button>

<input type="text" name="q" placeholder="Search products">
<button
    data-liveview-function="search_products"
    data-action="click->page#run">
    Search
</button>
<div id="products-list"></div>
#+END_SRC

* Frontend Integration
:PROPERTIES:
:ONE: one-custom-default-doc
:CUSTOM_ID: /docs/frontend/
:TITLE: Frontend Integration
:DESCRIPTION: How to call handlers from the frontend.
:NAVIGATOR-ACTIVE: docs
:END:

The frontend is responsible for capturing events and sending messages over WebSocket. No logic, rendering, or state is in the frontend: the backend does all the work.

Django LiveView uses [[https://stimulus.hotwired.dev/][Stimulus]] to manage DOM events and avoid collisions. The JavaScript assets are bundled within the package.

** Calling Handlers

To call a handler from a button click:

#+BEGIN_SRC html
<button
    data-liveview-function="say_hello"
    data-action="click->page#run">
    Say Hello
</button>
#+END_SRC

- ~data-liveview-function~: The handler name registered with ~@liveview_handler~
- ~data-action~: Stimulus action (event->controller#action)

** Sending Form Data

All form fields are automatically extracted and sent in ~content["form"]~:

#+BEGIN_SRC html
<div>
    <input type="text" name="name" placeholder="Enter your name">
    <button
        data-liveview-function="say_hello"
        data-action="click->page#run">
        Submit
    </button>
</div>
#+END_SRC

** Sending Custom Data

Use ~data-data-*~ attributes to send additional data:

#+BEGIN_SRC html
<button
    data-liveview-function="open_modal"
    data-data-modal-id="123"
    data-data-user-id="456"
    data-action="click->page#run">
    Open Modal
</button>
#+END_SRC

Access in Python:

#+BEGIN_SRC python
@liveview_handler("open_modal")
def open_modal(consumer, content):
    data = content.get("data", {})
    modal_id = data.get("modal_id")      # from data-data-modal-id
    user_id = data.get("user_id")        # from data-data-user-id
#+END_SRC

** Stimulus Actions Reference

Available Stimulus actions:

- ~data-action="click->page#run"~: Execute LiveView function on click
- ~data-action="input->page#run"~: Execute on input change (real-time)
- ~data-action="submit->page#run"~: Execute on form submit
- ~data-action="change->page#run"~: Execute on change event
- ~data-action="blur->page#run"~: Execute when element loses focus
- ~data-action="page#stop"~: Stop event propagation

* Forms
:PROPERTIES:
:ONE: one-custom-default-doc
:CUSTOM_ID: /docs/forms/
:TITLE: Forms
:DESCRIPTION: Form handling in Django LiveView.
:NAVIGATOR-ACTIVE: docs
:END:

Django LiveView works seamlessly with Django forms. Form data is sent via WebSocket instead of HTTP.

** Form Handling Example

Python handler:

#+BEGIN_SRC python
@liveview_handler("submit_contact")
def submit_contact(consumer, content):
    from .forms import ContactForm

    form = ContactForm(content["form"])

    if form.is_valid():
        # Save to database
        contact = form.save()

        # Show success message
        html = render_to_string("contact_success.html", {
            "message": "Thank you! We'll be in touch."
        })
    else:
        # Show form with errors
        html = render_to_string("contact_form.html", {
            "form": form
        })

    send(consumer, {
        "target": "#contact-container",
        "html": html
    })
#+END_SRC

HTML template:

#+BEGIN_SRC html
<div id="contact-container">
    <form>
        <input type="text" name="name" placeholder="Name" required>
        <input type="email" name="email" placeholder="Email" required>
        <textarea name="message" placeholder="Message" required></textarea>

        <button
            data-liveview-function="submit_contact"
            data-action="click->page#run"
            type="button">
            Submit
        </button>
    </form>
</div>
#+END_SRC

** Real-time Validation

#+BEGIN_SRC python
@liveview_handler("validate_field")
def validate_field(consumer, content):
    field_name = content["data"]["field"]
    field_value = content["form"].get(field_name, "")

    # Validate
    error = None
    if field_name == "email" and "@" not in field_value:
        error = "Invalid email address"
    elif field_name == "name" and len(field_value) < 3:
        error = "Name must be at least 3 characters"

    # Show error or success
    html = f'<span class="{"error" if error else "success"}">{error or "✓"}</span>'

    send(consumer, {
        "target": f"#error-{field_name}",
        "html": html
    })
#+END_SRC

#+BEGIN_SRC html
<input
    type="text"
    name="email"
    data-liveview-function="validate_field"
    data-data-field="email"
    data-action="blur->page#run">
<span id="error-email"></span>
#+END_SRC

* Broadcasting
:PROPERTIES:
:ONE: one-custom-default-doc
:CUSTOM_ID: /docs/broadcasting/
:TITLE: Broadcasting
:DESCRIPTION: Broadcasting updates to multiple users.
:NAVIGATOR-ACTIVE: docs
:END:

Send updates to all connected clients using the ~broadcast~ parameter:

** Simple Broadcast

#+BEGIN_SRC python
@liveview_handler("notify_all")
def notify_all(consumer, content):
    message = content["form"]["message"]

    html = render_to_string("notification.html", {
        "message": message
    })

    send(consumer, {
        "target": "#notifications",
        "html": html,
        "append": True
    }, broadcast=True)  # Sends to ALL connected users
#+END_SRC

** Background Thread Broadcast with Auto-removal

#+BEGIN_SRC python
from threading import Thread
from time import sleep
from uuid import uuid4

@liveview_handler("send_notification")
def send_notification(consumer, content):
    notification_id = str(uuid4().hex)
    message = "New update available!"

    def broadcast_notification():
        # Send notification
        html = render_to_string("notification.html", {
            "id": notification_id,
            "message": message
        })

        send(consumer, {
            "target": "#notifications",
            "html": html,
            "append": True
        }, broadcast=True)

        # Remove after 5 seconds
        sleep(5)
        send(consumer, {
            "target": f"#notification-{notification_id}",
            "remove": True
        }, broadcast=True)

    Thread(target=broadcast_notification).start()
#+END_SRC

* Advanced Features
:PROPERTIES:
:ONE: one-custom-default-doc
:CUSTOM_ID: /docs/advanced/
:TITLE: Advanced Features
:DESCRIPTION: Advanced features of Django LiveView.
:NAVIGATOR-ACTIVE: docs
:END:

** Intersection Observer (Infinite Scroll)

Trigger functions when elements enter or exit the viewport:

#+BEGIN_SRC python
ITEMS_PER_PAGE = 10

@liveview_handler("load_more")
def load_more(consumer, content):
    page = int(content["data"].get("page", 1))

    # Fetch items
    start = (page - 1) * ITEMS_PER_PAGE
    end = start + ITEMS_PER_PAGE
    items = Item.objects.all()[start:end]
    is_last_page = end >= Item.objects.count()

    # Append items to list
    send(consumer, {
        "target": "#items-list",
        "html": render_to_string("items_partial.html", {
            "items": items
        }),
        "append": True
    })

    # Update or remove intersection observer trigger
    if is_last_page:
        html = ""
    else:
        html = render_to_string("load_trigger.html", {
            "next_page": page + 1
        })

    send(consumer, {
        "target": "#load-more-trigger",
        "html": html
    })
#+END_SRC

HTML template:

#+BEGIN_SRC html
<!-- load_trigger.html -->
<div
    data-liveview-intersect-appear="load_more"
    data-data-page="{{ next_page }}"
    data-liveview-intersect-threshold="200">
    <p>Loading more...</p>
</div>
#+END_SRC

**Attributes:**
- ~data-liveview-intersect-appear="function_name"~: Call when element appears
- ~data-liveview-intersect-disappear="function_name"~: Call when element disappears
- ~data-liveview-intersect-threshold="200"~: Trigger 200px before entering viewport (default: 0)

** Auto-focus

Automatically focus elements after rendering:

#+BEGIN_SRC html
<input
    type="text"
    name="title"
    value="{{ item.title }}"
    data-liveview-focus="true">
#+END_SRC

** Init Functions

Execute functions when elements are first rendered:

#+BEGIN_SRC html
<div
    data-liveview-init="init_counter"
    data-data-counter-id="1"
    data-data-initial-value="0">
    <span id="counter-1-value"></span>
</div>
#+END_SRC

** Debounce

Reduce server calls by adding a delay before sending requests. Perfect for search inputs and real-time validation:

#+BEGIN_SRC html
<input
    type="search"
    name="search"
    data-liveview-function="search_articles"
    data-liveview-debounce="500"
    data-action="input->page#run"
    placeholder="Search articles...">
#+END_SRC

The ~data-liveview-debounce="500"~ attribute waits 500ms after the user stops typing before sending the request. This dramatically reduces server load and provides a better user experience.

**Example: Real-time search with debounce**

#+BEGIN_SRC python
from liveview import liveview_handler, send
from django.template.loader import render_to_string

@liveview_handler("search_articles")
def search_articles(consumer, content):
    query = content["form"]["search"]
    articles = Article.objects.filter(title__icontains=query)

    html = render_to_string("search_results.html", {
        "articles": articles
    })

    send(consumer, {
        "target": "#search-results",
        "html": html
    })
#+END_SRC

Without debounce, typing "python" would send 6 requests (one per letter). With ~data-liveview-debounce="500"~, it sends only 1 request after the user stops typing for 500ms.

** Middleware System

Add middleware to run before handlers for authentication, logging, or rate limiting:

#+BEGIN_SRC python
from liveview import liveview_registry, send

def auth_middleware(consumer, content, function_name):
    """Check if user is authenticated before running handler"""
    user = consumer.scope.get("user")

    if not user or not user.is_authenticated:
        send(consumer, {
            "target": "#error",
            "html": "<p>You must be logged in</p>"
        })
        return False  # Cancel handler execution

    return True  # Continue to handler

def logging_middleware(consumer, content, function_name):
    """Log all handler calls"""
    import logging
    logger = logging.getLogger(__name__)

    user = consumer.scope.get("user")
    logger.info(f"Handler '{function_name}' called by {user}")

    return True  # Continue to handler

# Register middleware
liveview_registry.add_middleware(auth_middleware)
liveview_registry.add_middleware(logging_middleware)
#+END_SRC

** Script Execution

Execute JavaScript code directly from your Python handlers.

⚠️ *Security Warning:* Only execute scripts from trusted sources. Never pass user input directly to the ~script~ parameter without sanitization, as this can lead to XSS (Cross-Site Scripting) vulnerabilities.

*** Basic Script Execution

#+BEGIN_SRC python
from liveview import liveview_handler, send

@liveview_handler("show_notification")
def show_notification(consumer, content):
    message = content["form"]["message"]

    # Execute JavaScript to show a browser notification
    send(consumer, {
        "script": f"""
            if (Notification.permission === 'granted') {{
                new Notification('New Message', {{
                    body: '{message}',
                    icon: '/static/icon.png'
                }});
            }}
        """
    })
#+END_SRC

*** Combining HTML and Script

You can combine HTML updates with script execution:

#+BEGIN_SRC python
@liveview_handler("load_chart")
def load_chart(consumer, content):
    import json
    chart_data = json.dumps(get_chart_data())

    # Update HTML
    html = render_to_string("chart_container.html", {
        "chart_id": "sales-chart"
    })

    send(consumer, {
        "target": "#chart-container",
        "html": html
    })

    # Initialize chart with JavaScript
    send(consumer, {
        "script": f"""
            const ctx = document.getElementById('sales-chart');
            new Chart(ctx, {{
                type: 'bar',
                data: {chart_data}
            }});
        """
    })
#+END_SRC

*** Inline Scripts in HTML

Django LiveView automatically extracts and executes ~<script>~ tags from HTML responses:

#+BEGIN_SRC python
@liveview_handler("load_interactive_component")
def load_interactive_component(consumer, content):
    html = '''
        <div id="counter">
            <button id="increment">Count: <span>0</span></button>
        </div>
        <script>
            let count = 0;
            document.getElementById('increment').addEventListener('click', () => {
                count++;
                document.querySelector('#increment span').textContent = count;
            });
        </script>
    '''

    send(consumer, {
        "target": "#component-container",
        "html": html
    })
#+END_SRC

The script will be automatically extracted and executed after the HTML is rendered.

*** Use Cases

- Integrating third-party JavaScript libraries (charts, maps, etc.)
- Triggering browser APIs (notifications, geolocation, etc.)
- Initializing complex UI components
- Playing sounds or animations
- Focusing specific elements with custom logic

*** Best Practices

1. ✓ Sanitize any user input before including in scripts
2. ✓ Use JSON serialization for data: ~import json; json.dumps(data)~
3. ✓ Prefer ~<script>~ tags in templates over the ~script~ parameter
4. ✓ Keep scripts focused and minimal
5. ✗ Don't use ~eval()~ or similar dangerous functions
6. ✗ Don't pass unsanitized user input to scripts

** File Upload Handling

Handle file uploads with Base64 encoding for WebSocket transmission.

*** Server-Side File Processing

#+BEGIN_SRC python
import base64
from io import BytesIO
from PIL import Image
from django.core.files.base import ContentFile

@liveview_handler("upload_avatar")
def upload_avatar(consumer, content):
    user = consumer.scope.get("user")

    if not user or not user.is_authenticated:
        send(consumer, {
            "target": "#upload-status",
            "html": "<p class='error'>Please log in to upload</p>"
        })
        return

    # Get Base64 data from form
    base64_data = content["form"].get("avatar", "")

    if not base64_data:
        send(consumer, {
            "target": "#upload-status",
            "html": "<p class='error'>No file selected</p>"
        })
        return

    try:
        # Extract format and data
        # Format: "data:image/png;base64,iVBORw0KGgoAAAANS..."
        format_str, img_str = base64_data.split(';base64,')
        ext = format_str.split('/')[-1]

        # Decode Base64
        img_data = base64.b64decode(img_str)

        # Validate it's an image
        image = Image.open(BytesIO(img_data))

        # Resize if needed
        if image.width > 500 or image.height > 500:
            image.thumbnail((500, 500), Image.Resampling.LANCZOS)

            # Save resized image to bytes
            buffer = BytesIO()
            image.save(buffer, format=ext.upper())
            img_data = buffer.getvalue()

        # Save to user profile
        filename = f"avatar_{user.id}.{ext}"
        user.profile.avatar.save(
            filename,
            ContentFile(img_data),
            save=True
        )

        # Show success
        html = f'''
            <p class='success'>Avatar uploaded successfully!</p>
            <img src="{user.profile.avatar.url}" alt="Avatar" width="100">
        '''

        send(consumer, {
            "target": "#upload-status",
            "html": html
        })

    except Exception as e:
        import logging
        logger = logging.getLogger(__name__)
        logger.error(f"Error uploading avatar: {e}")

        send(consumer, {
            "target": "#upload-status",
            "html": "<p class='error'>Upload failed. Please try again.</p>"
        })
#+END_SRC

*** HTML Template

#+BEGIN_SRC html
<div>
    <input type="file" id="avatar-upload" accept="image/*">
    <button
        data-liveview-function="upload_avatar"
        data-action="click->page#run">
        Upload Avatar
    </button>
    <div id="upload-status"></div>
</div>

<script>
    // Encode file as Base64 before sending
    document.querySelector('[data-liveview-function="upload_avatar"]')
        .addEventListener('click', async (e) => {
            const fileInput = document.getElementById('avatar-upload');
            const file = fileInput.files[0];

            if (file) {
                const reader = new FileReader();
                reader.onload = () => {
                    // Store Base64 in hidden input
                    let hiddenInput = document.getElementById('avatar-data');
                    if (!hiddenInput) {
                        hiddenInput = document.createElement('input');
                        hiddenInput.type = 'hidden';
                        hiddenInput.name = 'avatar';
                        hiddenInput.id = 'avatar-data';
                        document.querySelector('div').appendChild(hiddenInput);
                    }
                    hiddenInput.value = reader.result;
                };
                reader.readAsDataURL(file);
            }
        });
</script>
#+END_SRC

*** File Size Limitations

WebSocket has practical limits for Base64-encoded files:

- ✓ *Small files* (< 1MB): Images, documents, avatars
- ⚠️ *Medium files* (1-5MB): May work but can be slow
- ✗ *Large files* (> 5MB): Not recommended, use traditional HTTP upload

For large files, use a traditional HTTP POST to upload, then notify via WebSocket.

*** Security Considerations

1. ✓ Validate file types (check magic bytes, not just extensions)
2. ✓ Limit file sizes on the server
3. ✓ Scan files for malware if accepting from untrusted users
4. ✓ Store files outside the web root
5. ✓ Use unique filenames to prevent overwrites
6. ✓ Validate image dimensions and format with Pillow/PIL

** Message Queue System

Django LiveView automatically queues messages when the WebSocket connection is not ready.

*** How It Works

When you call a LiveView handler but the WebSocket is:
- Still connecting
- Temporarily disconnected
- Reconnecting after a network failure

The message is automatically queued and sent once the connection is restored.

#+BEGIN_SRC html
<button
    data-liveview-function="save_draft"
    data-action="click->page#run">
    Save Draft
</button>
#+END_SRC

If the user clicks "Save Draft" while offline, the message is queued. When the connection is restored, all queued messages are sent automatically in order.

*** User Feedback During Queueing

Show users when their actions are being queued:

#+BEGIN_SRC html
<div id="connection-status"></div>

<script>
    // Monitor connection and queue status
    setInterval(() => {
        const statusEl = document.getElementById('connection-status');
        const ws = window.myWebSocket;

        if (ws && ws.readyState === WebSocket.OPEN) {
            statusEl.innerHTML = '<span class="online">🟢 Connected</span>';
        } else if (ws && ws.readyState === WebSocket.CONNECTING) {
            statusEl.innerHTML = '<span class="connecting">🟡 Connecting...</span>';
        } else {
            statusEl.innerHTML = '<span class="offline">🔴 Disconnected</span>';
        }
    }, 1000);
</script>
#+END_SRC

** Network Connectivity Handling

Django LiveView automatically handles network connectivity changes.

*** Automatic Detection

The framework detects when:
- Network goes offline (airplane mode, WiFi disconnect, etc.)
- Network comes back online
- Connection to the server is lost
- Connection to the server is restored

*** Visual Feedback

Create a connection status modal that appears when connectivity is lost:

#+BEGIN_SRC html
<!-- templates/base.html -->
{% load static %}
{% load liveview %}
<!DOCTYPE html>
<html lang="en" data-room="{% liveview_room_uuid %}">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}My Site{% endblock %}</title>
    <style>
        .no-connection {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            background: #ff6b6b;
            color: white;
            padding: 1rem;
            text-align: center;
            z-index: 9999;
        }

        .no-connection--show {
            display: block;
        }

        .no-connection--hide {
            display: none;
        }
    </style>
</head>
<body data-controller="page">
    <!-- Connection status notification -->
    <div id="no-connection" class="no-connection no-connection--hide">
        ⚠️ Connection lost. Reconnecting...
    </div>

    {% block content %}{% endblock %}

    <script src="{% static 'liveview/liveview.min.js' %}" defer></script>
</body>
</html>
#+END_SRC

The framework automatically shows/hides this modal when connectivity changes.

*** Reconnection Behavior

When the connection is lost, Django LiveView:

1. Shows the ~#no-connection~ modal (if it exists)
2. Queues any new messages
3. Attempts to reconnect automatically
4. Uses exponential backoff between attempts
5. Tries up to 5 times before giving up

Default reconnection settings:

- *Initial delay:* 3 seconds
- *Maximum attempts:* 5
- *Backoff multiplier:* 1.5x
- *Maximum delay:* 30 seconds

Reconnection delays: 3s → 4.5s → 6.75s → 10.12s → 15.18s

** Registry Management

Advanced control over LiveView handler registration.

*** Listing All Handlers

#+BEGIN_SRC python
from liveview import liveview_registry

# Get all registered handler names
handlers = liveview_registry.list_functions()
print(handlers)  # ['say_hello', 'load_articles', 'submit_form', ...]
#+END_SRC

*** Getting a Specific Handler

#+BEGIN_SRC python
# Get handler by name
handler = liveview_registry.get_handler("say_hello")

if handler:
    print(f"Handler found: {handler.__name__}")
else:
    print("Handler not found")
#+END_SRC

*** Unregistering Handlers

Remove a handler from the registry:

#+BEGIN_SRC python
# Unregister a specific handler
liveview_registry.unregister("old_handler")

# Verify it's gone
if liveview_registry.get_handler("old_handler") is None:
    print("Handler successfully unregistered")
#+END_SRC

Use cases:
- Removing deprecated handlers
- Disabling features at runtime
- Testing and cleanup

*** Clearing All Handlers

#+BEGIN_SRC python
# Remove all registered handlers
liveview_registry.clear()

# Verify
print(liveview_registry.list_functions())  # []
#+END_SRC

⚠️ *Warning:* This clears ALL handlers. Only use in testing or when reinitializing the application.

*** Dynamic Handler Registration

Register handlers programmatically without the decorator:

#+BEGIN_SRC python
from liveview import liveview_registry, send

def dynamic_handler(consumer, content):
    send(consumer, {
        "target": "#result",
        "html": "<p>Dynamic handler executed!</p>"
    })

# Register manually
handler_name = "dynamic_action"
decorated_func = liveview_registry.register(handler_name)(dynamic_handler)

# Now callable from frontend
# <button data-liveview-function="dynamic_action" data-action="click->page#run">
#+END_SRC

** WebSocket Configuration

Customize WebSocket connection settings.

*** Custom WebSocket Path

Change the default WebSocket URL path:

#+BEGIN_SRC python
# routing.py
from liveview.routing import get_liveview_path

websocket_urlpatterns = [
    get_liveview_path("custom/path/<str:room_name>/"),
]
#+END_SRC

Update frontend configuration:

#+BEGIN_SRC html
<!-- templates/base.html -->
<script>
    window.webSocketConfig = {
        host: '{{ request.get_host }}',
        protocol: '{% if request.is_secure %}wss{% else %}ws{% endif %}',
        path: '/custom/path/'  // Custom path
    };
</script>
<script src="{% static 'liveview/liveview.min.js' %}" defer></script>
#+END_SRC

*** Custom Host and Protocol

For development or special deployments:

#+BEGIN_SRC html
<script>
    window.webSocketConfig = {
        host: 'api.example.com',  // Different host
        protocol: 'wss'            // Force secure WebSocket
    };
</script>
#+END_SRC

*** Reconnection Configuration

Modify reconnection behavior by editing ~frontend/webSocketsCli.js~ before building:

#+BEGIN_SRC javascript
// frontend/webSocketsCli.js

// Default values:
const RECONNECT_INTERVAL = 3000;        // Initial delay: 3 seconds
const MAX_RECONNECT_ATTEMPTS = 5;       // Maximum attempts: 5
const RECONNECT_BACKOFF_MULTIPLIER = 1.5;  // Exponential multiplier

// Custom values (example):
const RECONNECT_INTERVAL = 5000;        // Initial delay: 5 seconds
const MAX_RECONNECT_ATTEMPTS = 10;      // Maximum attempts: 10
const RECONNECT_BACKOFF_MULTIPLIER = 2.0;  // Double delay each time
#+END_SRC

Then rebuild the JavaScript:

#+BEGIN_SRC sh
cd frontend
npm run build:min
#+END_SRC

* Error Handling
:PROPERTIES:
:ONE: one-custom-default-doc
:CUSTOM_ID: /docs/error-handling/
:TITLE: Error Handling
:DESCRIPTION: Handle errors and exceptions in Django LiveView.
:NAVIGATOR-ACTIVE: docs
:END:

Proper error handling is essential for production applications. Django LiveView provides several patterns for handling errors gracefully.

** Basic Error Handling

Wrap your handler logic in try-except blocks to catch and handle errors:

#+BEGIN_SRC python
from liveview import liveview_handler, send
from django.template.loader import render_to_string

@liveview_handler("load_article")
def load_article(consumer, content):
    article_id = content.get("data", {}).get("article_id")

    try:
        from .models import Article
        article = Article.objects.get(id=article_id)

        html = render_to_string("article_detail.html", {
            "article": article
        })

        send(consumer, {
            "target": "#article-container",
            "html": html
        })
    except Article.DoesNotExist:
        # Show error message to user
        send(consumer, {
            "target": "#article-container",
            "html": "<p class='error'>Article not found</p>"
        })
    except Exception as e:
        # Log unexpected errors
        import logging
        logger = logging.getLogger(__name__)
        logger.error(f"Error loading article {article_id}: {e}")

        # Show generic error to user
        send(consumer, {
            "target": "#article-container",
            "html": "<p class='error'>An error occurred. Please try again.</p>"
        })
#+END_SRC

** Validation Errors

Handle validation errors from Django forms:

#+BEGIN_SRC python
@liveview_handler("submit_form")
def submit_form(consumer, content):
    from .forms import ArticleForm

    form = ArticleForm(content.get("form", {}))

    if form.is_valid():
        article = form.save()

        # Show success message
        html = render_to_string("article_success.html", {
            "article": article
        })

        send(consumer, {
            "target": "#form-container",
            "html": html
        })
    else:
        # Show form with errors
        html = render_to_string("article_form.html", {
            "form": form,
            "errors": form.errors
        })

        send(consumer, {
            "target": "#form-container",
            "html": html
        })
#+END_SRC

Template (~article_form.html~):

#+BEGIN_SRC html
{% if errors %}
<div class="errors">
    {% for field, error_list in errors.items %}
        <p class="error">{{ field }}: {{ error_list.0 }}</p>
    {% endfor %}
</div>
{% endif %}

<form>
    {{ form.as_p }}
    <button
        data-liveview-function="submit_form"
        data-action="click->page#run">
        Submit
    </button>
</form>
#+END_SRC

** Permission and Authentication Errors

Check permissions before executing operations:

#+BEGIN_SRC python
@liveview_handler("delete_article")
def delete_article(consumer, content):
    user = consumer.scope.get("user")
    article_id = content.get("data", {}).get("article_id")

    # Check authentication
    if not user or not user.is_authenticated:
        send(consumer, {
            "target": "#error-message",
            "html": "<p class='error'>You must be logged in to delete articles</p>"
        })
        return

    try:
        from .models import Article
        article = Article.objects.get(id=article_id)

        # Check permissions
        if article.author != user and not user.is_staff:
            send(consumer, {
                "target": "#error-message",
                "html": "<p class='error'>You don't have permission to delete this article</p>"
            })
            return

        article.delete()

        # Show success and remove from DOM
        send(consumer, {
            "target": f"#article-{article_id}",
            "remove": True
        })

        send(consumer, {
            "target": "#success-message",
            "html": "<p class='success'>Article deleted successfully</p>"
        })
    except Article.DoesNotExist:
        send(consumer, {
            "target": "#error-message",
            "html": "<p class='error'>Article not found</p>"
        })
#+END_SRC

** Database Transaction Errors

Use database transactions to ensure data integrity:

#+BEGIN_SRC python
from django.db import transaction

@liveview_handler("create_post_with_tags")
def create_post_with_tags(consumer, content):
    from .models import Post, Tag

    try:
        with transaction.atomic():
            # Create post
            post = Post.objects.create(
                title=content["form"]["title"],
                content=content["form"]["content"]
            )

            # Add tags
            tag_names = content["form"]["tags"].split(",")
            for tag_name in tag_names:
                tag, _ = Tag.objects.get_or_create(name=tag_name.strip())
                post.tags.add(tag)

            # Success
            html = render_to_string("post_created.html", {
                "post": post
            })

            send(consumer, {
                "target": "#post-container",
                "html": html
            })
    except Exception as e:
        # Transaction is automatically rolled back
        import logging
        logger = logging.getLogger(__name__)
        logger.error(f"Error creating post: {e}")

        send(consumer, {
            "target": "#error-message",
            "html": "<p class='error'>Failed to create post. Please try again.</p>"
        })
#+END_SRC

** Timeout Errors

Handle long-running operations with timeouts:

#+BEGIN_SRC python
import asyncio
from threading import Thread

@liveview_handler("process_large_file")
def process_large_file(consumer, content):
    file_id = content.get("data", {}).get("file_id")

    def process_with_timeout():
        try:
            from .models import File
            file_obj = File.objects.get(id=file_id)

            # Simulate long operation
            result = file_obj.process()  # This might take a while

            # Send success
            html = render_to_string("process_success.html", {
                "result": result
            })

            send(consumer, {
                "target": "#result-container",
                "html": html
            }, broadcast=True)
        except Exception as e:
            send(consumer, {
                "target": "#result-container",
                "html": f"<p class='error'>Processing failed: {str(e)}</p>"
            })

    # Start processing in background
    Thread(target=process_with_timeout).start()

    # Show immediate feedback
    send(consumer, {
        "target": "#result-container",
        "html": "<p>Processing file... This may take a moment.</p>"
    })
#+END_SRC

** Global Error Handler with Middleware

Create a middleware to catch all errors:

#+BEGIN_SRC python
from liveview import liveview_registry, send
import logging

logger = logging.getLogger(__name__)

def error_handling_middleware(consumer, content, function_name):
    """Catch all exceptions and show user-friendly error"""
    try:
        # Continue to handler
        return True
    except Exception as e:
        # Log the error
        logger.error(f"Error in handler '{function_name}': {e}", exc_info=True)

        # Show error to user
        send(consumer, {
            "target": "#global-error",
            "html": "<p class='error'>Something went wrong. Our team has been notified.</p>"
        })

        # Don't continue to handler
        return False

# Register middleware
liveview_registry.add_middleware(error_handling_middleware)
#+END_SRC

Note: Middleware runs ~before~ the handler. To catch errors ~during~ handler execution, wrap your handler logic in try-except blocks.

** Logging Best Practices

#+BEGIN_SRC python
import logging

logger = logging.getLogger(__name__)

@liveview_handler("important_operation")
def important_operation(consumer, content):
    user = consumer.scope.get("user")

    # Log the operation attempt
    logger.info(f"User {user.id if user else 'anonymous'} attempting operation")

    try:
        # Perform operation
        result = perform_operation()

        # Log success
        logger.info(f"Operation successful for user {user.id if user else 'anonymous'}")

        send(consumer, {
            "target": "#result",
            "html": f"<p>Success: {result}</p>"
        })
    except ValueError as e:
        # Log validation errors as warnings
        logger.warning(f"Validation error for user {user.id if user else 'anonymous'}: {e}")
        send(consumer, {
            "target": "#result",
            "html": f"<p class='error'>Invalid input: {e}</p>"
        })
    except Exception as e:
        # Log unexpected errors as errors
        logger.error(f"Unexpected error for user {user.id if user else 'anonymous'}: {e}", exc_info=True)
        send(consumer, {
            "target": "#result",
            "html": "<p class='error'>An unexpected error occurred</p>"
        })
#+END_SRC

** Automatic Error Handling

Django LiveView automatically catches and reports handler exceptions.

*** Built-in Error Handling

When a handler raises an unhandled exception, the framework automatically:

1. Logs the error with ~logging.error()~
2. Sends an error message to the client
3. Re-raises the exception for debugging

Example of an error that will be automatically handled:

#+BEGIN_SRC python
@liveview_handler("risky_operation")
def risky_operation(consumer, content):
    # This will raise a ZeroDivisionError
    result = 10 / 0

    send(consumer, {
        "target": "#result",
        "html": f"<p>Result: {result}</p>"
    })
#+END_SRC

The client will automatically receive:

#+BEGIN_SRC json
{
    "error": "Handler error: division by zero",
    "function": "risky_operation"
}
#+END_SRC

*** Handling Errors on the Client

Detect and display errors sent from the server:

#+BEGIN_SRC html
<div id="result"></div>
<div id="error-display" class="error" style="display: none;"></div>

<script>
    // Listen for error messages
    window.myWebSocket.addEventListener('message', (event) => {
        const data = JSON.parse(event.data);

        if (data.error) {
            // Show error to user
            const errorDiv = document.getElementById('error-display');
            errorDiv.textContent = `Error: ${data.error}`;
            errorDiv.style.display = 'block';

            // Hide after 5 seconds
            setTimeout(() => {
                errorDiv.style.display = 'none';
            }, 5000);
        }
    });
</script>
#+END_SRC

*** Custom Error Responses

For better control, handle errors explicitly within your handler:

#+BEGIN_SRC python
@liveview_handler("safe_operation")
def safe_operation(consumer, content):
    try:
        result = perform_calculation()

        send(consumer, {
            "target": "#result",
            "html": f"<p class='success'>Result: {result}</p>"
        })

    except ValueError as e:
        # Handle expected errors gracefully
        send(consumer, {
            "target": "#result",
            "html": f"<p class='error'>Invalid input: {e}</p>"
        })

    except Exception as e:
        # Log and show generic error
        import logging
        logger = logging.getLogger(__name__)
        logger.error(f"Unexpected error: {e}", exc_info=True)

        send(consumer, {
            "target": "#result",
            "html": "<p class='error'>An unexpected error occurred</p>"
        })
        # Don't re-raise - this prevents automatic error handling
#+END_SRC

*** Configuring Error Logging

Configure logging for LiveView errors in ~settings.py~:

#+BEGIN_SRC python
# settings.py
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'ERROR',
            'class': 'logging.FileHandler',
            'filename': '/var/log/django/liveview_errors.log',
        },
        'mail_admins': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler',
        }
    },
    'loggers': {
        'liveview': {
            'handlers': ['file', 'mail_admins'],
            'level': 'ERROR',
            'propagate': False,
        },
    },
}
#+END_SRC

Now all handler errors are logged to a file and emailed to admins.

* Testing
:PROPERTIES:
:ONE: one-custom-default-doc
:CUSTOM_ID: /docs/testing/
:TITLE: Testing
:DESCRIPTION: Testing Django LiveView handlers and applications.
:NAVIGATOR-ACTIVE: docs
:END:

Testing LiveView handlers ensures your real-time features work correctly. Django LiveView handlers are regular Python functions, so you can test them like any other Django code.

** Unit Testing Handlers

Test handlers directly by calling them with mock data:

#+BEGIN_SRC python
from django.test import TestCase
from unittest.mock import Mock, patch
from myapp.liveview_components.articles import load_article

class ArticleHandlerTest(TestCase):
    def setUp(self):
        from myapp.models import Article
        self.article = Article.objects.create(
            title="Test Article",
            content="Test content"
        )

    def test_load_article_success(self):
        # Create mock consumer
        consumer = Mock()

        # Create content dict
        content = {
            "data": {"article_id": str(self.article.id)},
            "form": {},
            "function": "load_article"
        }

        # Patch the send function to capture calls
        with patch("myapp.liveview_components.articles.send") as mock_send:
            # Call handler
            load_article(consumer, content)

            # Verify send was called
            mock_send.assert_called_once()

            # Verify the call arguments
            call_args = mock_send.call_args[0]
            self.assertEqual(call_args[0], consumer)

            # Verify HTML contains article title
            html = call_args[1]["html"]
            self.assertIn("Test Article", html)
            self.assertIn("Test content", html)

    def test_load_article_not_found(self):
        consumer = Mock()
        content = {
            "data": {"article_id": "99999"},
            "form": {}
        }

        with patch("myapp.liveview_components.articles.send") as mock_send:
            load_article(consumer, content)

            # Verify error message is sent
            html = mock_send.call_args[0][1]["html"]
            self.assertIn("not found", html.lower())
#+END_SRC

** Integration Testing with WebSocket Client

Test the full WebSocket flow using Django Channels testing utilities:

#+BEGIN_SRC python
from channels.testing import WebsocketCommunicator
from django.test import TransactionTestCase
from myproject.asgi import application
import json

class LiveViewIntegrationTest(TransactionTestCase):
    async def test_websocket_connection(self):
        # Create WebSocket communicator
        communicator = WebsocketCommunicator(
            application,
            "/ws/liveview/test-room/"
        )

        # Connect
        connected, _ = await communicator.connect()
        self.assertTrue(connected)

        # Send message to handler
        await communicator.send_json_to({
            "function": "say_hello",
            "form": {"name": "Django"},
            "data": {},
            "lang": "en",
            "room": "test-room"
        })

        # Receive response
        response = await communicator.receive_json_from()

        # Verify response
        self.assertEqual(response["target"], "#greeting")
        self.assertIn("Hello, Django", response["html"])

        # Disconnect
        await communicator.disconnect()

    async def test_form_validation(self):
        from myapp.models import Article

        communicator = WebsocketCommunicator(
            application,
            "/ws/liveview/test-room/"
        )

        connected, _ = await communicator.connect()
        self.assertTrue(connected)

        # Send invalid form data
        await communicator.send_json_to({
            "function": "submit_article",
            "form": {"title": ""},  # Empty title should fail
            "data": {},
            "lang": "en",
            "room": "test-room"
        })

        response = await communicator.receive_json_from()

        # Verify error is shown
        self.assertIn("error", response["html"].lower())

        # Verify article was not created
        self.assertEqual(Article.objects.count(), 0)

        await communicator.disconnect()
#+END_SRC

** Testing Broadcasting

Test that handlers broadcast to all connected users:

#+BEGIN_SRC python
from channels.testing import WebsocketCommunicator
from django.test import TransactionTestCase
from myproject.asgi import application

class BroadcastTest(TransactionTestCase):
    async def test_broadcast_to_all_users(self):
        # Connect multiple users
        user1 = WebsocketCommunicator(
            application,
            "/ws/liveview/room-1/"
        )
        user2 = WebsocketCommunicator(
            application,
            "/ws/liveview/room-1/"
        )

        await user1.connect()
        await user2.connect()

        # User 1 sends a broadcast message
        await user1.send_json_to({
            "function": "notify_all",
            "form": {"message": "Hello everyone!"},
            "data": {},
            "lang": "en",
            "room": "room-1"
        })

        # Both users should receive the message
        response1 = await user1.receive_json_from()
        response2 = await user2.receive_json_from()

        self.assertIn("Hello everyone!", response1["html"])
        self.assertIn("Hello everyone!", response2["html"])

        await user1.disconnect()
        await user2.disconnect()
#+END_SRC

** Testing with Authenticated Users

Test handlers that require authentication:

#+BEGIN_SRC python
from channels.testing import WebsocketCommunicator
from django.contrib.auth import get_user_model
from django.test import TransactionTestCase
from channels.db import database_sync_to_async

User = get_user_model()

class AuthenticatedHandlerTest(TransactionTestCase):
    async def test_authenticated_handler(self):
        # Create user
        user = await database_sync_to_async(User.objects.create_user)(
            username="testuser",
            password="testpass123"
        )

        # Create authenticated communicator
        communicator = WebsocketCommunicator(
            application,
            "/ws/liveview/user-room/",
        )
        communicator.scope["user"] = user

        connected, _ = await communicator.connect()
        self.assertTrue(connected)

        # Send request that requires authentication
        await communicator.send_json_to({
            "function": "delete_article",
            "data": {"article_id": "123"},
            "form": {},
            "lang": "en",
            "room": "user-room"
        })

        response = await communicator.receive_json_from()

        # Should succeed (not show "must be logged in" error)
        self.assertNotIn("must be logged in", response["html"].lower())

        await communicator.disconnect()
#+END_SRC

** Testing Middleware

Test that middleware correctly filters requests:

#+BEGIN_SRC python
from django.test import TestCase
from unittest.mock import Mock, patch
from myapp.middleware import auth_middleware

class MiddlewareTest(TestCase):
    def test_auth_middleware_blocks_anonymous(self):
        # Create mock consumer with anonymous user
        consumer = Mock()
        consumer.scope = {"user": None}

        content = {"function": "protected_handler"}

        with patch("myapp.middleware.send") as mock_send:
            # Call middleware
            result = auth_middleware(consumer, content, "protected_handler")

            # Should block the request
            self.assertFalse(result)

            # Should send error message
            mock_send.assert_called_once()
            self.assertIn("logged in", mock_send.call_args[0][1]["html"])

    def test_auth_middleware_allows_authenticated(self):
        from django.contrib.auth import get_user_model
        User = get_user_model()

        # Create mock consumer with authenticated user
        user = User(username="testuser", is_authenticated=True)
        consumer = Mock()
        consumer.scope = {"user": user}

        content = {"function": "protected_handler"}

        # Call middleware
        result = auth_middleware(consumer, content, "protected_handler")

        # Should allow the request
        self.assertTrue(result)
#+END_SRC

** End-to-End Testing with Selenium

Test the full user experience including JavaScript:

#+BEGIN_SRC python
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class E2ETest(StaticLiveServerTestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.driver = webdriver.Chrome()
        cls.driver.implicitly_wait(10)

    @classmethod
    def tearDownClass(cls):
        cls.driver.quit()
        super().tearDownClass()

    def test_real_time_update(self):
        # Open the page
        self.driver.get(f"{self.live_server_url}/")

        # Type in input field
        input_field = self.driver.find_element(By.NAME, "name")
        input_field.send_keys("Django")

        # Click button
        button = self.driver.find_element(
            By.CSS_SELECTOR,
            "[data-liveview-function='say_hello']"
        )
        button.click()

        # Wait for update
        greeting = WebDriverWait(self.driver, 5).until(
            EC.presence_of_element_located((By.ID, "greeting"))
        )

        # Verify content updated
        self.assertIn("Hello, Django", greeting.text)
#+END_SRC

** Testing Best Practices

1. **Test handler logic separately** from WebSocket connection
2. **Mock the ~send~ function** to verify what gets sent to clients
3. **Test error cases** to ensure proper error handling
4. **Test permissions** to prevent unauthorized access
5. **Use ~TransactionTestCase~** for WebSocket tests (not ~TestCase~)
6. **Test broadcasts** to ensure multi-user features work correctly
7. **Write E2E tests** for critical user flows

* Deployment
:PROPERTIES:
:ONE: one-custom-default-doc
:CUSTOM_ID: /docs/deployment/
:TITLE: Deployment
:DESCRIPTION: Deploy Django LiveView applications to production.
:NAVIGATOR-ACTIVE: docs
:END:

Deploying Django LiveView requires an ASGI server (like Daphne or Uvicorn), Redis for the channel layer, and proper WebSocket support.

** Production Requirements

- **ASGI Server**: Daphne, Uvicorn, or Hypercorn
- **Redis**: For channel layer (shared state between workers)
- **WebSocket Support**: Reverse proxy must support WebSockets (Nginx, Caddy, Traefik)
- **Process Manager**: Supervisor, systemd, or Docker
- **SSL/TLS**: Required for ~wss://~ connections in production

** Using Daphne (Recommended)

Install Daphne:

#+BEGIN_SRC sh
pip install daphne
#+END_SRC

Run Daphne in production:

#+BEGIN_SRC sh
daphne -b 0.0.0.0 -p 8000 myproject.asgi:application
#+END_SRC

With multiple workers for better performance:

#+BEGIN_SRC sh
daphne -b 0.0.0.0 -p 8000 --workers 4 myproject.asgi:application
#+END_SRC

** Using Uvicorn

Install Uvicorn:

#+BEGIN_SRC sh
pip install uvicorn[standard]
#+END_SRC

Run Uvicorn:

#+BEGIN_SRC sh
uvicorn myproject.asgi:application --host 0.0.0.0 --port 8000 --workers 4
#+END_SRC

** Redis Configuration for Production

Update ~settings.py~ for production Redis:

#+BEGIN_SRC python
# settings.py
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [
                {
                    "address": ("redis", 6379),  # Redis hostname
                    "password": "your-redis-password",
                    "db": 0,
                }
            ],
            "capacity": 1500,  # Max messages per channel
            "expiry": 10,      # Message expiry in seconds
        },
    },
}
#+END_SRC

For Redis Sentinel (high availability):

#+BEGIN_SRC python
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [
                {
                    "sentinels": [
                        ("sentinel1", 26379),
                        ("sentinel2", 26379),
                        ("sentinel3", 26379),
                    ],
                    "master_name": "mymaster",
                    "password": "your-redis-password",
                }
            ],
        },
    },
}
#+END_SRC

** Nginx Configuration

Configure Nginx to proxy WebSocket connections:

#+BEGIN_SRC nginx
upstream django {
    server 127.0.0.1:8000;
}

server {
    listen 80;
    server_name example.com;

    # Redirect HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # Static files
    location /static/ {
        alias /path/to/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    location /media/ {
        alias /path/to/media/;
        expires 7d;
    }

    # WebSocket support
    location /ws/ {
        proxy_pass http://django;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Increase timeout for long-lived connections
        proxy_read_timeout 86400;
        proxy_send_timeout 86400;
    }

    # Regular HTTP requests
    location / {
        proxy_pass http://django;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
#+END_SRC

** Docker Deployment

~Dockerfile~:

#+BEGIN_SRC dockerfile
FROM python:3.11-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy project
COPY . .

# Collect static files
RUN python manage.py collectstatic --noinput

# Expose port
EXPOSE 8000

# Run Daphne
CMD ["daphne", "-b", "0.0.0.0", "-p", "8000", "myproject.asgi:application"]
#+END_SRC

~docker-compose.yml~:

#+BEGIN_SRC yaml
version: '3.8'

services:
  redis:
    image: redis:7-alpine
    restart: always
    volumes:
      - redis_data:/data
    command: redis-server --requirepass your-redis-password

  web:
    build: .
    restart: always
    ports:
      - "8000:8000"
    depends_on:
      - redis
    environment:
      - REDIS_HOST=redis
      - REDIS_PASSWORD=your-redis-password
      - DJANGO_SETTINGS_MODULE=myproject.settings
    volumes:
      - static_volume:/app/staticfiles
      - media_volume:/app/media

  nginx:
    image: nginx:alpine
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - static_volume:/static:ro
      - media_volume:/media:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - web

volumes:
  redis_data:
  static_volume:
  media_volume:
#+END_SRC

** Systemd Service

Create ~/etc/systemd/system/django-liveview.service~:

#+BEGIN_SRC ini
[Unit]
Description=Django LiveView Application
After=network.target redis.service

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/path/to/project
Environment="PATH=/path/to/venv/bin"
ExecStart=/path/to/venv/bin/daphne -b 0.0.0.0 -p 8000 myproject.asgi:application
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
#+END_SRC

Enable and start:

#+BEGIN_SRC sh
sudo systemctl enable django-liveview
sudo systemctl start django-liveview
sudo systemctl status django-liveview
#+END_SRC

** Environment Variables

Store sensitive configuration in environment variables:

#+BEGIN_SRC python
# settings.py
import os

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")
DEBUG = os.environ.get("DJANGO_DEBUG", "False") == "True"

ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [{
                "address": (
                    os.environ.get("REDIS_HOST", "127.0.0.1"),
                    int(os.environ.get("REDIS_PORT", 6379))
                ),
                "password": os.environ.get("REDIS_PASSWORD"),
            }],
        },
    },
}
#+END_SRC

** Performance Tuning

1. **Use Redis persistence** (~appendonly yes~) for channel layer reliability
2. **Increase worker count** based on CPU cores (~workers = CPU cores * 2 + 1~)
3. **Set proper ~capacity~ and ~expiry~** in channel layer config
4. **Enable HTTP/2** in Nginx for better performance
5. **Use connection pooling** for database
6. **Configure WebSocket timeouts** appropriately
7. **Monitor Redis memory** usage and set ~maxmemory~ limits

** Monitoring

Monitor your Django LiveView application:

#+BEGIN_SRC python
# settings.py
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {
        "file": {
            "level": "INFO",
            "class": "logging.FileHandler",
            "filename": "/var/log/django/liveview.log",
        },
    },
    "loggers": {
        "liveview": {
            "handlers": ["file"],
            "level": "INFO",
            "propagate": False,
        },
    },
}
#+END_SRC

Monitor key metrics:
- WebSocket connection count
- Redis memory usage
- Handler execution time
- Error rates
- Message throughput

** Security Checklist

- ✓ Use HTTPS/WSS in production
- ✓ Set ~ALLOWED_HOSTS~ correctly
- ✓ Use ~AllowedHostsOriginValidator~ in ASGI
- ✓ Enable Redis password authentication
- ✓ Set proper CORS headers if needed
- ✓ Use authentication middleware for protected handlers
- ✓ Validate and sanitize all user input
- ✓ Set WebSocket rate limiting
- ✓ Monitor for suspicious activity

** Scaling Horizontally

To scale beyond one server:

1. **Use external Redis** (not Redis on the same server)
2. **Share sessions** across servers (database or Redis sessions)
3. **Load balance** WebSocket connections (sticky sessions not required)
4. **Use Redis Sentinel or Cluster** for Redis high availability
5. **Monitor all instances** with centralized logging

* API Reference
:PROPERTIES:
:ONE: one-custom-default-doc
:CUSTOM_ID: /docs/api-reference/
:TITLE: API Reference
:DESCRIPTION: Complete API reference for Django LiveView.
:NAVIGATOR-ACTIVE: docs
:END:

Complete reference for all Django LiveView functions, decorators, and classes.

** Decorators

*** ~@liveview_handler(name)~

Register a function as a LiveView handler.

**Parameters:**
- ~name~ (str): The handler name that will be called from the frontend

**Returns:** Decorated function

**Example:**

#+BEGIN_SRC python
from liveview import liveview_handler

@liveview_handler("my_handler")
def my_handler(consumer, content):
    pass
#+END_SRC

** Functions

*** ~send(consumer, data, broadcast=False)~

Send data to the client(s).

**Parameters:**
- ~consumer~ (WebsocketConsumer): The WebSocket consumer instance
- ~data~ (dict): Dictionary containing the update instructions
- ~broadcast~ (bool, optional): If True, send to all connected clients. Default: False

**Data dictionary keys:**
- ~target~ (str, required): CSS selector of element to update
- ~html~ (str, optional): HTML content to insert
- ~append~ (bool, optional): If True, append instead of replace. Default: False
- ~remove~ (bool, optional): If True, remove the target element. Default: False
- ~url~ (str, optional): Update browser URL (uses ~history.pushState~)
- ~title~ (str, optional): Update page title
- ~scroll~ (str, optional): CSS selector to scroll to
- ~scrollTop~ (bool, optional): If True, scroll to top of page
- ~script~ (str, optional): JavaScript code to execute

**Returns:** None

**Examples:**

#+BEGIN_SRC python
from liveview import send

# Replace content
send(consumer, {
    "target": "#container",
    "html": "<p>New content</p>"
})

# Append content
send(consumer, {
    "target": "#list",
    "html": "<li>New item</li>",
    "append": True
})

# Remove element
send(consumer, {
    "target": "#notification-123",
    "remove": True
})

# Update URL and title
send(consumer, {
    "target": "#main",
    "html": "<h1>New Page</h1>",
    "url": "/new-page/",
    "title": "New Page - My Site"
})

# Execute JavaScript
send(consumer, {
    "script": "console.log('Hello from server');"
})

# Broadcast to all users
send(consumer, {
    "target": "#notifications",
    "html": "<p>New message!</p>",
    "append": True
}, broadcast=True)
#+END_SRC

** Handler Content Parameter

Every handler receives a ~content~ dictionary with the following structure:

#+BEGIN_SRC python
{
    "function": str,      # Handler name that was called
    "form": dict,         # All form inputs {name: value}
    "data": dict,         # Custom data-data-* attributes
    "lang": str,          # Current language (from <html lang="">)
    "room": str           # WebSocket room identifier
}
#+END_SRC

**Example access:**

#+BEGIN_SRC python
@liveview_handler("example")
def example(consumer, content):
    function_name = content["function"]           # "example"
    user_input = content["form"]["name"]          # From <input name="name">
    custom_id = content["data"]["item_id"]        # From data-data-item-id
    language = content["lang"]                    # "en", "es", etc.
    room_id = content["room"]                     # "user_123" or UUID
#+END_SRC

** Registry

*** ~liveview_registry~

Global registry for handlers and middleware.

**Methods:**

**** ~add_middleware(middleware_func)~

Add a middleware function to run before handlers.

**Parameters:**
- ~middleware_func~ (callable): Function with signature ~(consumer, content, function_name) -> bool~

**Returns:** None

**Example:**

#+BEGIN_SRC python
from liveview import liveview_registry

def my_middleware(consumer, content, function_name):
    # Run before handler
    print(f"Calling {function_name}")

    # Return True to continue, False to cancel
    return True

liveview_registry.add_middleware(my_middleware)
#+END_SRC

**** ~list_functions()~

Get list of all registered handler names.

**Returns:** List[str]

**Example:**

#+BEGIN_SRC python
from liveview import liveview_registry

handlers = liveview_registry.list_functions()
print(handlers)  # ['say_hello', 'load_articles', ...]
#+END_SRC

**** ~get_handler(function_name)~

Get a specific handler by name.

**Parameters:**
- ~function_name~ (str): Name of the handler to retrieve

**Returns:** Callable or None

**Example:**

#+BEGIN_SRC python
from liveview import liveview_registry

handler = liveview_registry.get_handler("say_hello")

if handler:
    print(f"Handler found: {handler.__name__}")
else:
    print("Handler not found")
#+END_SRC

**** ~get_all_handlers()~

Get a dictionary of all registered handlers with their functions.

**Returns:** Dict[str, Callable]

**Example:**

#+BEGIN_SRC python
from liveview import liveview_registry

all_handlers = liveview_registry.get_all_handlers()

for name, func in all_handlers.items():
    print(f"Handler: {name}, Function: {func.__name__}")
#+END_SRC

**** ~unregister(function_name)~

Remove a handler from the registry.

**Parameters:**
- ~function_name~ (str): Name of the handler to remove

**Returns:** Callable or None (the removed handler, or None if not found)

**Example:**

#+BEGIN_SRC python
from liveview import liveview_registry

# Unregister a handler
liveview_registry.unregister("old_handler")

# Verify it's gone
if liveview_registry.get_handler("old_handler") is None:
    print("Handler successfully unregistered")
#+END_SRC

**** ~clear()~

Remove all registered handlers.

**Returns:** None

**Example:**

#+BEGIN_SRC python
from liveview import liveview_registry

# Remove all handlers
liveview_registry.clear()

# Verify
print(liveview_registry.list_functions())  # []
#+END_SRC

⚠️ *Warning:* This clears ALL handlers. Only use in testing or when reinitializing the application.

** Routing

*** ~get_liveview_urlpatterns()~

Get URL patterns for WebSocket routing.

**Returns:** List of URL patterns

**Example:**

#+BEGIN_SRC python
from liveview.routing import get_liveview_urlpatterns
from channels.routing import URLRouter

application = URLRouter(
    get_liveview_urlpatterns()
)
#+END_SRC

** Frontend Attributes

*** HTML Attributes

**** ~data-controller="page"~

Required on ~<html>~ or ~<body>~ to activate Stimulus controller.

#+BEGIN_SRC html
<html data-controller="page">
#+END_SRC

**** ~data-room="room-id"~

WebSocket room identifier. Use ~{% liveview_room_uuid %}~ to generate a random UUID for each request.

#+BEGIN_SRC html
{% load liveview %}
<html data-room="{% liveview_room_uuid %}">
#+END_SRC

**** ~data-liveview-function="handler-name"~

Specifies which handler to call.

#+BEGIN_SRC html
<button data-liveview-function="say_hello">Click</button>
#+END_SRC

**** ~data-action="event->controller#action"~

Stimulus action binding.

#+BEGIN_SRC html
<button data-action="click->page#run">Click</button>
<input data-action="input->page#run">
<form data-action="submit->page#run">
#+END_SRC

**** ~data-data-*~

Custom data attributes sent to handler in ~content["data"]~.

#+BEGIN_SRC html
<button data-data-user-id="123" data-data-action="delete">
#+END_SRC

Becomes:

#+BEGIN_SRC python
content["data"]["user_id"]  # "123"
content["data"]["action"]   # "delete"
#+END_SRC

**** ~data-liveview-debounce="milliseconds"~

Delay before sending request.

#+BEGIN_SRC html
<input data-liveview-debounce="500" data-action="input->page#run">
#+END_SRC

**** ~data-liveview-focus="true"~

Auto-focus element after rendering.

#+BEGIN_SRC html
<input data-liveview-focus="true">
#+END_SRC

**** ~data-liveview-init="handler-name"~

Call handler when element is first rendered.

#+BEGIN_SRC html
<div data-liveview-init="initialize_chart">
#+END_SRC

**** ~data-liveview-intersect-appear="handler-name"~

Call handler when element appears in viewport.

#+BEGIN_SRC html
<div data-liveview-intersect-appear="load_more">
#+END_SRC

**** ~data-liveview-intersect-disappear="handler-name"~

Call handler when element leaves viewport.

#+BEGIN_SRC html
<div data-liveview-intersect-disappear="pause_video">
#+END_SRC

**** ~data-liveview-intersect-threshold="pixels"~

Trigger intersection before entering viewport.

#+BEGIN_SRC html
<div
    data-liveview-intersect-appear="load_more"
    data-liveview-intersect-threshold="200">
#+END_SRC

** Consumer Scope

The ~consumer~ parameter provides access to connection metadata:

#+BEGIN_SRC python
@liveview_handler("example")
def example(consumer, content):
    # Get authenticated user
    user = consumer.scope.get("user")

    # Get session data
    session = consumer.scope.get("session")

    # Get URL route kwargs
    url_kwargs = consumer.scope.get("url_route", {}).get("kwargs", {})

    # Get headers
    headers = dict(consumer.scope.get("headers", []))
#+END_SRC

* Internationalization
:PROPERTIES:
:ONE: one-custom-default-doc
:CUSTOM_ID: /docs/internationalization/
:TITLE: Internationalization
:DESCRIPTION: Multi-language support in Django LiveView.
:NAVIGATOR-ACTIVE: docs
:END:

Django LiveView automatically passes the current language to handlers:

#+BEGIN_SRC python
from django.utils import translation

@liveview_handler("show_content")
def show_content(consumer, content):
    # Get language from WebSocket message
    lang = content.get("lang", "en")

    # Activate language for this context
    translation.activate(lang)

    try:
        html = render_to_string("content.html", {
            "title": _("Welcome"),
            "message": _("This content is in your language")
        })

        send(consumer, {
            "target": "#content",
            "html": html
        })
    finally:
        # Always deactivate to avoid side effects
        translation.deactivate()
#+END_SRC

The language is automatically detected from the ~<html>~ tag:

#+BEGIN_SRC html
{% load static i18n %}
<!doctype html>{% get_current_language as CURRENT_LANGUAGE %}
<html lang="{{ CURRENT_LANGUAGE }}">
#+END_SRC

* FAQ
:PROPERTIES:
:ONE: one-custom-default-doc
:CUSTOM_ID: /docs/faq/
:TITLE: FAQ
:DESCRIPTION: Frequently asked questions about Django LiveView.
:NAVIGATOR-ACTIVE: docs
:END:

** Do I need to know JavaScript to use Django LiveView?

No, you don't need to. You can create SPAs without using APIs, without JavaScript, and without learning anything new. If you know Python, you know how to use Django LiveView.

** Can I use JavaScript?

Yes, you can. You can use JavaScript to enhance your application, but it's not required for basic functionality.

** Can I use Django's native tools?

Of course. You can still use all of Django's native tools, such as its ORM, forms, authentication, admin, etc.

** Do I need to use React, Vue, Angular or any other frontend framework?

No. All logic, rendering and state is in the backend.

** Can I use Django REST Framework or GraphQL?

Yes, you can use both alongside Django LiveView.

** What's the difference between v0.1.0 and v2.0.0?

v2.0.0 is a complete rewrite with a much simpler API:
- Module name changed from ~django_liveview~ to ~liveview~ for cleaner imports
- Simpler decorator-based API with ~@liveview_handler~
- Built-in auto-discovery of handlers
- JavaScript assets bundled within the package
- More comprehensive documentation

** Who finances the project?

This project is maintained by [[https://andros.dev][Andros Fenollosa]] in his free time. If you want to support the project visit [[https://liberapay.com/androsfenollosa/][Liberapay]].

* Tutorial
:PROPERTIES:
:ONE: one-custom-default-tutorials
:CUSTOM_ID: /quick-start/
:TITLE: Quick start
:DESCRIPTION: Get started with Django LiveView the easy way.
:NAVIGATOR-ACTIVE: tutorial
:END:

Welcome to the quick start. Here you will learn how to create your first real-time SPA using Django LiveView. I assume you have a basic understanding of Django and Python.

** Step 1: Installation

#+BEGIN_SRC sh
pip install django-liveview
#+END_SRC

** Step 2: Configure Django

Add to your ~settings.py~:

#+BEGIN_SRC python
# settings.py
INSTALLED_APPS = [
    "daphne",  # Must be first for ASGI support
    "channels",
    "liveview",
    # ... your other apps
]

# ASGI configuration
ASGI_APPLICATION = "your_project.asgi.application"

# Configure Channels with Redis
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("127.0.0.1", 6379)],
        },
    },
}
#+END_SRC

** Step 3: Setup ASGI routing

Create or update ~asgi.py~:

#+BEGIN_SRC python
# asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator
from liveview.routing import get_liveview_urlpatterns

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "your_project.settings")

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AllowedHostsOriginValidator(
        AuthMiddlewareStack(
            URLRouter(
                get_liveview_urlpatterns()
            )
        )
    ),
})
#+END_SRC

** Step 4: Add JavaScript to your base template

#+BEGIN_SRC html
<!-- templates/base.html -->
{% load static %}
{% load liveview %}
<!DOCTYPE html>
<html lang="en" data-room="{% liveview_room_uuid %}">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}My Site{% endblock %}</title>
</head>
<body data-controller="page">
    {% block content %}{% endblock %}

    <!-- Django LiveView JavaScript -->
    <script src="{% static 'liveview/liveview.min.js' %}" defer></script>
</body>
</html>
#+END_SRC

**Important attributes:**
- ~data-room="{% liveview_room_uuid %}"~ on ~<html>~: unique room ID for each page load
- ~data-controller="page"~ on ~<body>~: activates the Stimulus controller

** Step 5: Create your first LiveView handler

Create ~app/liveview_components/hello.py~:

#+BEGIN_SRC python
# app/liveview_components/hello.py
from liveview import liveview_handler, send
from django.template.loader import render_to_string

@liveview_handler("say_hello")
def say_hello(consumer, content):
    """Handle 'say_hello' function from client"""
    name = content.get("form", {}).get("name", "World")

    html = render_to_string("hello_message.html", {
        "message": f"Hello, {name}!"
    })

    send(consumer, {
        "target": "#greeting",
        "html": html
    })
#+END_SRC

Create the template ~templates/hello_message.html~:

#+BEGIN_SRC html
<h1>{{ message }}</h1>
#+END_SRC

** Step 6: Use it in your page

#+BEGIN_SRC html
<!-- templates/hello_page.html -->
{% extends "base.html" %}

{% block content %}
<div>
    <input type="text" name="name" placeholder="Enter your name">
    <button
        data-liveview-function="say_hello"
        data-action="click->page#run">
        Say Hello
    </button>

    <div id="greeting">
        <h1>Hello, World!</h1>
    </div>
</div>
{% endblock %}
#+END_SRC

** Step 7: Run your project

#+BEGIN_SRC sh
# Run Django with Daphne (ASGI server)
python manage.py runserver
#+END_SRC

That's it! Click the button and see real-time updates. 🎉

Congratulations! You have created your first real-time SPA using Django LiveView.

** Live Demo

Want to see Django LiveView in action? Check out our [[https://django-liveview-demo.andros.dev/][live demo]] showcasing a real-time alert system with WebSocket communication, form validation, and broadcast notifications.

The source code is available at [[https://github.com/Django-LiveView/demo-alarms][GitHub]].

* Source code
:PROPERTIES:
:ONE: one-custom-default-page
:CUSTOM_ID: /source-code/
:TITLE: Source code
:DESCRIPTION: List of all related source code.
:NAVIGATOR-ACTIVE: source code
:END:

You can find all the source code in the following repositories:

- [[https://github.com/Django-LiveView/liveview][LiveView]]: Source code of the Django framework published on PyPI as django-liveview
- [[https://github.com/Django-LiveView/docs][Website and Docs]]: All documentation, including this page
- Templates
    - [[https://github.com/Django-LiveView/starter-template][Starter]]: Check all the features of Django LiveView
    - [[https://github.com/Django-LiveView/minimal-template][Minimal]]: The minimal template to get started
- Demos
    - [[https://github.com/Django-LiveView/demo-alarms][Alert System]]: Real-time alert system with WebSocket communication ([[https://django-liveview-demo.andros.dev/][live demo]])
    - [[https://github.com/Django-LiveView/demo-snake][Snake]]: The classic game of Snake

* Books
:PROPERTIES:
:ONE: one-custom-default-page
:CUSTOM_ID: /books/
:TITLE:
:DESCRIPTION: Books about Django LiveView.
:NAVIGATOR-ACTIVE: books
:END:

There are no books specifically about Django LiveView yet, but you can find books about Django working with HTML over the Wire technology.

** Building SPAs with Django and HTML Over the Wire

#+ATTR_HTML: :class center-block image image--cover-book
[[#/img/books/building-spas.avif][Building SPAs with Django and HTML Over the Wire]]

Learn to build real-time single page applications with Python.

Buy:

- [[https://www.packtpub.com/product/building-spas-with-django-and-html-over-the-wire/9781803240190][Packt]]
- [[https://www.amazon.com/Real-time-Django-over-Wire-Channels-ebook/dp/B0B3DV54ZT/][Amazon.com]]
- [[https://www.amazon.es/Real-time-Django-over-Wire-Channels-ebook/dp/B0B3DV54ZT/][Amazon.es]]
