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.formsanddjango.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 contextWidget 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.
-
floppyformsassumes aDjangoTemplatesbackend is configured withAPP_DIRSset toTrue. It does not provide support forJinja2. -
It's approach would create a framework-ey dependence in
django.forms.widgetsondjango.template. It is better if the render mechanism is explicitly passed into the widget. -
floppyformsdoesn't support the documented iteration API forBoundFieldwidgets. 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.