* Home :PROPERTIES: :ONE: one-custom-default-home :CUSTOM_ID: / :TITLE: :DESCRIPTION: Framework for creating Realtime SPAs using HTML over the Wire technology. :END: ** 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 or APIs. One technology provides a secure, stable and low-delay connection for real-time web applications. #+ATTR_HTML: :class center-block image image--home [[#/img/step-1.avif][Architecture send]] #+ATTR_HTML: :class center-block image image--home [[#/img/step-2.avif][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 print article number 2. 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). #+ATTR_HTML: :class center-block image image--home [[#/img/step-3.avif][Send string]] 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. #+ATTR_HTML: :class center-block image image--home [[#/img/step-4.avif][Send JSON]] 5- JavaScript draws the received HTML in the indicated selector. #+ATTR_HTML: :class center-block image image--home [[#/img/step-5.avif][Place HTML]] The same process is repeated for each action, such as clicking a button, submitting a form, etc. ** What are your superpowers? - All in real time. The data is sent and received instantly. - Single connection. You don't need to open and close connections like calls to an API. - Create SPAs without using APIs o JavaScript frameworks. - Uses Django's template system to render the frontend (The use of JavaScript is anecdotal). - 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. Are you ready to create your first Realtime SPA? Let's go to the [[#/docs/quickstart/][Quickstart]]. * Quickstart :PROPERTIES: :ONE: one-custom-default-doc :CUSTOM_ID: /docs/quickstart/ :TITLE: Quickstart :DESCRIPTION: Get started with Django LiveView the easy way. :END: 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. #+BEGIN_SRC python 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())]) ) ), } ) #+END_SRC ** 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~. #+BEGIN_SRC python # 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) #+END_SRC 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~. #+BEGIN_SRC html {# my-app/templates/layouts/base.html #} {% load static i18n %} {% get_current_language as CURRENT_LANGUAGE %} {{ title }}
{% include 'components/header.html' %}
{% include page %}
#+END_SRC 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~. #+BEGIN_SRC html {# my-app/templates/pages/home.html #} {% load static %}

#+END_SRC 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 from liveview.utils import get_html async def home(request): return render(request, "layouts/base.html", await get_home_context()) #+END_SRC ** 11. Create URL Finally, we will create the URL that will render the page. #+BEGIN_SRC python # my-app/urls.py from django.urls import path from .views import home urlpatterns = [ path("", home, name="home"), ] #+END_SRC ** 12. Run the server Run the server. #+BEGIN_SRC sh python manage.py runserver #+END_SRC And open the browser at ~http://localhost:8000/~. You should see the home page with a button that generates a random number. #+ATTR_HTML: :class center-block image image--responsive [[#/img/quickstart/minimal-template.webp][Random number]] * Actions :PROPERTIES: :ONE: one-custom-default-doc :CUSTOM_ID: /docs/actions/ :TITLE: Actions :DESCRIPTION: Actions of Django LiveView. :END: 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: #+BEGIN_SRC python # 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 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) #+END_SRC Let's explain each part. - ~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. - ~url~: The URL of the page. It will be used to change the direction of the browser and the user perceives a page change, even if it is not real. - ~title~: The title of the page. - ~meta~: They are the SEO and Open Graph meta tags. - ~active_nav~: It is used to highlight the active page in the navigation menu. - ~page~: Name of the template that will be rendered. it is the same as ~template~. The function ~send_page()~ is responsible for rendering the page and sending it. #+BEGIN_SRC python from liveview.utils import ( get_html, update_active_nav, enable_lang, loading, ) @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) #+END_SRC ~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. Whenever you want to send a new HTML to the frontend, you will use the ~send_html()~ function with the following structure. #+BEGIN_SRC python data = { "action": "home->send_page", "selector": "#main", "html": "

My home

Welcome to my home

", } await consumer.send_html(data) #+END_SRC - ~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. Optionally we can include others. - ~append~: Default: false. If true, the HTML will be added, not replaced. - ~scroll~: Default: false. If true, the page will be scrolled to the selector - ~scrollTop~: Default: false. If true, the page will be scrolled to the top. When you update via context, you add the following. They are all optional. - ~title~: The title of the page. - ~meta~: They are the SEO and Open Graph meta tags. - ~active_nav~: It is used to highlight the active page in the navigation menu. - ~page~: Name of the template that will be rendered. ** Decorators You can use the following decorators to make your actions more readable and maintainable. - ~@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. ** Database access (ORM) 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~. #+BEGIN_SRC python from channels.db import database_sync_to_async from .models import Article template = "pages/articles.html" # Database @database_sync_to_async def get_articles(): # New return Article.objects.all() # Functions async def get_context(consumer=None): articles = await get_articles() context = get_global_context(consumer=consumer) # Update context context.update( { ... "articles": await get_articles(), # New } ) return context #+END_SRC Now you can use the ~articles~ variable in the template. #+BEGIN_SRC html {% for article in articles %}

