- Enhanced grammar and punctuation throughout documentation - Added security warning about room ID enumeration attacks - Introduced liveview_room_uuid template tag for secure random room IDs - Clarified attribute naming conversion (camelCase to snake_case) - Improved formatting with colons instead of em dashes for better readability - Added comprehensive error handling, testing, and deployment sections - Removed draft.org file (no longer needed) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
68 KiB
- Home
- Install
- Handlers
- Frontend Integration
- Forms
- Broadcasting
- Advanced Features
- Error Handling
- Testing
- Deployment
- API Reference
- Decorators
- Functions
- Handler Content Parameter
- Registry
- Routing
- Frontend Attributes
- HTML Attributes
data-controller="page"data-room="room-id"data-liveview-function="handler-name"data-action="event->controller#action"data-data-*data-liveview-debounce="milliseconds"data-liveview-focus="true"data-liveview-init="handler-name"data-liveview-intersect-appear="handler-name"data-liveview-intersect-disappear="handler-name"data-liveview-intersect-threshold="pixels"
- HTML Attributes
- Consumer Scope
- Internationalization
- FAQ
- Tutorial
- Source code
- Books
Home
What is HTML over the Wire?
HTML over the Wire, or HTML over WebSockets, is a strategy for creating real-time SPAs by establishing a WebSocket connection between a client and server. It allows JavaScript to request actions (its only responsibility is to handle events) while the backend handles the business logic and renders HTML. This means you can create dynamic pages without reloading, without AJAX or APIs. This technology provides a secure, stable, and low-latency connection for real-time web applications.
What is Django LiveView? 🚀
Django LiveView is a framework for creating real-time, interactive web applications entirely in Python 🐍, inspired by Phoenix LiveView and 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.
Let's illustrate with an example. I want to print article number 2.
- A WebSocket connection (a channel) is established between the client and the server.
- JavaScript sends a message via WebSocket to the server (Django).
- Django interprets the message and renders the HTML of the article through the template system and the database.
- Django sends the HTML to JavaScript via the channel and specifies which selector to embed it in.
- JavaScript renders the received HTML in the indicated selector.
The same process is repeated for each action, such as clicking a button, submitting a form, etc.
What are your superpowers? 💪
- 🎯 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
Are you ready to create your first real-time SPA? Let's go to the Quick start.
Install
Requirements
- Python 3.10+
- Django 4.2+
- Redis (for Channels layer)
- Channels 4.0+
Installation
Install Django LiveView with pip:
pip install django-liveview
Configure Django
Add to your settings.py:
# 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)],
},
},
}
Setup ASGI routing
Create or update asgi.py:
# 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()
)
)
),
})
Add JavaScript to your base template
<!-- 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>
Important attributes:
data-room="{% liveview_room_uuid %}"on<html>: unique room ID for each page loaddata-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)
<html lang="en" data-room="{% if request.user.is_authenticated %}user-{{ request.user.id }}{% else %}{% liveview_room_uuid %}{% endif %}">
⚠️ 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 to see the installation in action.
Handlers
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:
from liveview import liveview_handler, send
@liveview_handler("say_hello")
def say_hello(consumer, content):
send(consumer, {
"target": "#greeting",
"html": "<p>Hello, World!</p>"
})
HTML code:
<div>
<div id="greeting"></div>
<button
data-liveview-function="say_hello"
data-action="click->page#run">
Say Hello
</button>
</div>
When you click the button:
- The frontend sends a WebSocket message with
function: "say_hello" - Django LiveView calls the
say_hellohandler - The handler sends back HTML to replace the content of
#greeting - 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:
@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>"
})
HTML code:
<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>
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:
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
})
HTML code:
<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>
Template (profile_card.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>
Understanding the Content Parameter
Every handler receives two parameters: consumer and content. The content dictionary contains all the information from the client.
@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
Auto-discovery
Django LiveView automatically discovers handlers in liveview_components/ directories within your installed apps:
my_app/
├── liveview_components/
│ ├── __init__.py
│ ├── users.py
│ ├── posts.py
│ └── comments.py
Handlers are loaded on startup with this output:
✓ Imported: my_app.liveview_components.users
✓ Imported: my_app.liveview_components.posts
✓ Imported: my_app.liveview_components.comments
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:
@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
})
HTML code:
<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>
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:
@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
})
HTML code:
<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>
Template (posts_list.html):
{% for post in posts %}
<article class="post">
<h3>{{ post.title }}</h3>
<p>{{ post.content }}</p>
</article>
{% endfor %}
Removing Elements from the DOM
Instead of hiding elements with CSS, you can completely remove them from the page.
Python code:
@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
})
HTML code:
<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>
Updating the URL Without Page Reload
Create SPA-like navigation by updating both content and the browser URL.
Python code:
@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
})
HTML code:
<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>
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:
@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
})
HTML code:
<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>
Frontend Integration
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 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:
<button
data-liveview-function="say_hello"
data-action="click->page#run">
Say Hello
</button>
data-liveview-function: The handler name registered with@liveview_handlerdata-action: Stimulus action (event->controller#action)
Sending Form Data
All form fields are automatically extracted and sent in content["form"]:
<div>
<input type="text" name="name" placeholder="Enter your name">
<button
data-liveview-function="say_hello"
data-action="click->page#run">
Submit
</button>
</div>
Sending Custom Data
Use data-data-* attributes to send additional data:
<button
data-liveview-function="open_modal"
data-data-modal-id="123"
data-data-user-id="456"
data-action="click->page#run">
Open Modal
</button>
Access in 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
Stimulus Actions Reference
Available Stimulus actions:
data-action="click->page#run": Execute LiveView function on clickdata-action="input->page#run": Execute on input change (real-time)data-action="submit->page#run": Execute on form submitdata-action="change->page#run": Execute on change eventdata-action="blur->page#run": Execute when element loses focusdata-action="page#stop": Stop event propagation
Forms
Django LiveView works seamlessly with Django forms. Form data is sent via WebSocket instead of HTTP.
Form Handling Example
Python handler:
@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
})
HTML template:
<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>
Real-time Validation
@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
})
<input
type="text"
name="email"
data-liveview-function="validate_field"
data-data-field="email"
data-action="blur->page#run">
<span id="error-email"></span>
Broadcasting
Send updates to all connected clients using the broadcast parameter:
Simple Broadcast
@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
Background Thread Broadcast with Auto-removal
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()
Advanced Features
Intersection Observer (Infinite Scroll)
Trigger functions when elements enter or exit the viewport:
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
})
HTML template:
<!-- 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>
Attributes:
data-liveview-intersect-appear="function_name": Call when element appearsdata-liveview-intersect-disappear="function_name": Call when element disappearsdata-liveview-intersect-threshold="200": Trigger 200px before entering viewport (default: 0)
Auto-focus
Automatically focus elements after rendering:
<input
type="text"
name="title"
value="{{ item.title }}"
data-liveview-focus="true">
Init Functions
Execute functions when elements are first rendered:
<div
data-liveview-init="init_counter"
data-data-counter-id="1"
data-data-initial-value="0">
<span id="counter-1-value"></span>
</div>
Debounce
Reduce server calls by adding a delay before sending requests. Perfect for search inputs and real-time validation:
<input
type="search"
name="search"
data-liveview-function="search_articles"
data-liveview-debounce="500"
data-action="input->page#run"
placeholder="Search articles...">
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
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
})
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:
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)
Error Handling
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:
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>"
})
Validation Errors
Handle validation errors from Django forms:
@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
})
Template (article_form.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>
Permission and Authentication Errors
Check permissions before executing operations:
@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>"
})
Database Transaction Errors
Use database transactions to ensure data integrity:
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>"
})
Timeout Errors
Handle long-running operations with timeouts:
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>"
})
Global Error Handler with Middleware
Create a middleware to catch all errors:
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)
Note: Middleware runs before the handler. To catch errors during handler execution, wrap your handler logic in try-except blocks.
Logging Best Practices
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>"
})
Testing
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:
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())
Integration Testing with WebSocket Client
Test the full WebSocket flow using Django Channels testing utilities:
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()
Testing Broadcasting
Test that handlers broadcast to all connected users:
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()
Testing with Authenticated Users
Test handlers that require authentication:
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()
Testing Middleware
Test that middleware correctly filters requests:
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-to-End Testing with Selenium
Test the full user experience including JavaScript:
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)
Testing Best Practices
- Test handler logic separately from WebSocket connection
- Mock the
sendfunction to verify what gets sent to clients - Test error cases to ensure proper error handling
- Test permissions to prevent unauthorized access
- Use
TransactionTestCasefor WebSocket tests (notTestCase) - Test broadcasts to ensure multi-user features work correctly
- Write E2E tests for critical user flows
Deployment
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:
pip install daphne
Run Daphne in production:
daphne -b 0.0.0.0 -p 8000 myproject.asgi:application
With multiple workers for better performance:
daphne -b 0.0.0.0 -p 8000 --workers 4 myproject.asgi:application
Using Uvicorn
Install Uvicorn:
pip install uvicorn[standard]
Run Uvicorn:
uvicorn myproject.asgi:application --host 0.0.0.0 --port 8000 --workers 4
Redis Configuration for Production
Update settings.py for production Redis:
# 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
},
},
}
For Redis Sentinel (high availability):
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [
{
"sentinels": [
("sentinel1", 26379),
("sentinel2", 26379),
("sentinel3", 26379),
],
"master_name": "mymaster",
"password": "your-redis-password",
}
],
},
},
}
Nginx Configuration
Configure Nginx to proxy WebSocket connections:
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;
}
}
Docker Deployment
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"]
docker-compose.yml:
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:
Systemd Service
Create /etc/systemd/system/django-liveview.service:
[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
Enable and start:
sudo systemctl enable django-liveview
sudo systemctl start django-liveview
sudo systemctl status django-liveview
Environment Variables
Store sensitive configuration in environment variables:
# 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"),
}],
},
},
}
Performance Tuning
- Use Redis persistence (
appendonly yes) for channel layer reliability - Increase worker count based on CPU cores (
workers = CPU cores * 2 + 1) - Set proper
capacityandexpiryin channel layer config - Enable HTTP/2 in Nginx for better performance
- Use connection pooling for database
- Configure WebSocket timeouts appropriately
- Monitor Redis memory usage and set
maxmemorylimits
Monitoring
Monitor your Django LiveView application:
# 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,
},
},
}
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_HOSTScorrectly - ✓ Use
AllowedHostsOriginValidatorin 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:
- Use external Redis (not Redis on the same server)
- Share sessions across servers (database or Redis sessions)
- Load balance WebSocket connections (sticky sessions not required)
- Use Redis Sentinel or Cluster for Redis high availability
- Monitor all instances with centralized logging
API Reference
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:
from liveview import liveview_handler
@liveview_handler("my_handler")
def my_handler(consumer, content):
pass
Functions
send(consumer, data, broadcast=False)
Send data to the client(s).
Parameters:
consumer(WebsocketConsumer): The WebSocket consumer instancedata(dict): Dictionary containing the update instructionsbroadcast(bool, optional): If True, send to all connected clients. Default: False
Data dictionary keys:
target(str, required): CSS selector of element to updatehtml(str, optional): HTML content to insertappend(bool, optional): If True, append instead of replace. Default: Falseremove(bool, optional): If True, remove the target element. Default: Falseurl(str, optional): Update browser URL (useshistory.pushState)title(str, optional): Update page titlescroll(str, optional): CSS selector to scroll toscrollTop(bool, optional): If True, scroll to top of pagescript(str, optional): JavaScript code to execute
Returns: None
Examples:
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)
Handler Content Parameter
Every handler receives a content dictionary with the following structure:
{
"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
}
Example access:
@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
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:
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)
list_functions()
Get list of all registered handler names.
Returns: List[str]
Example:
from liveview import liveview_registry
handlers = liveview_registry.list_functions()
print(handlers) # ['say_hello', 'load_articles', ...]
Routing
get_liveview_urlpatterns()
Get URL patterns for WebSocket routing.
Returns: List of URL patterns
Example:
from liveview.routing import get_liveview_urlpatterns
from channels.routing import URLRouter
application = URLRouter(
get_liveview_urlpatterns()
)
Frontend Attributes
HTML Attributes
data-controller="page"
Required on <html> or <body> to activate Stimulus controller.
<html data-controller="page">
data-room="room-id"
WebSocket room identifier. Use {% liveview_room_uuid %} to generate a random UUID for each request.
{% load liveview %}
<html data-room="{% liveview_room_uuid %}">
data-liveview-function="handler-name"
Specifies which handler to call.
<button data-liveview-function="say_hello">Click</button>
data-action="event->controller#action"
Stimulus action binding.
<button data-action="click->page#run">Click</button>
<input data-action="input->page#run">
<form data-action="submit->page#run">
data-data-*
Custom data attributes sent to handler in content["data"].
<button data-data-user-id="123" data-data-action="delete">
Becomes:
content["data"]["user_id"] # "123"
content["data"]["action"] # "delete"
data-liveview-debounce="milliseconds"
Delay before sending request.
<input data-liveview-debounce="500" data-action="input->page#run">
data-liveview-focus="true"
Auto-focus element after rendering.
<input data-liveview-focus="true">
data-liveview-init="handler-name"
Call handler when element is first rendered.
<div data-liveview-init="initialize_chart">
data-liveview-intersect-appear="handler-name"
Call handler when element appears in viewport.
<div data-liveview-intersect-appear="load_more">
data-liveview-intersect-disappear="handler-name"
Call handler when element leaves viewport.
<div data-liveview-intersect-disappear="pause_video">
data-liveview-intersect-threshold="pixels"
Trigger intersection before entering viewport.
<div
data-liveview-intersect-appear="load_more"
data-liveview-intersect-threshold="200">
Consumer Scope
The consumer parameter provides access to connection metadata:
@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", []))
Internationalization
Django LiveView automatically passes the current language to handlers:
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()
The language is automatically detected from the <html> tag:
{% load static i18n %}
<!doctype html>{% get_current_language as CURRENT_LANGUAGE %}
<html lang="{{ CURRENT_LANGUAGE }}">
FAQ
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_liveviewtoliveviewfor 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 Andros Fenollosa in his free time. If you want to support the project visit Liberapay.
Tutorial
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
pip install django-liveview
Step 2: Configure Django
Add to your settings.py:
# 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)],
},
},
}
Step 3: Setup ASGI routing
Create or update asgi.py:
# 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()
)
)
),
})
Step 4: Add JavaScript to your base template
<!-- 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>
Important attributes:
data-room="{% liveview_room_uuid %}"on<html>: unique room ID for each page loaddata-controller="page"on<body>: activates the Stimulus controller
Step 5: Create your first LiveView handler
Create app/liveview_components/hello.py:
# 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
})
Create the template templates/hello_message.html:
<h1>{{ message }}</h1>
Step 6: Use it in your page
<!-- 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 %}
Step 7: Run your project
# Run Django with Daphne (ASGI server)
python manage.py runserver
That's it! Click the button and see real-time updates. 🎉
Congratulations! You have created your first real-time SPA using Django LiveView.
Source code
You can find all the source code in the following repositories:
- LiveView: Source code of the Django framework published on PyPI as django-liveview
- Website and Docs: All documentation, including this page
-
Templates
-
Demos
- Snake: The classic game of Snake
Books
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
Building SPAs with Django and HTML Over the Wire
Learn to build real-time single page applications with Python.
Buy: