Files
docs/one.org
Andros Fenollosa 774d39438a
Some checks failed
Gitea Actions Deploy / deploy (push) Has been cancelled
Improve handlers documentation with FastAPI-style examples
- Add progressive examples from simple to complex
- Include both Python and HTML code for each example
- Add template examples for complex use cases
- Fix data-data-* attributes to use snake_case (not camelCase)
- Add detailed explanations for each feature
- Improve readability and learning curve

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-06 12:20:53 +01:00

34 KiB

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.

Architecture send

Architecture receive

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.

  1. A WebSocket connection (a channel) is established between the client and the server.
  2. JavaScript sends a message via WebSocket to the server (Django).

Send string

  1. Django interprets the message and renders the HTML of the article through the template system and the database.
  2. Django sends the HTML to JavaScript via the channel and specifies which selector to embed it in.

Send JSON

  1. JavaScript renders the received HTML in the indicated selector.

Place HTML

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 %}
<!DOCTYPE html>
<html lang="en" data-room="{% if request.user.is_authenticated %}{{ request.user.id }}{% else %}anonymous{% endif %}">
<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 on <html> — unique identifier for WebSocket room (user-specific or shared)
  • data-controller="page" on <body> — activates the Stimulus controller

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:

  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:

@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"].

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_handler
  • data-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("modalId")      # from modal-id
    user_id = data.get("userId")        # from user-id

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

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

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_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 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 %}
<!DOCTYPE html>
<html lang="en" data-room="{% if request.user.is_authenticated %}{{ request.user.id }}{% else %}anonymous{% endif %}">
<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>

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

    • Starter: Check all the features of Django LiveView
    • Minimal: The minimal template to get started
  • 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: