banner



How To Display Only Some Blog Text Django

This tutorial will cover how to build dynamic forms in Django using Htmx. It will also cover the basic concepts of Django formsets. But most of all, we're going to focus on how to make dynamic forms look and feel good.

You can find the code from this tutorial in this GitHub repository

If you want to watch the video instead of reading:

Project Setup

Start by creating a Django project:

          virtualenv            env            source            env/bin/activate pip            install            django django-admin startproject djforms        
The latest version of Django at the time of this tutorial is 3.2.6

Run the migrations:

          python manage.py migrate        

Models

For this project we will work with the same set of models. Create a Django app and register it in the settings:

          python manage.py startapp books        
            INSTALLED_APPS              =              [              .              .              .              'django.contrib.staticfiles'              ,              'books'              ]                      
Add it to INSTALLED_APPS in settings.py

Inside books/models.py add the following models:

                      from            django.db            import            models            class            Author            (models.Model)            :            name            =            models.CharField(max_length=            50            )            def            __str__            (self)            :            return            self.name            class            Book            (models.Model)            :            author            =            models.ForeignKey(Author,            on_delete=models.CASCADE)            title            =            models.CharField(max_length=            100            )            number_of_pages            =            models.PositiveIntegerField(default=            1            )            def            __str__            (self)            :            return            self.title                  

And add the following to books/admin.py:

                      from            django.contrib            import            admin            from            .models            import            Author,            Book            class            BookInLineAdmin            (admin.TabularInline)            :            model            =            Book            class            AuthorAdmin            (admin.ModelAdmin)            :            inlines            =            [BookInLineAdmin]            admin.site.register(Author,            AuthorAdmin)                  

Using these models we can create an author and add as many books as we want to that author.

Run the migrations:

          python manage.py makemigrations python manage.py migrate        

How to use Django Formsets

Formsets are one of the best parts of Django. The idea behind formsets is that you get a really flexible rendering of forms in your template and you don't have to write a lot of code to achieve it.

A formset is a layer of abstraction to work with multiple forms on the same page - Django docs

Formset factories are the main tools you can use to create formsets in Django:

                      from            django.forms.models            import            (            inlineformset_factory,            formset_factory,            modelform_factory,            modelformset_factory            )                  

Create a file forms.py inside the books app and add the following:

                      from            django            import            forms            from            .models            import            Book            class            BookForm            (forms.ModelForm)            :            class            Meta            :            model            =            Book         fields            =            (            'title'            ,            'number_of_pages'            )                  

We'll use the inlineformset_factory to create the formset but the other functions work pretty much the same way. The only difference is that modelform_factory and modelformset_factory work specifically with forms that inherit from forms.ModelForm.

