docs/one.org
Andros Fenollosa e3fd6afcd7 Update
2024-03-09 11:41:54 +01:00

23 KiB

Home

What is HTML over the Wire?

HTML over ther Wire, or HTML over the WebSockets, is a strategy for creating real-time SPAs by creating a WebSockets connection between a client and a server. It allows JavaScript to request actions, its only responsibility is to handle events, and the backend handles the business logic as well as rendering HTML. This means you can create pages without reloading the page, without AJAX, APIs or requests. One technology provides a secure, stable and low-delay connection for real-time web applications.

Architecture send

Architecture receive

What is Django LiveView?

Django LiveView is a framework for creating Realtime SPAs using HTML over the Wire technology. It is inspired by Phoenix LiveView and it is built on top of Django Channels.

It allows you to create interactive web applications using only HTML, CSS and Python. JavaScript ONLY is used to capture events, send and receive strings over a WebSockets channel.

Let's illustrate with an example. I want to render article number 2.

Send string

Send JSON

  1. A WebSockets connection, a channel, is established between the client and the server.
  2. JavaScript sends a text, not a request, via WebSockets to the Server (in our case Django).
  3. Django interprets the text and renders the HTML of the article through the template system and the database.
  4. Django sends the HTML to JavaScript via the channel and tells it which selector to embed it in.
  5. JavaScript draws 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.
  • Uses Django's template system to render the frontend (Without JavaScript).
  • The logic is not split between the backend and the frontend, it all stays in Python.
  • You can still use all of Django's native tools, such as its ORM, forms, plugins, etc.
  • Everything is asynchronous by default.
  • Don't learn anything new. If you know Python, you know how to use Django LiveView.
  • All in real time.

Now 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.

Are you ready to create your first Realtime SPA? Let's go to the Quickstart.

Quickstart

Welcome to the Quickstart guide. Here you will learn how to create your first Realtime SPA using Django LiveView. I assume you have a basic understanding of Django and Python.

All the steps are applied in a minimalist template.

1. Install Django

Install Django, create a project and an app.

2. Install LiveView

Install django-liveview with pip.

pip install django-liveview

3. Modify the configuration

Add liveview to your installed INSTALLED_APPS.

INSTALLED_APPS = [
    "daphne",
    "channels",
    "liveview",
]

Then indicate in which previously created App you want to implement LiveView.

LIVEVIEW_APPS = ["website"]

4. Migration

Execute the migrations so that the LiveView tables are generated.

python manage.py migrate

5. ASGI

Modify the ASGI file, asgi.py to add the LiveView routing. In this example it is assumed that settings.py is inside core, in your case it may be different.

import os
import django

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
django.setup()

from channels.auth import AuthMiddlewareStack
from django.core.asgi import get_asgi_application
from channels.security.websocket import AllowedHostsOriginValidator
from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import re_path
from liveview.consumers import LiveViewConsumer


application = ProtocolTypeRouter(
    {
        # Django's ASGI application to handle traditional HTTP requests
        "http": get_asgi_application(),
        # WebSocket handler
        "websocket": AuthMiddlewareStack(
            AllowedHostsOriginValidator(
                URLRouter([re_path(r"^ws/liveview/$", LiveViewConsumer.as_asgi())])
            )
        ),
    }
)

6. Create your first Action

Place where the functions and logic of the business logic are stored. We will start by creating an action to generate a random number and print it.

Create inside your App a folder called actions, here will go all the actions for each page. Now we will create inside the folder a file named home.py.

# my-app/actions/home.py
from liveview.context_processors import get_global_context
from core import settings
from liveview.utils import (
    get_html,
    update_active_nav,
    enable_lang,
    loading,
)
from channels.db import database_sync_to_async
from django.templatetags.static import static
from django.urls import reverse
from django.utils.translation import gettext as _
from random import randint

template = "pages/home.html"

# Database

# Functions

async def get_context(consumer=None):
    context = get_global_context(consumer=consumer)
    # Update context
    context.update(
        {
            "url": settings.DOMAIN_URL + reverse("home"),
            "title": _("Home") + " | Home",
            "meta": {
                "description": _("Home page of the website"),
                "image": f"{settings.DOMAIN_URL}{static('img/seo/og-image.jpg')}",
            },
            "active_nav": "home",
            "page": template,
        }
    )
    return context


@enable_lang
@loading
async def send_page(consumer, client_data, lang=None):
    # Nav
    await update_active_nav(consumer, "home")
    # Main
    my_context = await get_context(consumer=consumer)
    html = await get_html(template, my_context)
    data = {
        "action": client_data["action"],
        "selector": "#main",
        "html": html,
    }
    data.update(my_context)
    await consumer.send_html(data)

async def random_number(consumer, client_data, lang=None):
    my_context = await get_context(consumer=consumer)
    data = {
        "action": client_data["action"],
        "selector": "#output-random-number",
        "html": randint(0, 10),
    }
    data.update(my_context)
    await consumer.send_html(data)

