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

January 23, 2022 - by Themba Mahlangu - 4 min read

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

Prerequisites

Code
Basic knowledge of Django forms and views

How to use AlpineJS, TailwindCSS with Django -
[
 see this post here
](https://www.advantch.com/blog/build-a-modern-web-app-using-django-and-javascript/)

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.

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
*

Code
 text
Code
`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
*

Code
 text
Code
`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

Code
 text
Code
`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:

Code
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
* ]
*

Code
 text
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
*

Code
 text
Code
`
<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>

Code
styling - TailwindCSS includes a
[
 collection of modifiers
](https://tailwindcss.com/docs/hover-focus-and-other-states#appendix)
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

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

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


Django on customizing widget instances


TailwindCSS

docs


AlpineJS docs