Django and HTMX - List And Create Page (with no refreshes!)

In this post, we're going to create a page that allows the user to track a list of their favourite movies. The user will be able to add new movies, and will see these new movies dynamically appear in their list without any page refreshes.

This is all possible due to HTMX!

We will wire up an HTMX POST request to the backend when a user adds a new film to their list, which will return rendered HTML content that can be slotted into the DOM with the all the user's chosen films (including the new film).

The associated video for this post can be found below.

Objectives

In this post, we will:

  • Learn how to build a reactive webpage that allows users to list out their favourite films and add new films
  • Learn how to return rendered templates from HTMX endpoints that can be swapped into the DOM upon changes

Project Setup

This will extend the code from the previous tutorial, which can be found on Github in the "Video #2" folder on the repo.

Adding a Film Model

We are going to allow a user to maintain a list of their favourite films, and this list will be user-specific. To do this, we are going to need a database model to persist a user's film choices. Let's create a model below.

class Film(models.Model):
    name = models.CharField(max_length=128, unique=True)
    users= models.ManyToManyField(User, related_name='films')

This model will store the film's name (which the user will enter on the webpage), and will also link each film to a list of users through a ManyToManyField. A film may be on many users' lists, and a user may have many films on their own list.

We don't want duplicate film names in the database, so we also specify that the name should be unique.

We now make the migrations and migrate these changes to the database with the following commands.

python manage.py makemigrations
python manage.py migrate

Excellent, we have modified our database to add a table for our movies, and have an implicit junction table to handle the user-film relations. Now, we're going to start building new templates and views to handle listing and creating movies with HTMX.

Creating Film ListView

Let's start by creating the template for the film list page.This page is going to give users the ability to:

  1. List out all of the movies they have in their list
  2. Add a new movie to their list

For this, we need to add this code to a new template, templates/films.html.

{% extends 'base.html' %}

{% block content %}
<div class="align-center col-10 offset-1">
    <div class="d-flex justify-content-between align-items-center">
        <p class="lead ml-0">My Films</p>
        <form class="d-flex align-items-center">
            {% csrf_token %}
            <input type="text" 
                   name="filmname" 
                   class="form-control-sm mr-2" 
                   placeholder="Enter a film" />

            <button hx-post="{% url 'add-film' %}" 
                    hx-target="#film-list" 
                    type="submit" 
                    class="btn btn-success btn-sm">Add Film</button>
        </form>
    </div>
    <hr/>

    <div id="film-list">
        {% include 'partials/film-list.html' %}
    </div>
</div>
{% endblock content %} 

We extend the base.html template, and notably add a <form> element on lines 7-18. This form has a single <input> field that enables the user to type the name of the film they'd like to add to their list.

There is also a <button> that allows the user to submit the form. This is where we are going to apply HTMX attributes. We use hx-post="{% url 'add-film' %}" to signify that we want HTMX to send a POST request to this endpoint (which we will create soon). This event will happen on the button's default trigger, which is the click action.

We also use the hx-target="#film-list" attribute to inform HTMX that the returned HTML from the AJAX request should be swapped into an element with the ID of film-list. This exists on line 22.

Within the target <div> on lines 22-24, we are including another template that is responsible for listing out the films that the user has already chosen. Let's define this template now, in the templates/partials/film-list.html file.

{% if films %}
    <ul class="list-group col-4">
    {% for film in films %}
        <li class="list-group-item">{{ film.name }}</li>
    {% endfor %}
    </ul>
{% else %}
    <p>You do not have any films in your list</p>
{% endif %}

This a simple template fragment, responsible for displaying all the films the user has added to their list. The template context variable films will be populated in the view that we are about to write.

This template fragment is what will be returned by our HTMX AJAX endpoint, and will then be swapped into the DOM to replace the {% include 'partials/film-list.html' %} statement from the parent films.html template.

Creating the URL and View

