In this short guide, you will learn how to create a custom form widget for your Django app.
LEVEL - 💻 💻 Intermediate to advanced - Basic knowledge of Django forms and templates is assumed.
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.
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:
AvatarFileUploadInput
which inherits from the ClearableFileInput class and replaces the 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>
{% include "django/forms/widgets/attrs.html" %}
- default attributes included with the widget. for example the accept="image/* and id=avatar attributeblur-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.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.
Related Articles
All ArticlesSuccess
Error
Warning
Info