{{ article.title }}

{{ article.content }}

{% endfor %} #+END_SRC If you want the SSR (Server Side Rendering) to continue working, you need to modify the view function so that it is asynchronous. From: #+BEGIN_SRC python async def articles(request): return render(request, settings.TEMPLATE_BASE, await get_context()) #+END_SRC To: #+BEGIN_SRC python from asgiref.sync import sync_to_async async def articles(request): return await sync_to_async(render)(request, settings.TEMPLATE_BASE, await get_context()) #+END_SRC * Views :PROPERTIES: :ONE: one-custom-default-doc :CUSTOM_ID: /docs/views/ :TITLE: Views :DESCRIPTION: Views of Django LiveView. :END: 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 async def home(request): return render(request, settings.TEMPLATE_BASE, await get_home_context()) #+END_SRC 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 async def blog_list(request): return await sync_to_async(render)(request, settings.TEMPLATE_BASE, await get_list_context()) #+END_SRC Or transform ~articles~ to a list. But you lose the benefits of ORM. * Routing :PROPERTIES: :ONE: one-custom-default-doc :CUSTOM_ID: /docs/routing/ :TITLE: Routing :DESCRIPTION: Routing of Django LiveView. :END: 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. #+BEGIN_SRC html role="button" >Ver completo #+END_SRC - ~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. #+BEGIN_SRC html role="button" >Ver completo #+END_SRC To receive the data in action ~blog_single.py~ you can use the ~client_data~ parameter with the ~data~ key. #+BEGIN_SRC python @enable_lang @loading async def send_page(consumer, client_data, lang=None): slug = client_data["data"]["slug"] # ... #+END_SRC Here you can see a typical example of a single page of a blog. #+BEGIN_SRC python @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) #+END_SRC * History :PROPERTIES: :ONE: one-custom-default-doc :CUSTOM_ID: /docs/history/ :TITLE: History :DESCRIPTION: History management of Django LiveView. :END: 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 :PROPERTIES: :ONE: one-custom-default-doc :CUSTOM_ID: /docs/deploy/ :TITLE: Deploy :DESCRIPTION: Deploy Django LiveView to production. :END: 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. #+BEGIN_SRC nginx 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; } } #+END_SRC 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]]. * FAQ :PROPERTIES: :ONE: one-custom-default-doc :CUSTOM_ID: /docs/faq/ :TITLE: FAQ (Frequently Asked Questions) :DESCRIPTION: Frequently asked questions about Django LiveView. :END: ** 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. * Source code :PROPERTIES: :ONE: one-custom-default-page :CUSTOM_ID: /source-code/ :TITLE: Source code :DESCRIPTION: List of all related source code. :END: You can find all the source code in the following repositories: - [[https://github.com/Django-LiveView/liveview][LiveView]]: Source code of the Django framework and app published in pip. - [[https://github.com/Django-LiveView/docs][Website and Docs]]: All documentation, including this same page. - Templates - [[https://github.com/Django-LiveView/starter-template][Starter]]: Check all the features of Django LiveView. - [[https://github.com/Django-LiveView/minimal-template][Minimal]]: The minimal template to get started. - [[https://github.com/Django-LiveView/assets][Assets]]: Frontend assets. - Demos - [[https://github.com/Django-LiveView/demo-snake][Snake]]: The classic game of Snake. * Books :PROPERTIES: :ONE: one-custom-default-page :CUSTOM_ID: /books/ :TITLE: :DESCRIPTION: Books about Django LiveView. :END: There are no books about Django LiveView yet. But you can find book about Django working with HTML over the Wire technology. ** Building SPAs with Django and HTML Over the Wire: Learn to build real-time single page applications with Python (English Edition) #+ATTR_HTML: :class center-block image image--cover-book [[#/img/books/building-spas.avif][Building SPAs with Django and HTML Over the Wire]] Buy: - [[https://www.packtpub.com/product/building-spas-with-django-and-html-over-the-wire/9781803240190][Packt]] - [[https://www.amazon.com/Real-time-Django-over-Wire-Channels-ebook/dp/B0B3DV54ZT/][Amazon.com]] - [[https://www.amazon.es/Real-time-Django-over-Wire-Channels-ebook/dp/B0B3DV54ZT/][Amazon.es]]