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)
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
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'
}});
}}
"""
})
Combining HTML and Script
You can combine HTML updates with script execution:
@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}
}});
"""
})
Inline Scripts in HTML
Django LiveView automatically extracts and executes <script> tags from HTML responses:
@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
})
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
✓ Sanitize any user input before including in scripts
✓ Use JSON serialization for data:
import json; json.dumps(data)✓ Prefer
<script>tags in templates over thescriptparameter✓ Keep scripts focused and minimal
✗ Don't use
eval()or similar dangerous functions✗ Don't pass unsanitized user input to scripts
File Upload Handling
Handle file uploads with Base64 encoding for WebSocket transmission.
Server-Side File Processing
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>"
})
HTML Template
<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>
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
✓ Validate file types (check magic bytes, not just extensions)
✓ Limit file sizes on the server
✓ Scan files for malware if accepting from untrusted users
✓ Store files outside the web root
✓ Use unique filenames to prevent overwrites
✓ 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.
<button
data-liveview-function="save_draft"
data-action="click->page#run">
Save Draft
</button>
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:
<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>
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:
<!-- 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>
The framework automatically shows/hides this modal when connectivity changes.
Reconnection Behavior
When the connection is lost, Django LiveView:
Shows the
#no-connectionmodal (if it exists)Queues any new messages
Attempts to reconnect automatically
Uses exponential backoff between attempts
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
from liveview import liveview_registry
# Get all registered handler names
handlers = liveview_registry.list_functions()
print(handlers) # ['say_hello', 'load_articles', 'submit_form', ...]
Getting a Specific Handler
# Get handler by name
handler = liveview_registry.get_handler("say_hello")
if handler:
print(f"Handler found: {handler.__name__}")
else:
print("Handler not found")
Unregistering Handlers
Remove a handler from the registry:
# 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")
Use cases:
Removing deprecated handlers
Disabling features at runtime
Testing and cleanup
Clearing All Handlers
# Remove all registered handlers
liveview_registry.clear()
# Verify
print(liveview_registry.list_functions()) # []
⚠️ Warning: This clears ALL handlers. Only use in testing or when reinitializing the application.
Dynamic Handler Registration
Register handlers programmatically without the decorator:
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">
WebSocket Configuration
Customize WebSocket connection settings.
Custom WebSocket Path
Change the default WebSocket URL path:
# routing.py
from liveview.routing import get_liveview_path
websocket_urlpatterns = [
get_liveview_path("custom/path/<str:room_name>/"),
]
Update frontend configuration:
<!-- 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>
Custom Host and Protocol
For development or special deployments:
<script>
window.webSocketConfig = {
host: 'api.example.com', // Different host
protocol: 'wss' // Force secure WebSocket
};
</script>
Reconnection Configuration
Modify reconnection behavior by editing frontend/webSocketsCli.js before building:
// 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
Then rebuild the JavaScript:
cd frontend
npm run build:min