Actions | Django LiveView

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.

Backend side

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 django.utils.translation import gettext as _
from django.templatetags.static import static
from django.urls import reverse

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)

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.


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)

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.

data = {
    "action": "home->send_page",
    "selector": "#main",
    "html": "<h1>My home</h1><p>Welcome to my home</p>",
}
await consumer.send_html(data)
  • 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.

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

Now you can use the articles variable in the template.

{% for article in articles %}
    <h2>{{ article.title }}</h2>
    <p>{{ article.content }}</p>
{% endfor %}

If you want the SSR (Server Side Rendering) to continue working, you need to modify the view function so that it is asynchronous.

From:

async def articles(request):
    return render(request, settings.TEMPLATE_BASE, await get_context())

To:

from asgiref.sync import sync_to_async

async def articles(request):
    return await sync_to_async(render)(request, settings.TEMPLATE_BASE, await get_context())

Frontend side

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

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

<button
    data-action="click->page#changePage"
    data-page="about_me"
   >Go to about me page</button>

It would be equivalent to doing:

<button
    data-action="click->page#run"
    data-liveview-action="about_me"
    data-liveview-function="send_page"
   >Go to about me page</button>