Django custom form fields and widgets
In table 6-2 you saw the wide variety of built-in Django form fields, from basic text and number types, to more specialized text types (e.g. CSV, pre-defined options), including file and directory types. But as extensive as these built-in form fields are, in certain circumstances it can be necessary to build custom form fields.
Similarly, in table 6-2 you
learned how all Django forms fields are linked to Django widgets
which define the HTML produced by a form field. While in previous
sections (e.g. listing 6-25 and listing 6-26) you learned how it's
possible to use a form field's widget
property to
override its default widget with custom properties (e.g. a CSS
class
attribute) or assign it a different widget
altogether (e.g. a forms.Textarea
widget to a
forms.CharField
field), in certain circumstances,
playing around with Django's built-in widgets (i.e. adding
attributes or switching one built-in widget for another) can be
insufficient for complex HTML form inputs.
Up next, you'll learn how to customize both Django form fields and widgets.
As you've learned throughout this chapter, there's a fuzzy relationship between a form field and a form widget, which can make it hard to determine which one to customize when built-in options become insufficient.
As a rule of thumb, if you need to constantly change a form field's data or validation logic, you should use a custom form field. If you need to constantly change a form field's HTML output (e.g. form tags, CSS classes, JavaScript events), you should use a custom widget.
Create custom form fields
The good news about creating
custom form fields is you don't have to write everything from
scratch, compared to other custom Django constructs (e.g. a custom template filter or custom context processor). Since Django's built-in form
fields are sub-classes that inherit their behavior from the
forms.Form
class, you can further sub-class a built-in
form field into a more specialized form field. Therefore, a custom
form field can start with a basic set of functionalities present in
a built-in form field, which you can then customize as
required.
For example, if you find yourself
creating forms with the built-in forms.FileField
and
constantly customizing it in a certain way (e.g. for certain file
sizes or types), you can create a custom form field (e.g.
PdfFileField
, MySpecialFileField
) that
inherits the behavior from forms.FileField
, customize
it and use the custom form field directly in your forms.
Listing 6-29 illustrates a custom
form field that inherits its behavior from the built-in
forms.ChoiceField
form field.
Listing 6-29. Django custom form field inherits behavior from forms.ChoiceField
class GenderField(forms.ChoiceField): def __init__(self, *args, **kwargs): super(GenderField, self).__init__(*args, **kwargs) self.error_messages = {"required":"Please select a gender, it's required"} self.choices = ((None,'Select gender'),('M','Male'),('F','Female'))
As you can see in listing 6-29,
the GenderField
class inherits its behavior from the
built-in forms.ChoiceField
field class, giving it
automatic access to the same behaviors and features as this latter
class. Next, the __init__
method -- used to initialize
instances of the class -- a call to super()
is made to
ensure the initialization process of the parent class (i.e.
forms.ChoiceField
) is made and immediately after
values are assigned to the error_messages
and
choices
fields.
The error_messages
and choices
fields in listing 6-29 might look familiar
because they're arguments typically used in the built-in
forms.ChoiceField
and are part of Django's standard
form field arguments described earlier in this chapter. You can
similarly add any other argument supported by form fields (e.g.
self.required
, self.widget
) so the custom
form field is created with behaviors set by form field
arguments.
Once you have a custom form field
like the one in listing 6-29, you can use it to declare a form
field in a Django form class (e.g. gender =
<pkg_location>.GenderField()
vs. gender =
forms.ChoiceField(error_messages={...},choices=...)
). As you
can see, custom form fields are a great choice if you're doing
repetitive customizations on built-in form fields.
Customize built-in widgets
In listing 6-25 and listing 6-26 you already explored some customizations associated with Django's built-in widgets. For example, in listing 6-25 you saw how it's possible to customize the default built-in widget assigned to form fields, while in listing 6-26 you saw how it's possible to add custom attributes to built-in widgets. In this section you'll learn how to globally customize built-in widgets.
Looking back at table 6-2, you
can see for example the forms.widget.TextInput()
widget produces an HTML output like <input type="text"
...>
and similarly all the other widgets in table 6-2 produce their own specific HTML output.
Given the high-expectations set forth by many front-end designs, producing this type of basic boilerplate HTML output can be a non-starter for a lot of Django projects. For example, if you want to tightly integrate JavaScript jQuery or ReactJS logic into HTML forms, customizing the default HTML produced by Django's built-in widgets can be a necessity. In these circumstances, the ideal approach is to produce custom HTML for Django's built-in widgets, forgoing the use of the default markup defined by Django in its out-of-the-box state.
The first thing you need to know
about customizing Django's built-in widgets is where Django keeps
its default built-in widgets. Django builds the HTML output for its
built-in widgets from the Django templates located inside the
django/forms/templates/django/forms/widgets/
directory
in the main Django distribution (e.g. if your Python installation
is located at
/python/coffeehouse/lib/python3.5/site-packages/
,
append this directory path to locate the widget templates)
If you look inside this last
directory, you'll find templates (e.g. input.html
,
radio.html
) for each built-in Django widget. Be aware
all these widgets templates don't use plain HTML, but instead use
Django template syntax (e.g. {% include %}
tags,
{% if %}
conditionals ) to favor code re-use. If
you're unfamiliar with Django template syntax, look over chapter three.
Now, while you could directly modify the templates in this location to alter the HTML output produced by each widget, don't do this. The recommended approach to customize the output for each built-in widget is to include custom built-in widgets on a project basis. Therefore the first step to customize Django's built-in widgets is to build custom built-in widgets and make them part of a Django project.
Tip Copy all the built-in widgets in the Django
distribution (i.e. the templates inside
django/forms/templates/django/forms/widgets/
) to your
project and modify them as needed.
Since built-in widgets are Django
templates, they need to be placed in a project directory where they
can be discovered. This means custom built-in widgets must be
placed in a project directory that's part of the DIRS
list declared in the TEMPLATES
variable in
settings.py
. In most cases, a project declares a
directory named templates
-- as part of
DIRS
-- that also contains a project's templates --
but you can use any directory so long as it's declared as part of
DIRS
-- see the section 'Template search paths' in
chapter three for more details on DIRS
.
Besides using a directory that's
part of the DIRS
list to locate widget templates,
Django also expects to locate custom built-in widgets in the same
path it uses for its default built-in widgets (i.e. those included
in the Django distribution).
Therefore, if you have a project
directory named templates
as part of the
DIRS
list, inside this templates
directory you'll need to create the same directory path
django/forms/widgets/
and inside this last
widgets
sub-directory place the custom built-in
widgets (e.g. to customize the built-in input.html
widget located in the Django distribution at
django/forms/templates/django/forms/widgets/input.html
you would create a project version at
<project_dir>/templates/django/forms/widgets/input.html
,
this way the latter input.html
template takes
precedence over the default built-in distribution template).
Once you have the custom built-in
widgets set up in your project, you need to make two configuration
changes to your project's settings.py
The first
configuration requires you add the FORM_RENDERER
variable, as follows:
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
By default, Django built-in
widgets are loaded through a standalone Django template renderer --
django.forms.renderers.DjangoTemplates
-- which is
unrelated to a project's main TEMPLATES
configuration
containing the DIRS
list with a project's templates.
Because you now placed custom built-in widgets inside a directory
declared as part of the DIRS
list, you need to
configure Django to use a project's main TEMPLATES
configuration as part of the form rendering process. By setting the
FORM_RENDERER
variable to
django.forms.renderers.TemplatesSetting
, Django also
inspects paths in the DIRS
list of the main
TEMPLATES
configuration for built-in widgets. The last
sub-section on custom widgets provides additional details on the
FORM_RENDERER
variable.
Finally, because you're
overriding the django.forms
package in a project to
get custom built-in widgets, you must also declare the
django.forms
package as part of the
INSTALLED_APPS
list in settings.py
.
Create custom form widgets
Custom form widgets are used for cases where you need to keep Django's built-in widget functionality as-is, but still need to customize the HTML output produced by widgets.
Tip Before creating custom form widgets, look closely at the built-in widgets in table 6-2 and the widgets in the sidebar 'Django hidden built-in widgets'. You may be able to find what you're looking for without the need to create custom widgets.
Similar to custom form fields,
custom form widgets have the advantage of being class-based and can
therefore inherit their behavior from built-in widgets. For
example, if you find yourself constantly customizing the built-in
forms.widgets.TextInput
widget in a certain way (e.g.
HTML attributes or JavaScript events), you can create a custom
widget (e.g. CustomerWidget
,
EmployeeWidget
) that inherits its behavior from
forms.widgets.TextInput
, customize it and use the
custom widget directly in form fields.
For example, let's say you're
constantly modifying Django text widgets to include the HTML 5
placeholder
attribute -- used to give end-users a hint
about the purpose of an input form field and which goes away when a
user focuses on a field. Listing 6-30 illustrates a custom form
widget which generates the HTML 5 placeholder
attribute based on the field name
attribute assigned
to a widget.
Listing 6-30. Django custom form widget inherits behavior from forms.widgets.Input
class PlaceholderInput(forms.widgets.Input): template_name = 'about/placeholder.html' input_type = 'text' def get_context(self, name, value, attrs): context = super(PlaceholderInput, self).get_context(name, value, attrs) context['widget']['attrs']['maxlength'] = 50 context['widget']['attrs']['placeholder'] = name.title() return context
Note Theforms.widgets.Input
widget is a more general purpose widget thanforms.widgets.TextInput
and does not include text input behaviors, hence it's often the preferred choice to build custom widgets due to its basic feature set. It's worth mentioningforms.widgets.Input
is the parent widget offorms.widgets.TextInput
,forms.widgets.NumberInput
,forms.widgets.EmailInput
and other input widgets described in table 6-2.
As you can see in listing 6-30,
the PlaceholderInput
class inherits its behavior from
the built-in forms.widgets.Input
widget class, giving
it access to the same behaviors and features as this latter
class.
Next, are two class fields. The
template_name
field defines the backing template for
the custom widget which points to
'about/placeholder.html'
-- note that if you omit the
template_name
field, the parent class template is used
(i.e. for the forms.widgets.Input
widget the template
is django/forms/widgets/input.html
). The
input_type
field is a requirement for
forms.widgets.Input
sub-classes and is used to assign
the HTML input type
attribute, values can include:
text
, number
, email
,
url
, password
or any other valid HTML
input type
value.
Inside the custom widget class is
the get_context()
method, which is used to set the
context for the backing widget template -- just like standard
Django templates. In this case, a call is made to
super()
to ensure the context for the parent widget
class template (i.e. forms.widgets.Input
) is set and
immediately after a pair of attributes -- maxlength
and placeholder
-- are set on the widget context in
the ['widget']['attrs']
dictionary for use inside the
widget template. Note the value for maxlength
is fixed
and the value for placeholder
is taken from the field
name
attribute and converted to a title with Python's
standard title
method. Finally, the
get_context()
method returns the updated
context
reference to pass it to the widget
template.
Now lets take a look at the
widget template 'about/placeholder.html'
in listing
6-31.
Listing 6-31. Django custom form widget inherits behavior from forms.widgets.Input
# about/placeholder.html <input type="{{ widget.type }}" name="{{ widget.name }}" {% if widget.value != None %} value="{{ widget.value }}" {% endif %} {% include "django/forms/widgets/attrs.html" %} /> # django/forms/widgets/attrs.html {% for name, value in widget.attrs.items %} {% if value is not False %} {{ name }} {% if value is not True %}="{{ value }}"{% endif %} {% endif %} {% endfor %}
You can see in listing 6-31 the
HTML input
tag is generated with values from the
widget
dictionary set through the
get_context
method. The values for
widget.type
, widget.name
and
widget.value
are all set behind the scenes in the
parent class (i.e. forms.widgets.Input
). In addition,
notice the HTML input
tag uses the built-in widget
template django/forms/widgets/attrs.htm
l shown at the
bottom of listing 6-31. This last widget template loops over all
the elements in the widgets.attrs
context dictionary
and generates the input
attributes. Since the
widgets.attrs
context dictionary is modified in
listing 6-30 to include the maxlength
and
placeholder
attributes, the input
tag is
generated with these additional attributes (e.g. <input
type="text" name="email" maxlength="50"
placeholder="Email">
).
Now let's take a step back to
discuss the location of the 'about/placeholder.html'
widget template. By default, Django custom widgets are only
searched for inside the templates
folders in all
Django apps defined in INSTALLED_APPS
. This means that
in order for Django to find the custom
'about/placeholder.html'
widget template, it must be
placed inside any project app's templates
folder (e.g.
given a project app named about
, the custom widget
should be located under
templates/about/placeholder.html
, where the
templates
folder is at the same level of an app's
models.py
and views.py
files). It's
possible to define custom widgets on a global directory, but I'll
discuss this in the last sub-section on custom widgets.
Finally, once you define the
custom widget in the right location, you can use it as part of a
Django form field (e.g.
email=forms.EmailField(widget=<pkg_location>.PlaceholderInput)
) to generate an HTML input
tag with a default
placeholder
attribute.
Custom form widget configuration options
In the previous two sections on customizing form widgets, I didn't mention a variety of configuration options in order to avoid getting sidetracked from the main task at hand. But now that you know how to customize Django's built-in widgets and how to create custom form widgets, I can provide you with these additional details.
The first topic is related to
finding and loading custom widget templates. Django defines a form
renderer through the FORM_RENDERER
variable in
settings.py
. Table 6-4 describes the three different
values supported by the FORM_RENDERER
variable.
Table 6-4 FORM_RENDERER values that influence finding and loading custom widget templates
Form renderer class | Description |
---|---|
django.forms.renderers.DjangoTemplates (Default) | Searches and loads widget templates from the built-in django/forms/templates/ directory (i.e. the distribution). Searches and loads widget templates from all the templates directories inside apps declared in INSTALLED_APPS. |
django.forms.renderers.JinjaTemplates | Searches and loads widget templates from the built-in django/forms/jinja2/ directory (i.e. the distribution). Searches and loads widget templates from all the jinja2 directories inside apps declared in INSTALLED_APPS. |
django.forms.renderers.TemplatesSetting | Searches and loads widget templates based on a project's TEMPLATES configuration (e.g. its DIRS values). NOTE: This renderer requires you declare the django.forms package as part of INSTALLED_APPS |
As you can see in table 6-4, there's even a Jinja template renderer to allow you to customize widgets backed by Jinja templates, in addition to other renderers used in the previous sections.
In listing 6-30 you learned how
custom widget classes use fields (e.g. template_name
,
input_type
) to specify certain behaviors. Fields in
widgets classes are highly dependent on the parent widget class.
For example, although template_name
is valid for all
built-in widgets, more specialized built-in widgets can accept
additional fields. If in doubt, consult the fields supported for
the built-in widget[4] used as a widget's parent
class.
In listing 6-30 you also learned
how to access and modify a widget's template context through the
get_context()
method. Although in listing 6-30 you
only added a couple of widget attributes to the
context['widget']['attrs']
dictionary, the parent
context['widget']
dictionary is a large data structure
that stores all data associated with a widget, where you can
inclusively update a widget's field values. The following snippet
illustrates the contents of the context['widget']
for
a form field using the PlaceholderInput
widget class
from listing 6-30:
{'widget': {'attrs': {'placeholder': 'Email', 'maxlength': 50}, 'name': 'email', 'is_hidden': False, 'type': 'text', 'value': None, 'template_name': 'about/placeholder.html', 'required': True } }
As you can see, in addition to
the HTML input attributes stored under the attrs
key,
there are other widget
field keys (e.g.
name
, is_hidden
) that are made available
in a template to render the final output. This also means there's
nothing limiting you from adding custom data keys to the
context['widget']
dictionary inside the
get_context()
method to integrate them as part of the
final template layout (e.g.
'react':{<react_data>}
,
'jquery':{<jquery_data>}).