Inside forms.py add the following:

                      from            django.forms.models            import            inlineformset_factory            from            .models            import            Author            .            .            .            BookFormSet            =            inlineformset_factory(            Author,            Book,            form=BookForm,            min_num=            2            ,            # minimum number of forms that must be filled in            extra=            1            ,            # number of empty forms to display            can_delete=            False            # show a checkbox in each form to delete the row            )                  

Here we are creating an inline formset. The first argument is the parent model, which in this case is the Author. The second argument is the child model which is the Book.  The form argument is the form used to create Book instances, and the other arguments change the styling of the form.

We'll now use this form in a function-based view. Inside books/views.py add the following:

                      from            django.shortcuts            import            redirect,            render            from            .forms            import            BookFormSet            from            .models            import            Author            def            create_book            (request,            pk)            :            author            =            Author.objects.get(            id            =pk)            books            =            Book.objects.            filter            (author=author)            formset            =            BookFormSet(request.POST            or            None            )            if            request.method            ==            "POST"            :            if            formset.is_valid(            )            :            formset.instance            =            author             formset.save(            )            return            redirect(            "create-book"            ,            pk=author.            id            )            context            =            {            "formset"            :            formset,            "author"            :            author,            "books"            :            books            }            return            render(request,            "create_book.html"            ,            context)                  

In this view we create an instance of the BookFormSet and pass it into the context. If the request method is a POST request we then pass the request into the form, check if it is valid and then call the save() method. Because we are using a ModelForm this will save the values of the form as Book instances. Notice we're also assigning the instance of the formset as the author. The instance property is needed to link the child models to the parent.

Important to note is that this view requires the primary key of the author that we will add books to. Create a few authors in the Django admin:

            python manage.py createsuperuser          
Add a superuser so you can login to the admin

Add authors in the admin

Add the view to the project urls.py:

                      from            django.contrib            import            admin            from            django.urls            import            path            from            books.views            import            create_book   urlpatterns            =            [            path(            'admin/'            ,            admin.site.urls)            ,            path(            '<pk>/'            ,            create_book,            name=            'create-book'            )            ]                  

In the root of the project create a templates folder and inside it create create_book.html. Add the following to it:

                                    <!              DOCTYPE              html              >                                                      <html              lang                              =                "en"                            >                                                      <head              >                                                      <meta              charset                              =                "UTF-8"                            >                                                      <meta              http-equiv                              =                "X-UA-Compatible"                            content                              =                "IE=edge"                            >                                                      <meta              name                              =                "viewport"                            content                              =                "width=device-width, initial-scale=1.0"                            >                                                      <title              >            Create a book                              </title              >                                                      </head              >                                                      <body              >                                                      <h1              >            Create books for {{ author.name }}                              </h1              >                                                      <form              method                              =                "POST"                            >                        {% csrf_token %}         {{ formset.management_form }}         {{ formset.as_p }}                                          <button              >            Submit                              </button              >                                                      </form              >                                                      <hr              >                                                      <h2              >            Books                              </h2              >                        {% for book in books %}                                          <p              >            {{ book.title }} - {{ book.number_of_pages }}                              </p              >                        {% endfor %}                                          </body              >                                                      </html              >                              

Register the templates folder in the settings.py:

          TEMPLATES            =            [            {            .            .            .            'DIRS'            :            [BASE_DIR            /            "templates"            ]            ,            .            .            .            }            ,            ]                  

Visit http://127.0.0.1:8000/1 and you should see three forms to create books as well as the heading showing Create books for Joe.

Inspect the page and go to the Elements tab in the developer tools - you should see the following:

Developer tools of Django formset

Django's formsets include a lot of hidden fields. The {{ formset.management_form }} renders them in the template. These fields are very important because they provide Django with meta information about the forms. To understand how to make dynamic formsets it is important to understand how the forms are rendered.

Create some books

Fill in the book form and submit it. You should see the newly created books display at the bottom of the page.

How to setup Htmx with Django

Htmx is a library that allows you to access modern browser features directly from HTML, rather than using JavaScript.

There are many examples of how to use Htmx for things like deleting table rows, progress bars, file uploads and much more.

So the question is; how do you use Htmx for dynamic forms?

There are some packages available to setup Htmx with Django. However, we are going to install it from scratch. You can also follow the official Htmx installation docs.

Create a base html file

We will use a base.html for all the other templates to inherit from so that they all contain the required files for Htmx. Create templates/base.html and add the following:

                                    <!              DOCTYPE              html              >                                                      <html              lang                              =                "en"                            >                                                      <head              >                                                      <meta              charset                              =                "UTF-8"                            >                                                      <meta              http-equiv                              =                "X-UA-Compatible"                            content                              =                "IE=edge"                            >                                                      <meta              name                              =                "viewport"                            content                              =                "width=device-width, initial-scale=1.0"                            >                                                      <title              >            Htmx Formsets                              </title              >                                                      <script              src                              =                "https://unpkg.com/htmx.org@1.5.0"                            integrity                              =                "sha384-oGA+prIp5Vchu6we2YkI51UtVzN9Jpx2Z7PnR1I78PnZlN8LkrCT4lqqqmDkyrvI"                            crossorigin                              =                "anonymous"                            >                                                                  </script              >                                                      </head              >                                                      <body              >                        {% block content %}     {% endblock content %}                                          <script              >                                                      document                .                body                .                addEventListener                (                'htmx:configRequest'                ,                (                event                )                =>                {                event.                detail                .                headers                [                'X-CSRFToken'                ]                =                '{{ csrf_token }}'                ;                }                )                                                                    </script              >                                                      </body              >                                                      </html              >                              

In the head of the document we've added the script to use the CDN for Htmx. We've also added a script at the bottom for Htmx to listen for requests and add the csrf_token so that POST requests are accepted.

Htmx Book Create View

The first Htmx view we'll create is the view that will return a new form.

Inside views.py add the following view:

                      from            .forms            import            BookForm            def            create_book_form            (request)            :            form            =            BookForm(            )            context            =            {            "form"            :            form            }            return            render(request,            "partials/book_form.html"            ,            context)                  

Notice that we are using the BookForm here. Not the BookFormSet.

Add the view to the urls.py:

                      from            books.views            import            create_book_form   urlpatterns            =            [            .            .            .            ,            path(            'htmx/create-book-form/'            ,            create_book_form,            name=            'create-book-form'            )            ]                  

Now back inside create_book.html replace everything with the following:

          {% extends "base.html" %}  {% block content %}                                          <h1              >            Create books for {{ author.name }}                              </h1              >                                                      <button              type                              =                "button"                            hx-get                              =                "{% url                'create-book-form'                %}"                            hx-target                              =                "#bookforms"                            hx-swap                              =                "beforeend"                            >                        Add form                                          </button              >                                                      <div              id                              =                "bookforms"                            >                                                      </div              >                        {% endblock %}        

We're now extending from base.html which lets us use Htmx properties. On the button element we've added the hx-get attribute which is pointing to the create-book-form URL. The target is set as the div with an ID of bookforms . Lastly the hx-swap attribute is for configuring how the response is rendered. beforeend will add the response to the end of the div.

When you click the button a GET request is sent to the backend where Django will return an HTML response of an empty BookForm . The HTML response is then added to the bookforms div.

Click the Add form button and you should see the following:

Dynamic forms in Django

This gives a nice dynamic feeling.

To get the form submissions to work we have to change the create_book view. It no longer works with FormSets so it now looks like this:

                      def            create_book            (request,            pk)            :            author            =            Author.objects.get(            id            =pk)            books            =            Book.objects.            filter            (author=author)            form            =            BookForm(request.POST            or            None            )            if            request.method            ==            "POST"            :            if            form.is_valid(            )            :            book            =            form.save(commit=            False            )            book.author            =            author             book.save(            )            return            HttpResponse(            "success"            )            else            :            return            render(request,            "partials/book_form.html"            ,            context=            {            "form"            :            form            }            )            context            =            {            "form"            :            form,            "author"            :            author,            "books"            :            books            }            return            render(request,            "create_book.html"            ,            context)                  

Notice the else statement returns a render of the form with the book_form.html template so that the form errors can be displayed.

We also have to add some functionality to book_form.html

                                                    <div              hx-target                              =                "this"                            hx-swap                              =                "outerHTML"                            >                                                      <form              method                              =                "POST"                            >                        {% csrf_token %}         {{ form }}                                          <button              type                              =                "submit"                            hx-post                              =                "."                            >            Submit                              </button              >                                                      </form              >                                                      </div              >                              

We have wrapped the form inside a div with two Htmx properties. The hx-target specifies this as the target which means it is pointing to itself. The hx-swap property has been set to outerHTML . Combining these two properties basically means that when the form is submitted, the entire form will be replaced by the response. The hx-post property on the button element ensures we send an Htmx request and not a normal request. The . value means the request will be sent to the current URL.

Test the form submission. You should see the form is replaced with success. That is because the HttpResponse is returning success. We can get more creative with this response by adding a detail view and returning the detail view response instead.

Htmx Book Detail View

Add the following view:

          from django.shortcuts import get_object_or_404   def detail_book(request, pk):     book = get_object_or_404(Book, id=pk)     context = {         "book": book     }     return render(request, "partials/book_detail.html", context)        

Add the view to the urls:

                      from            books.views            import            detail_book  urlpatterns            =            [            .            .            .            path(            'htmx/book/<pk>/'            ,            detail_book,            name=            "detail-book"            )            ,            ]                  

And create partials/book_detail.html:

                                                    <div              >                                                      <h3              >            Book Name: {{ book.title }}                              </h3              >                                                      <p              >            Number of pages: {{ book.number_of_pages }}                              </p              >                                                      </div              >                              

Change the response in the create_book view from:

                      return            HttpResponse(            "success"            )                  

to:

                      return            redirect(            "detail-book"            ,            pk=book.            id            )                  

This will return the detail view of the book as the response for when the form is submitted.

Test the form submission and you should see the book title and number of pages being displayed, while the form disappears.

Testing form response

Htmx Book Update View

Now we have the create view and detail view working. We'll add the update view so that when the book is created we can click a button to edit that book.

Create the view:

                      def            update_book            (request,            pk)            :            book            =            Book.objects.get(            id            =pk)            form            =            BookForm(request.POST            or            None            ,            instance=book)            if            request.method            ==            "POST"            :            if            form.is_valid(            )            :            form.save(            )            return            redirect(            "detail-book"            ,            pk=book.            id            )            context            =            {            "form"            :            form,            "book"            :            book            }            return            render(request,            "partials/book_form.html"            ,            context)                  

This works similarly to the create view. The main difference is that we're passing in instance=book to the form to update the book. We're also returning partials/book_form.html which renders the same form as in the create_view. But be careful though. In the template there's no way to distinguish between updating books and creating new books.

Update book_form.html so that the button is different depending on if we're updating an existing book:

          {% if book %}                                          <button              type                              =                "submit"                            hx-post                              =                "{% url                'update-book'                book.id %}"                            >                        Submit                                          </button              >                        {% else %}                                          <button              hx-post                              =                "."                            >                        Submit                                          </button              >                        {% endif %}        

Add the view to the urls:

          from books.views import update_book  urlpatterns = [     ...     path('htmx/book/<pk>/update/', update_book, name="update-book"), ]        

Replace the contents of book_detail.html with the following:

          <div hx-target="this" hx-swap="outerHTML">      <h3>Book Name: {{ book.title }}</h3>     <p>Number of pages: {{ book.number_of_pages }}</p>     <button hx-get="{% url 'update-book' book.id %}">Update</button>  </div>        

Similar to book_form.html , in this template we've added the attributes hx-target and hx-swap so that when the request is made it swaps the entire detail snippet for the response - which in this case is the populated form from the update view.

Test it out and check that the books are being updated after you save.

Htmx Book Delete View

Add the view:

                      def            delete_book            (request,            pk)            :            book            =            get_object_or_404(Book,            id            =pk)            if            request.method            ==            "POST"            :            book.delete(            )            return            HttpResponse(            ""            )            return            HttpResponseNotAllowed(            [            "POST"            ,            ]            )                  

Add it to the urls:

                      from            books.views            import            delete_book   urlpatterns            =            [            path(            'htmx/book/<pk>/delete/'            ,            delete_book,            name=            "delete-book"            )            ,            ]                  

Add a delete button to the book_detail.html:

                                                    <button              hx-post                              =                "{% url                'delete-book'                book.id %}"                            >            Delete                              </button              >                              

To make testing easier, loop through the books in the create_book.html. Add the following inside the content block:

          {% for book in books %}  {% include "partials/book_detail.html" %}  {% endfor %}        

Test the delete button.  You should see the book removed from the page. Check the Django admin as well to confirm that the book is deleted.

Cancel Button

When clicking to update a book there is no way to cancel and go back to the detail view.

Update book_form.html to look like this:

                                                    <div              hx-target                              =                "this"                            hx-swap                              =                "outerHTML"                            >                                                      <form              method                              =                "POST"                            >                        {% csrf_token %}         {{ form }}                                          <button              type                              =                "submit"                            hx-post                              =                "."                            >            Submit                              </button              >                        {% if book %}                                          <button              type                              =                "submit"                            hx-post                              =                "{% url                'update-book'                book.id %}"                            >                        Submit                                          </button              >                                                      <button              hx-get                              =                "{% url                'detail-book'                book.id %}"                            >                        Cancel                                          </button              >                        {% else %}                                          <button              hx-post                              =                "."                            >                        Submit                                          </button              >                        {% endif %}                                          </form              >                                                      </div              >                              

We've added a button that requests the detail view. It will also replace the outer HTML with the response from the request. In this way it acts like a cancel button.

Now test to update a form and then click the cancel button. It should replace the form with the detail view of the book.

Django Formsets vs Htmx

Django's Formsets are very useful. But if you want to make the formsets look and feel good, particularly when using inline formsets, then you'll need to add JavaScript. This can land up being very complex and time consuming to get right.

I spent a lot of time trying to get formsets to play nice with Htmx. But ultimately decided that these two just don't work well together. Here's why:

Brennan Tymrak's article on dynamic formsets outlines a way to dynamically render formsets using JavaScript. When it comes to making formsets dynamic:

Adding additional forms requires using JavaScript to:

  • Get an existing form from the page
  • Make a copy of the form
  • Increment the number of the form
  • Insert the new form on the page
  • Update the number of total forms in the management form

To try replicate this functionality in Htmx defeats the point of using Htmx. It requires some complicated logic that might as well be done using JavaScript.

Ultimately, the solution to achieving dynamic form logic with Htmx is to not use formsets. As you've seen in this tutorial so far we haven't used formsets at all when dealing with Htmx.

While this solution might not end up with exactly the result you were looking for, in my experience the things that matter are:

  • How understandable and maintainable is the code?
  • Does the desired outcome solve the problem?

With what we've shown so far I believe both these boxes can be ticked.

Making Forms Look Good

One of the issues with formsets is that while they function well, they normally don't look great.

We're going to add TailwindCSS to the project to style the forms. We'll use the CDN because it is easier to test with. In production you would want to minimise the size of the CSS bundle. A project like django-tailwind can help achieve this.

Add the CDN

To base.html add the CDN in the head tag:

                                                    <head              >                        ...                                          <link              href                              =                "https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"                            rel                              =                "stylesheet"                            >                                                      </head              >                              

In base.html wrap the content block like this:

                                                    <div              class                              =                "py-5 px-4 sm:px-6 max-w-5xl mx-auto"                            >                        {% block content %}     {% endblock content %}                                          </div              >                              

Update create_book.html:

          {% extends "base.html" %}  {% block content %}                                          <div              class                              =                "md:flex md:items-center md:justify-between"                            >                                                      <div              class                              =                "flex-1 min-w-0"                            >                                                      <h2              class                              =                "text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate"                            >                        Create books for {{ author.name }}                                          </h2              >                                                      </div              >                                                      <div              class                              =                "mt-4 flex md:mt-0 md:ml-4"                            >                                                      <button              type                              =                "button"                            hx-get                              =                "{% url                'create-book-form'                %}"                            hx-target                              =                "#bookforms"                            hx-swap                              =                "beforeend"                            class                              =                "ml-3 inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"                            >                        Add form                                          </button              >                                                      </div              >                                                      </div              >                                                      <div              id                              =                "bookforms"                            class                              =                "py-5 mt-5"                            >                                                      </div              >                                                      <div              class                              =                "mt-5 py-5 border-t border-gray-100"                            >                        {% for book in books %}      {% include "partials/book_detail.html" %}      {% endfor %}                                          </div              >                        {% endblock %}        

Update book_form.html:

                                                    <div              hx-target                              =                "this"                            hx-swap                              =                "outerHTML"                            class                              =                "mt-3 py-3 px-3 bg-white shadow border border-gray-100"                            >                                                      <form              method                              =                "POST"                            >                        {% csrf_token %}         {{ form }}         {% if book %}                                          <button              type                              =                "submit"                            hx-post                              =                "{% url                'update-book'                book.id %}"                            class                              =                "inline-flex items-center px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"                            >                        Submit                                          </button              >                                                      <button              hx-get                              =                "{% url                'detail-book'                book.id %}"                            type                              =                "button"                            class                              =                "ml-2 inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"                            >                        Cancel                                          </button              >                        {% else %}                                          <button              type                              =                "submit"                            hx-post                              =                "."                            class                              =                "inline-flex items-center px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"                            >                        Submit                                          </button              >                        {% endif %}                                          </form              >                                                      </div              >                              

Update book_detail.html:

                                                    <div              hx-target                              =                "this"                            hx-swap                              =                "outerHTML"                            class                              =                "mt-3 py-3 px-3 bg-white shadow border border-gray-100"                            >                                                      <h3              class                              =                "text-lg leading-6 font-medium text-gray-900"                            >                        Book Name: {{ book.title }}                                          </h3              >                                                      <p              class                              =                "text-gray-600"                            >            Number of pages: {{ book.number_of_pages }}                              </p              >                                                      <div              class                              =                "mt-2"                            >                                                      <button              hx-get                              =                "{% url                'update-book'                book.id %}"                            class                              =                "inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"                            >                        Update                                          </button              >                                                      <button              hx-post                              =                "{% url                'delete-book'                book.id %}"                            class                              =                "ml-2 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"                            >            Delete                              </button              >                                                      </div              >                                                      </div              >                              

Crispy Forms

The go-to package for better forms is django-crispy-forms. We're going to use the TailwindCSS template pack for styling.

Install both packages:

          pip            install            django-crispy-forms crispy-tailwind        

Configure the package in settings.py:

          INSTALLED_APPS            =            (            .            .            .            "crispy_forms"            ,            "crispy_tailwind"            ,            .            .            .            )            CRISPY_ALLOWED_TEMPLATE_PACKS            =            "tailwind"            CRISPY_TEMPLATE_PACK            =            "tailwind"                  

Now in book_form.html load the tailwind filters at the top:

          {% load tailwind_filters %}        

And make the form crispy:

          {{ form|crispy }}        

Now we have much better looking forms. Play around with the project. Maybe there are some areas you want to improve on.

Conclusion

So far Htmx has been very useful. Using it you can write simple code that significantly improves the UI experience. Dynamic forms feel like a breeze and we don't even have to work with formsets or JavaScript.

Django Pro

If you want to become a professional Django developer you can find many courses over on learn.justdjango.com.

Links:

  • https://docs.djangoproject.com/en/3.2/topics/forms/formsets/

How To Display Only Some Blog Text Django

Source: https://justdjango.com/blog/dynamic-forms-in-django-htmx

Posted by: harristheadis.blogspot.com

0 Response to "How To Display Only Some Blog Text Django"

Post a Comment

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel