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
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
*
text
`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
*
text
`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
text
`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
*
]
*
text
`{% 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
*
text
`
<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
](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
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