How to create custom form widgets with Django, AlpineJS and TailwindCSS

In this short guide, you will learn how to create a custom form widget for your Django app.

Prerequisites

  • Basic knowledge of Django forms and views
  • How to use AlpineJS, TailwindCSS with Django - see this post here

LEVEL - 💻 💻 Intermediate to advanced - Basic knowledge of Django forms and templates is assumed.

The problem

You have ditched javascript for your forms, and are using HTMX. Your forms are sleek, 100% Django views and forms, no page reloads on submission but your widgets still look like this.

An ugly file upload widget

Create a new widget

The form above is based on a basic user model with at least the following fields. Authentication is out of the scope of this article. If you need more information about user models visit this section in the docs.

users/models.py

class User(AbstractUser):
    name = models.Charfield(_("Display Name"), blank=True, max_length=255)
    avatar = models.ImageField(upload_to="images)
    username = models.Charfield(_("username"), max_length=150, unique=True,..)

users/forms.py

from django import forms

class UpdateProfileForm(forms.ModelForm)
    
    class Meta:
        model = User
        fields = ["name", "username", "avatar"]
  

Notes

In Django, each model field is mapped to a default form field which in turn uses a default widget. A widget is Django’s representation of an HTML input element. It specifies specifying how the field will be rendered on the frontend and handles data extraction.

Using this setup, Django will use an image form field for the avatar model field. The default widget for images is the ClearableFileInput. It will render as <input type="file" ...> with an additional checkbox input to clear the field’s value, if the field is not required and has initial data. The widget uses the django/forms/widgets/clearable_file_input.html template.

Let's create a new custom file upload widget.

users/forms.py

from django import forms

class AvatarFileUploadInput(forms.ClearableFileInput):
    template_name = "users/form_widgets/avatar_file_upload_input.html


class UpdateProfileForm(forms.ModelForm)
    
    class Meta:
        model = User
        fields = ["name", "username", "avatar"]
        widgets = {"avatar": AvatarFileUploadInput }

Notes:

  • We create a new file input widget class called AvatarFileUploadInput which inherits from the ClearableFileInput class and replaces the template.
  • In the form class, we specify the new widget for the avatar field.

Add a custom file input template

In the snippet above, we specified a new template for our widget but have not created it yet. Let's do that now. For reference here is the original template from Django clearable_file_input.html template.

clearable_file_input.html template [Django source code]

{% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}"{% if widget.attrs.disabled %} disabled{% endif %}>
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}<br>
{{ widget.input_text }}:{% endif %}
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
Our custom widget:

Our custom widget:

users/form_widgets/avatar_file_upload_input.html


<div class="sm:col-span-6" x-data="{clear: false}">
    <div class="mt-1 flex items-center" :class="clear && 'blur-sm'">
      <span class="h-12 w-12 rounded-full overflow-hidden bg-gray-100 mr-2">
       {% if widget.is_initial %}
        <img class="mr-3" src="{{ widget.value.url }}" alt="{{ widget.value}}">
        {% else %}
        <svg class="h-full w-full text-gray-300" fill="currentColor" viewBox="0 0 24 24">
          <path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z"></path>
        </svg>
        {% endif %}
      </span>
      <label for="avatar" class="mt-0 relative cursor-pointer bg-white 
              rounded-md font-medium text-indigo-600 hover:text-indigo-500
              focus-within:outline-none focus-within:ring-2 
              focus-within:ring-offset-2 focus-within:ring-indigo-500">
       <span class="sr-only">Upload a file</span>
       <input class=" file:mr-4 file:py-2 file:px-4 file:border-0 
                      file:text-sm file:font-semibold file:bg-indigo-50 
                      file:text-violet-700 hover:file:bg-violet-100" 
             type="{{ widget.type }}" 
             name="{{ widget.name }}"
             {% include "django/forms/widgets/attrs.html" %}>
      </label>
    </div>
    {% if not widget.required %}
      <div class="flex flex-row mt-2 px-2">
       <label class="mt-0 mr-2" for="{{ widget.checkbox_id }}">
              Remove Profile Photo
       </label>
       <input x-model="clear" type="checkbox" 
              name="{{ widget.checkbox_name }}" 
              id="{{ widget.checkbox_id }}"
              {% if widget.attrs.disabled %} disabled{% endif %}>
      </div>
    {% endif %}
  </div>

Notes

  • x-data="{clear: false}" - this will instantiate this div as an AlpineJS component. AlpineJS needs to be included in your templates. You can read the article on how to set this up here.
  • <img class="mr-3" src="{{ widget.value.url }}" - here we are replacing the avatar text link with an image for the preview. In the original template this is shown as an anchor tag with the widget value as the text. <a href="{{ widget.value.url }}">{{ widget.value }}</a>
  • styling - TailwindCSS includes a collection of modifiers to help you with styling. In this example, we are using the file modifier to add some styling to our file upload field.
  • {% include "django/forms/widgets/attrs.html" %} - default attributes included with the widget. for example the accept="image/* and id=avatar attribute
  • AlpineJS enhancements - You will notice that we bind the tailwind class blur-sm to the AlpineJS data variable clear like this :class="clear && 'blur-sm'" . We also bind the value of the clear input checkbox to the same variable x-model="clear". When a user checks the box the avatar will be blurred.

Conclusion

That's it, if you have done everything correctly your new file upload widget should look like this.

A much better looking upload widget.Blurred image when photo is removed

This is example is basic but demonstrates how flexible Django can be. Combined with AlpineJS and TailwindCSS, this is a powerful stack to build amazing web apps.

If you are building an MVP, new product, SaaS application and want to get off the ground FAST, check out the Vanty Starter Kit - our Django-powered boilerplate for launching apps in record time.

Further reading

Copyright © 2022 www.advantch.com