There are several points in the above code to keep in mind.

  • template is the name of the template that will be rendered.
  • get_context() is a function that returns a dictionary with the context of the page.
  • send_page() is the function that will be executed when the page is loaded.
  • random_number() is the function that will be executed when the button is clicked.

7. Create the base template

Now we will create the base template, which will be the one that will be rendered when the page is loaded.

Create a folder called templates, or use your template folder, inside your App and inside it create another folder called layouts. Now create a file called base.html.

{# my-app/templates/layouts/base.html #}
{% load static i18n %}
<!doctype html>{% get_current_language as CURRENT_LANGUAGE %}
<html lang="{{ CURRENT_LANGUAGE }}">
    <head>
        <meta charset="utf-8">
        <title>{{ title }}</title>
        <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0, shrink-to-fit=no"
        >
        <meta
            name="description"
            content="{{ meta.description }}"
        >
        <meta
            property="og:image"
            content="{{ meta.image }}"
        >
	<script type="module" src="{% static 'js/main.js' %}" defer></script>
    </head>
    <body
		data-host="{{ request.get_host }}"
		data-debug="{{ DEBUG }}"
	>
            <section id="loading"></section>
	    <section id="notifications" class="notifications"></section>
	    <section id="no_connection"></section>
	    <div class="container">
		<header id="content-header">
		    {% include 'components/header.html' %}
		</header>
		<main id="main" class="main-container">{% include page %}</main>
		<footer id="content-footer">
		    {% include 'components/footer.html' %}
		</footer>
	    </div>
    </body>
</html>

In the future we will define main.js, a minimal JavaScript to connect the events and the WebSockets client.

8. Create the page template

We will create the home page template, which will be the one that will be rendered when the page is loaded.

Create a folder called pages in your template folder and inside it create a file called home.html.

{# my-app/templates/pages/home.html #}
{% load static %}

<main data-controller="home">
    <p>
	<button data-action="click->home#randomNumber">Random number</button>
    </p>
    <h2 id="output-random-number"></h2>
</main>

As you can see, we have defined a button to launch the action of generating the random number (button) and the place where we will print the result (output-random-number).

9. Create frontend

Now we are going to create the frontend, the part where we will manage the JavaScript events and invoke the actions.

Download assets and unzip it in your static folder. You will be left with the following route: /static/js/.

10. Create View

We will create the view that will render the page for the first time (like Server Side Rendering). The rest of the times will be rendered dynamically (like Single Page Application).

In a normal Django application we would create a view, views.py, similar to the following:

# my-app/views.py
from django.shortcuts import render

# Create your views here.
def home(request):
    return render(request, "pages/home.html")

With LiveView, on the other hand, you will have the following structure.

# my-app/views.py
from django.shortcuts import render
from .actions.home import get_context as get_home_context

from liveview.utils import get_html

async def home(request):
    return render(request, "layouts/base.html", await get_home_context())

11. Create URL

Finally, we will create the URL that will render the page.

# my-app/urls.py
from django.urls import path

from .views import home

urlpatterns = [
    path("", home, name="home"),
]

12. Run the server

Run the server.

python manage.py runserver

And open the browser at http://localhost:8000/. You should see the home page with a button that generates a random number.

Random number

Actions

Actions are where business logic is stored. The place where you write the functions in Python instead of JavaScript. They are the ones that will be executed when the page is loaded, when a button is clicked, when a form is submitted, etc. They will render the HTML and send it to the client. They are the ones that will receive the data from the client and process it. They are the heart of Django LiveView.

In every app you can create a folder called actions and inside it a file for each page. For example, home.py for the home page. The file will have the following structure:

# my-app/actions/home.py
from liveview.context_processors import get_global_context
from core import settings
from liveview.utils import (
    get_html,
    update_active_nav,
    enable_lang,
    loading,
)
from channels.db import database_sync_to_async
from django.templatetags.static import static

template = "pages/home.html"

# Database

# Functions

async def get_context(consumer=None):
    context = get_global_context(consumer=consumer)
    # Update context
    context.update(
	{
	    "url": settings.DOMAIN_URL + reverse("home"),
	    "title": _("Home") + " | Home",
	    "meta": {
		"description": _("Home page of the website"),
		"image": f"{settings.DOMAIN_URL}{static('img/seo/og-image.jpg')}",
	    },
	    "active_nav": "home",
	    "page": template,
	}
    )
    return context


@enable_lang
@loading
async def send_page(consumer, client_data, lang=None):
    # Nav
    await update_active_nav(consumer, "home")
    # Main
    my_context = await get_context(consumer=consumer)
    html = await get_html(template, my_context)
    data = {
	"action": client_data["action"],
	"selector": "#main",
	"html": html,
    }
    data.update(my_context)
    await consumer.send_html(data)

async def random_number(consumer, client_data, lang=None):
    my_context = await get_context(consumer=consumer)
    data = {
	"action": client_data["action"],
	"selector": "#output-random-number",
	"html": randint(0, 10),
    }
    data.update(my_context)
    await consumer.send_html(data)

Views

Django LiveView uses the same views as Django, but the main difference is that the views are asynchronous by default.

To make a view renderable by SSR (Server Side Rendering) and by SPA (Single Page Application), you need to create a function with the following structure:

  from .actions.home import get_context as get_home_context

  async def home(request):
      return render(request, settings.TEMPLATE_BASE, await get_home_context())

The get_home_context() function returns a dictionary with the context of the page present in the action. The settings.TEMPLATE_BASE is the base template that will be rendered, por example layouts/base.html.

If you want to render data from a database on the template, for example:

{% for article in articles %}
    {{ article.title }}
    {{ article.content }}
{% endfor %}

You will see an error: You cannot call this from an async context - use a thread or sync_to_async..

You can use the sync_to_async function from asgiref.

  from asgiref.sync import sync_to_async
  from .actions.blog_list import get_context as get_list_context

  async def blog_list(request):
      return await sync_to_async(render)(request, settings.TEMPLATE_BASE, await get_list_context())

Or transform articles to a list. But you lose the benefits of ORM.

Routing

If you want to move from one page to another, you can use the page controller and the changePage action.

For example, you can create a link to the about me page.

  <a
      data-controller="page"
      data-action="click->page#changePage"
      data-page="about_me"
      href="{% url "about me" %}" <!-- Optional -->
      role="button" <!-- Optional -->
     >Ver completo</a>
  • data-controller: Indicates that the element is a controller. page with functions to switch between pages.
  • data-action: Indicates that the element is an action. click to capture the click event. page#changePage to call the changePage function of the page controller.
  • data-page: Indicates the name of the page to which you want to move. The name is the same as the name of the action file. For example, actions/about_me.py.
  • href: Optional. It is recommended to use the href attribute to improve SEO or if JavaScript is disabled.
  • role: Optional. It is recommended to use the role attribute to improve accessibility or if JavaScript is disabled.

Send data

If you want to send data to the next page, you can use the data- attribute. All datasets will be sent.

For example, you can create a link to the blog single page with the slug of the article.

  <a
      data-controller="page"
      data-action="click->page#changePage"
      data-page="blog_single"
      data-slug="{{ article.slug }}"
      href="{% url "blog single" slug=article.slug %}" <!-- Optional -->
      role="button" <!-- Optional -->
     >Ver completo</a>

To receive the data in action blog_single.py you can use the client_data parameter with the data key.

    @enable_lang
    @loading
    async def send_page(consumer, client_data, lang=None):
	slug = client_data["data"]["slug"]
	# ...

Here you can see a typical example of a single page of a blog.

  @enable_lang
  @loading
  async def send_page(consumer, client_data, lang=None):
      # Nav
      await update_active_nav(consumer, "blog")
      # Main
      my_context = await get_context(consumer=consumer, slug=client_data["data"]["slug"])
      html = await get_html(template, my_context)
      data = {
	  "action": client_data["action"],
	  "selector": "#main",
	  "html": html,
      }
      data.update(my_context)
      await consumer.send_html(data)

History

If you make a SPA you will have a problem with the history management system. When you go back in history, you will lose the data and the HTML of the previous page. This is because the data is removed from the DOM. It is not a problem with Django LiveView.

Django LiveView has a history management system that allows you go back in history without receive any data from the server. Every time you change the page, the data and HTML are stored in the Session Storage. You don't need to do anything, it is automatic! 😸

The only limitation is forward navigation. If you want to go forward, you need to receive the data from the server because the data is remove from the Session Storage when you go back.

You can customize the history management system by editing the history controller in assets/js/mixins/history.js.

If you want to disable it, remove `startHistory();` from assets/js/main.js.

Deploy

You can deploy Django LiveView using any web server like reverse proxy.

Nginx

I recommend using Nginx. Here is an example of how to configure. Replace example.com with your domain and my-project with your folder name.

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $server_name;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_redirect off;
    }

    location /static {
        root /var/www/my-project;
    }

    location /media {
        root /var/www/my-project;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
}

It is important to note that the proxy_set_header lines are necessary for the WebSocket to work. You can see more about it in Channels.

FAQ

Do I need to know JavaScript to use Django LiveView?

No, you don't need. 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 capture events, send and receive strings over a WebSockets channel.

Can I use Django's native tools?

Of course. You can still use all of Django's native tools, such as its ORM, forms, plugins, 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.

Who finances the project?

Only me and my free time.

Make a blog

Below we will make a simple blog with classic features:

  • A list with posts
  • Single page post
  • Controls to navegate between list posts and singles
  • Pagination
  • Search

If you want to include a system commentary, read the next tutorial.

Creating models

Before starting, we will create the models that we will use in the blog.

Adding fake data

Preparing views (SSR)

Making templates

Including actions

Adding the feature: infinite scroll

Adding the feature: search

Add a commentary system

Creating models

Adding fake data

Preparing views (SSR)

Making templates

Including actions

Getting data

Showing

Source code

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

  • LiveView: Source code of the Django framework and app published in pip.
  • Website and Docs: All documentation, including this same page.
  • Templates

    • Starter: Check all the features of Django LiveView.
    • Minimal: The minimal template to get started.
    • Assets: Frontend assets.
  • Demos

    • Snake: The classic game of Snake.