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 or APIs. One technology provides a secure, stable and low-delay connection for real-time web applications.
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.
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. The can be launched by the backend or by the frontend. For example, when a button is clicked, the frontend sends a request to the backend to execute an action. But the backend can send a request to the frontend at any time, for example when a new commentary is added to an article then all clients will receive the new commentary. In other words, the actions can be executed by the backend or by the frontend. The more common actions will be render HTML and send it to the client, but you can do anything you want. 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:
~update_active_nav()~ updates the class that marks the page where we are in the menu. You need update the context with the data that you want to send to the client. ~get_html()~ is a function that renders the template with the context. ~send_html()~ is a function that sends the HTML to the client.
- ~action~: The name of the action that will be executed on the client side. It is used for cache management and to know which action to execute. It will almost always be the same action that the client sent us.
- ~selector~: The selector where the HTML will be placed.
- ~html~: The HTML that will be placed in the selector.
- ~@enable_lang~: It is used to enable the language. It is necessary to use the ~gettext~ function. If you site only has one language, you can remove it.
- ~@loading~: It is used to show a loading animation while the page is being rendered. If there is no loading delay, for example the database access is very fast or you don't access anything external like an API, you can remove it.
If you want to access the database, you can use the Django ORM as you would in a normal view. The only difference is that the views are asynchronous by default. You can use the ~database_sync_to_async~ function from ~channels.db~.
The frontend is responsible for capturing events and sending and receiving strings. No logic, rendering or state is in the frontend. It is the backend that does all the work.
Since DOM trees are constantly being created, updated and deleted, a small framework is used to manage events and avoid collisions: [[https://stimulus.hotwired.dev/][Stimulus]]. It is very simple and easy to use. In addition, some custom functions have been created to simplify more processes such as WebSockets connection, data sending, painting, history, etc. When you cloned the [[https://github.com/Django-LiveView/assets][assets]] repository you included all of that. You do not have to do anything.
*** Run actions
In the following example, we will create a button that when clicked will call the ~sent_articles_next_page~ function of the ~blog_list~ action (~actions/blog_list.py~).
#+BEGIN_SRC html
<button type="button"
data-action="click->page#run"
data-liveview-action="blog_list"
data-liveview-function="send_articles_next_page"
data-next-page="2"
>Cargar mas</button>
#+END_SRC
- ~data-action~: Required. Indicate the event (~click~), the controller (~page~ is the default controller in Django LiveView) and function in Stimulus (~run~). You will never change it.
- ~data-liveview-action~: Required. The name of the action file (~blog_list.py~).
- ~data-liveview-function~: Required. The name of the function in the action file (~send_articles_next_page~).
- ~data-next-page~: Optional. You can send data to the backend. In this case, the page number.
*** Change page
All actions have a mandatory function ~send_page~. It is used to move from one page to another. There is a quick function called ~changePage~ to do this.
For example, if you want to go to the About Me page, you can use the following code.
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:
#+BEGIN_SRC python
from .actions.home import get_context as 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:
#+BEGIN_SRC html
{% for article in articles %}
{{ article.title }}
{{ article.content }}
{% endfor %}
#+END_SRC
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~.
#+BEGIN_SRC python
from asgiref.sync import sync_to_async
from .actions.blog_list import get_context as get_list_context
- ~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.
Los formularios en Django LiveView son nativos de Django. Puedes usar los formularios de modelo, los formularios de clase, los formularios de funciones, etc. O crearlos directamente en HTML. La única diferencia es que los datos se envían a través de WebSockets en lugar de HTTP.
** Recoger un campo
A continución puedes ver un ejemplo de un formulario de búsqueda donde se crea un campo de texto.
#+BEGIN_SRC python
from django import forms
class SearchForm(forms.Form):
search = forms.CharField(
label="Search",
max_length=255,
widget=forms.TextInput(
attrs={
"data-action": "keyup.enter->page#run",
"data-liveview-action": "blog_list",
"data-liveview-function": "send_search_results",
},
),
)
#+END_SRC
Cuando se pulse la tecla ~Enter~ se ejecutará la función ~send_search_results~ del archivo de acción ~blog_list.py~.
Incluyelo en el contexto.
#+BEGIN_SRC python
async def get_context(consumer=None):
context = get_global_context(consumer=consumer)
# Update context
context.update(
{
...
"search_form": SearchForm(),
...
}
)
return context
#+END_SRC
E imprimirlo en el template puedes hacerlo mediante el objeto.
#+BEGIN_SRC html
<form id="search" class="search">
{{ search_form.as_p }}
</form>
#+END_SRC
Para recoger los datos en la función ~send_search_results~ puedes hacerlo mediante el objeto ~client_data~.
Recibir un campo o todo un formulario es similar. La diferencia es que ~action~, ~data-liveview-action~ y ~data-liveview-function~ se encuentran en el formulario, o botón de envío, y no en un campo concreto.
Veamos un ejemplo de un formulario de contacto.
#+BEGIN_SRC python
class ContactForm(forms.Form):
name = forms.CharField(
label="Name",
max_length=255,
)
email = forms.EmailField(
label="Email",
max_length=255,
)
message = forms.CharField(
label="Message",
)
#+END_SRC
Lo incluimos en el contexto.
#+BEGIN_SRC python
async def get_context(consumer=None):
context = get_global_context(consumer=consumer)
# Update context
context.update(
{
...
"contact_form": ContactForm(),
...
}
)
return context
#+END_SRC
Y lo imprimimos en el template con el botón de envío personalizado.
#+BEGIN_SRC html
<form id="contact" class="contact">
{{ contact_form.as_p }}
<button
type="button"
data-action="click->page#run"
data-liveview-action="contact"
data-liveview-function="send_contact"
>Send</button>
</form>
#+END_SRC
Los datos se recogen de la misma forma que en el caso anterior.
Para validar un formulario puedes hacerlo de la misma forma que en Django. Pero si quieres mostrar los errores en tiempo real, debes redibujar el formulario en cada ocasión.
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~.
Django LiveView uses the same internationalization system as Django. You can read more about it in the [[https://docs.djangoproject.com/en/dev/topics/i18n/][Django documentation]]. However, let's go deeper.
Every time that the client sends a request for action, it sends the language that the user has set in the browser.
#+BEGIN_SRC json
{
"action": "blog_list->send_page",
"data": {
"lang": "es"
}
}
#+END_SRC
The ~lang~ attribute is extracted directly from the ~<html>~ tag.
#+BEGIN_SRC html
{% load static i18n %}
<!doctype html>{% get_current_language as CURRENT_LANGUAGE %}
<html lang="{{ CURRENT_LANGUAGE }}">
#+END_SRC
You can access the language with the ~lang~ parameter of the action using the ~@enable_lang~ decorator.
Or you could read it with the ~lang~ key of the ~client_data~ parameter.
#+BEGIN_SRC python
@loading
async def send_page(consumer, client_data):
print(client_data["data"]["lang"])
#+END_SRC
You can read the tutorial [[#/tutorials/internationalize-with-subdomains/][Internationalize with subdomains]] to see how to create a multilingual website with subdomains.
It is very useful when the database access is slow or you access external services like APIs. You can make a loading animation while the page is being rendered.
For example, first create a template called ~loading.html~.
#+BEGIN_SRC html
<div
style="
position: fixed;
inset: 0;
z-index: 9999;
backdrop-filter: blur(3px);
"
></div>
#+END_SRC
This code will blur the entire page. You can customize it as you like.
It may be the case that the connection is lost, when user has lost the internet connection or the server has been restarted, and it's a problem because Django LiveView depends on a constant connection. In these cases, the client automatically will try to reconnect to the server. But while this happens, the user will have to be informed and any type of interaction blocked. If you do not block user interactions, actions will accumulate in the browser until communication is reestablished. Where they will all be sent at once. If, for example, the user impatiently presses the next page button 10 times, the user will skip 10 pages. It is a problem for him and for the server that will process many instructions at once.
If there is no connection, the ~#no-connection~ will be displayed with ~.no-connection--show~ class. Otherwise, it will be hidden with the ~.no-connection--hide~ class.
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 [[https://channels.readthedocs.io/en/latest/deploying.html][Channels]].
** 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.
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 [[https://github.com/Django-LiveView/minimal-template][minimalist template]].
** 1. Install Django
Install Django, create a project and an app.
** 2. Install LiveView
Install django-liveview with ~pip~.
#+BEGIN_SRC sh
pip install django-liveview
#+END_SRC
** 3. Modify the configuration
Add ~liveview~ to your installed ~INSTALLED_APPS~.
#+BEGIN_SRC python
INSTALLED_APPS = [
"daphne",
"channels",
"liveview",
]
#+END_SRC
Then indicate in which previously created App you want to implement LiveView.
#+BEGIN_SRC python
LIVEVIEW_APPS = ["website"]
#+END_SRC
** 4. Migration
Execute the migrations so that the LiveView tables are generated.
#+BEGIN_SRC python
python manage.py migrate
#+END_SRC
** 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.
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~.
#+BEGIN_SRC python
# my-app/actions/home.py
from liveview.context_processors import get_global_context
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~.
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 [[https://github.com/Django-LiveView/assets/archive/refs/heads/main.zip][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:
#+BEGIN_SRC python
# my-app/views.py
from django.shortcuts import render
# Create your views here.
def home(request):
return render(request, "pages/home.html")
#+END_SRC
With LiveView, on the other hand, you will have the following structure.
#+BEGIN_SRC python
# my-app/views.py
from django.shortcuts import render
from .actions.home import get_context as get_home_context
Here you will learn how to create a multilingual website using Django LiveView.
We using subdomains to define the language (~en.example.com~, ~es.example.com~...), instead of using prefixes in addresses (~example.com/en/blog/~, ~example.com/es/blog/~). They simplify SEO, maintain consistency in the Sitemap and are easy to test.
And add or modify the following settings in the same file (~settings.py~).
#+BEGIN_SRC python
# Languages
# Enable internationalization
USE_I18N = True
# Default language
LANGUAGE_CODE = "en"
# Available languages
LANGUAGES = [
("en", _("English")),
("es", _("Spanish")),
]
# Locale paths
LOCALE_PATHS = (BASE_DIR / "locale/",)
#+END_SRC
** 3. Redirection with Middleware
Create a middleware that redirects the user to the correct subdomain and sets the language. If the user enters ~en.example.com~ or ~example.com~, English language will be activated. If the user enters ~es.example.com~, Spanish language will be activated. And if the user enters ~en.example.com~, it will be redirected to ~example.com~.
Create a file called ~middlewares.py~ in your project folder and add the following code.
For the url of the page, you can edit the ~urls.py~ file to include the language.
#+BEGIN_SRC python
from django.urls import path
from django.utils.translation import gettext as _
from .views import home
urlpatterns = [
path(_("home") + "/", home, name="home"),
path(_("about") + "/", about, name="about"),
]
#+END_SRC
** 5. Make messages
Create the ~locale~ folder in the root of your project and run the following commands.
#+BEGIN_SRC sh
./manage.py makemessages -l en
./manage.py makemessages -l es
#+END_SRC
The files ~locale/en/LC_MESSAGES/django.po~ and ~locale/es/LC_MESSAGES/django.po~ will be created. You can edit them with a text editor or use a translation tool like [[https://poedit.net/][Poedit]].
** 6. Compile messages
After translating the texts, compile the messages.
#+BEGIN_SRC sh
./manage.py compilemessages
#+END_SRC
Your multilingual website is ready. You can test it by entering ~example.com~, ~en.example.com~ and ~es.example.com~.
** 7. Selector of languages
You can create a selector of languages in the header of your website.
#+BEGIN_SRC html
{% load i18n %}
{% get_current_language as CURRENT_LANGUAGE %}
{% get_available_languages as AVAILABLE_LANGUAGES %}