Advanced Features | Django LiveView

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 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:

<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

  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

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

  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.

<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:

  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

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