Enable form widgets to be rendered with user customizable template engines.
- Rendering should be customizable. User's should be able to use Jinja2, DTL or any custom loader they wish. It should be easily configurable.
- It should be possible to override widget templates with application templates or templates in project template directories.
- Form rendering should be customizable per form class, instance, or render call.
- Avoid implicit coupling between
django.forms
anddjango.template
.
There are two sides to implementing this feature:
- Converting individual widgets to be rendered with templates.
- Deciding how to instantiate a user-customizable template engine.
Both will be addressed below.
Widgets are updated in the following manner:
Each widget defines a template_name
attribute. For example:
class TextInput(Input):
input_type = 'text'
template_name = 'djangoforms/text.html'
Widget.get_context
is added. This returns a dictionary
representation of the Widget
for the template context. By default, this
contains the following values:
def get_context(self, name, value, attrs=None):
context = {}
context['widget'] = {
'name': name,
'is_hidden': self.is_hidden,
'required': self.is_required,
'value': self.format_value(value),
'attrs': self.build_attrs(self.attrs, attrs),
'template_name': self.template_name,
}
return context
Widget
subclasses can override get_context
to provide additional
information to the template. For example, Input
elements can add the
input type to widget['type']
, and MultiWidget
and add it's subwidgets
to widget['subwidgets']
.
Widget.render()
is updated to render the specified template with the result
of get_context
. This uses the rendering API as defined below.
Add a high-level render class in django.forms.renderers
. The requirement
of this class is to define a render()
method that takes template_name
,
context
, and request
.
The default renderer provided by Django will look something like this:
class TemplateRenderer(object):
@cached_property
def engine(self):
if templates_configured():
return
return self.default_engine()
@staticmethod
def default_engine():
return Jinja2({
'APP_DIRS': False,
'DIRS': [ROOT],
'NAME': 'djangoforms',
'OPTIONS': {},
})
@property
def loader(self):
engine = self.engine
if engine is None:
return get_template
else:
return engine.get_template
def render(self, template_name, context, request=None):
template = self.loader(template_name)
return template.render(context, request=request).strip()
This class first checks if the project has defined a template loader with
APP_DIRS=True
and django.forms
in INSTALLED_APPS
. If so, that
engine is used. Otherwise, A default Jinja2 backend is instantiated. This
backend makes minimal assumptions and only loads templates from the
django.forms
directory.
Users can specify a custom loader by updating their TEMPLATES
setting:
django-floppyforms
is a 3rd-party package that enables template-based
widget rendering for DTL. The approach it takes doesn't work so well for Django,
though.
-
floppyforms
assumes aDjangoTemplates
backend is configured withAPP_DIRS
set toTrue
. It does not provide support forJinja2
. -
It's approach would create a framework-ey dependence in
django.forms.widgets
ondjango.template
. It is better if the render mechanism is explicitly passed into the widget. -
floppyforms
doesn't support the documented iteration API forBoundField
widgets. See RadioSelect for example.
First, Form
would be updated to be aware of the render class.
This can be specified explicitly in multiple ways:
# On the class definition
class MyForm(forms.Form):
default_renderer = TemplateRenderer()
# In Form.__init__
form = MyForm(renderer=CustomRenderer())
# Or as an argument to render:
form.render(renderer=CustomRenderer())
Second, Form
would instantiate a default renderer from settings if none
of the above is specified. This is explained further below in the settings
section.
BoundField.as_widget()
is updated to pass self.form.renderer
to Widget.render()
.
Since the render object is an opaque API, django.forms.widget
doesn't need
to know about the underlying template implementation.
BoundField.__iter__()
is updated to return BoundWidget
instances. These are like
Widget
instances but self-renderable.
The implementation looks roughly as follows:
@html_safe
@python_2_unicode_compatible
class BoundWidget(object):
"""
A container class used when iterating over widgets. This is useful for
widgets that have choices. For example, the following can be used in a
template:
{% for radio in myform.beatles %}
<label for="{{ radio.id_for_label }}">
{{ radio.choice_label }}
<span class="radio">{{ radio.tag }}</span>
</label>
{% endfor %}
"""
def __init__(self, parent_widget, data, renderer):
self.parent_widget = parent_widget
self.data = data
self.renderer = renderer
def __str__(self):
return self.tag(wrap_label=True)
def tag(self, wrap_label=False):
context = {
'widget': self.data,
'wrap_label': wrap_label,
}
return self.parent_widget._render(
self.template_name, context, self.renderer,
)
@property
def template_name(self):
if 'template_name' in self.data:
return self.data['template_name']
return self.parent_widget.template_name
@property
def id_for_label(self):
return 'id_%s_%s' % (self.data['name'], self.data['index'])
@property
def choice_label(self):
return self.data['label']
This approach simplifies the old iteration API classes, allowing us
to remove classes like ChoiceInput
, ChoiceFieldRenderer
, and
RendererMixin
.
Add Jinja2 and DTL templates for each built-in widget. These would live in
django/forms/templates
and django/forms/jinja2
.
It's not practical or backwards-compatible to require every form to specify
a renderer explicitly. Because of this, the Form
class create a default
renderer if none is specified. This would be controlled by a new setting:
FORM_RENDERER = 'django.forms.renderers.TemplateRenderer'
The renderer would be loaded by a cached function like so:
@lru_cache.lru_cache()
def get_default_renderer():
from django.conf import settings
return load_renderer(settings.FORM_RENDERER)
In general, this change will be backwards-compatible. 3rd-party widgets that
define a custom render
method will continue to work until they implement
template-based rendering, although they will eventually need to be updated to
accept the renderer
keyword argument.
Certain built-in widgets, like ClearableFileInput
and RadioSelect
, will
change enough that subclasses of these widgets will break if they depend on
the widget internals. I don't think this is very common.