To display the list-page, we need to create a URL and link it to a view. Let's start with the URL. Within films/urls.py, add the following path to the urlpatterns list.

path("films/", views.FilmList.as_view(), name='film-list')

This will load a class-based view called FilmList that we will create in a moment. We've given this a URL a name of film-list, so let's also create a link to this in our templates/partials/navbar.html navigation bar. We need to replace the following HTML (which links to nowhere):

<a class="nav-link" href="#">Films</a>

With the following link to our new URL.

<a class="nav-link" href="{% url 'film-list' %}">Films</a>

We use Django's url templatetag to reference our new URL. Now, we need to build the FilmList view. Within views.py, add the following code.

from films.models import Film
from django.views.generic.list import ListView

class FilmList(ListView):
    template_name = 'films.html'
    model = Film
    context_object_name = 'films'

    def get_queryset(self):
        user = self.request.user
        return user.films.all() 

This view inherits from Django's ListView and links to the films.html template. Because we're listing out models, we specify the model = Film attribute.

A ListView should return a QuerySet of instances of the given model - by default, this will be all of the instances. In our case, we want to filter the returned QuerySet to only films added by the logged-in user, so we override the get_queryset() method to extract the user, and return their set of films.

This is enough to enable us to access the film-list page - although you will need to comment out the hx-post="{% url 'add-film' %}" attribute of the <button> element within the form, since we have not yet created that URL.

You should see something similar to the following.

We have a form on the top-right, and our button has HTMX attributes that will enable us to POST and create films on the backend, whilst adding them to the user's list.

We now need to set up the HTMX view to do this task, and to return new rendered HTML that HTMX can insert into the DOM at the hx-target. Currently, the button will not work. Let's create the HTMX AJAX view next!

Creating the HTMX View

Recall that the <button> element had an attribute hx-post="{% url 'add-film' %}". We now need to create this URL and this view, to allow HTMX to send the POST requests when the button is clicked.

Add the following URL to films/urls.py. This is an HTMX url, so I have chosen to add it to the htmx_urlpatterns list (however this is only my preference).

path('add-film/', views.add_film, name='add-film')

Importantly, this name matches the name referenced by the url templatetag within the hx-post attribute. HTMX can now send the POST request to this URL.

Next up, the add_film view within films/views.py. Add the following function-based view.

def add_film(request):
    # extract the film's name from the input field
    name = request.POST.get('filmname')

    # get or create the Film with the given name
    film = Film.objects.get_or_create(name=name)[0]

    # add the film to the user's list
    request.user.films.add(film)

    # return template with all of the user's films
    films = request.user.films.all()
    return render(request, 'partials/film-list.html', {'films': films})

The comments explain the process, but importantly, the final step after adding the new film to the user's list is to re-fetch all the films in their list, and then re-render the partials/film-list.html template with all the films (including the new one) in the context.

This returned template fragment is added to the hx-target="#film-list <div>. This allows the old fragment to be swapped with the new one, which also contains the new film that the user has added.

This allows the new film to appear without a refresh, as the AJAX response contains new HTML that is inserted into the DOM by HTMX. All without writing any JavaScript - beautiful!

This is all achieved without any page refreshes, making the process of adding a movie smooth and reactive to the user. The effect is similar to stateful front-end frameworks such as React and Vue.js, but without the complexity of managing and deploying those frameworks separately from Django.

Summary

In this post, we've built out a simple film list page, that allows users to view a list of their favourite films. This page also allows users to dynamically add new films to their list via HTMX AJAX requests, without page reloads, and in an efficient manner.

In this next post, we will look at how to delete these items dynamically with HTMX, and again without any refreshing of the page. We will build this out to give an effect similar to single-page applications, but without needing to write any JavaScript!

If you enjoyed this post, please subscribe to our YouTube channel and follow us on Twitter to keep up with our new content!

Please also consider buying us a coffee, to encourage us to create more posts and videos!

;