Hi all,

I'd like to propose a few extensions to Django's form library for 1.3.
I'm still working on some fine details, but before I get too far, I'd
like to field opinions so that I can:

 * Discover any edge cases I've missed in my analysis
 * Field any criticisms from people with more design/frontend
experience than myself
 * Determine any related problems that we have the opportunity to
solve at the same time
 * Find out if there is anyone in the community who is interested in
helping out.

Apologies in advance for the length, but there's a lot of detail to cover.

With this proposal, I'd like to address three problems:

 1. The layout problem. Django's forms can be rendered "as_ul",
"as_table" or "as_p", and that's it. These layout schemes can be
overridden and customized if you know what you're doing, but it's not
easy to do so. Furthermore, visual layout concerns aren't separated
from data processing concerns. You need to write (and install) a form
subclass to implement your own form layout. Although it's good
app-writing practice to ensure that forms can be easily substituted,
it's not an enforced or universal practice.

 2. The widget problem. This is a variant on the previous point. A
designer that wants to use a specialized calendar widget for a date
field needs to modify form code. This is a complexity that shouldn't
exist; a designer should be able to specify the widget library that
needs to be used (with all it's required rendering requirements,
javascript triggers etc) without modifying views and form processing
code.

 3. The DOCTYPE problem. Most importantly, there is the closing slash
problem, but the introduction of HTML5 also means that there are
richer input types like <input type="email"> that aren't available in
HTML4 or XHTML1. Django currently outputs XHTML1 unconditionally, and
has no support for the new HTML5 input types.

To solve these three problems, I'd like to propose that we add (and
promote) the use of a new approach to form rendering, based around the
use of a new {% form %} template tag. This proposal has some
similarities to a proposal made by in the 1.2 feature phase [1] -- but
that proposal was only aiming to solve the doctype issue.

[1] 
http://groups.google.com/group/django-developers/browse_thread/thread/bbf75f0eeaf9fa64

So: What I'm proposing is that we introduce a new template tag: {% form %}.

How does this solve the three problems?

Layout
------

The simplest approach for rendering a form would become:

{% form myform %}

This would effectively implement the as_table rendering strategy, just
as {{ myform }} does right now.

If we want a different rendering, we exploit the fact that {% load
%}ing a template library will override any template tags that are
redefined. {% form %} would be defined as part of the default template
tag library, implementing the 'as_table' strategy. However, if we load
a library that also defines the {% form %} tag, that definition will
override the base definition. If we want to use a custom rendering
style, we can get that by simply loading a different renderer that
implements that style:

{% load custom_renderer %}
{% form myform %}

Django would ship with {% form %} implementations of the 'as_p' and
'as_ul' strategies, so getting 'as_p' rendering would mean:

{% load xhtml_p_forms %}
{% form myform %}

{% form %} is just a template tag, but the default implementation
would be designed in such a way that it could be easily subclassed to
alter the rendering strategy for the form. I'm still tinkering with
details here, but broadly, the intention is to expose a similar
interface to that used by Form.as_*()  -- that is, returning format
strings that specify like '<td>%(errors)s%(field)s%(help_text)s</td>'
to define the rendering strategy; this would be implemented as a
function so that forms could decide on a per field basis what output
format is appropriate. However, unlike the existing as_*() approach,
you don't need to have access to the form in order to use the
different renderer, which means you can define and apply your
rendering strategy independent of the view and form code.

Since the form renderer exposes the logic for rendering individual
form rows, we can also expose the ability to render individual form
fields, plus the non-field errors and hidden fields:

{% form myform errors %}  -- All the non-field form errors, plus
hidden field errors
{% form myform field birthdate %} - output a full row for the
birthdate field (wrappers, label, errors, help etc)
{% form myform hidden %} -- output all the hidden fields

This just exposes the internal mechanics that makes the full-form
rendering of {% form myform %} possible.

Widgets
-------

The second problem is support for widgets and other rendering
customization. This can be addressed using extra arguments to the {%
form %} tag when rendering individual fields:

{% form myform field birthdate using calendar %}

This instructs the rendering of the birthday DateField to use the
'calendar' chrome.

What is chrome? Chrome is an attempt to overcome the practical
limitations of Django's Widgets.

When we introduced newforms, the intention was that widgets would be
the point at which form rendering would be customized. If a developer
wanted to use a rich Javascript rendering for the calendar, they would
define a custom Date widget, override the render() method to introduce
the appropriate Javascript and CSS hooks, and then define a form with
fields that specify the use of that widget. The way admin uses widgets
is probably the best example of how this was intended to work.

However, in practice, widgets aren't used like this. Widgets aren't
trivial to define, and they aren't easy to deploy, either. The
convoluted mechanics that ModelAdmin goes through in order to install
a custom calendar widget is probably the best demonstration of why
this idea hasn't taken off -- it's just too complex.

It's also not simple to "just use a different widget", either. The
interplay between form and widget is sufficiently complex that the
only time at which you can define the widget that is to be used is
when you define the field itself. This means that widget choice is
closely bound to form design, which makes it nigh impossible define a
scheme for deploying widgets as part of the template rendering layer.

So -- the idea behind chrome is to separate the raw HTML input
mechanism from the bits that make the input look good on the rendered
page.

Widgets remain as they are, but we de-emphasize their use as a
rendering customization tool. They become little more than the
decision over which HTML <input> (or <select>) will be used. It's
still important that widgets are distinct from fields. After all,
there are times when there are multiple input options for particular
field data -- consider the case of a location field, that will require
a custom lat/long split widget.

It's the job of the chrome to make the <input> provided by the Widget
look pretty. This might mean adding extra classes to the rendering of
the <input>; it might mean putting placeholder <div>s or other
elements around the input; or it might mean registering that a
javascript block is required to support that input.

Chrome can also be layered. Different pieces of chrome could perform
different roles, and could be applied one after he other. For example
in the following:

{% form myform field birthdate using calendar important %}

The "calendar" chrome might add the javascript and classes to enable
the rich widget, where the "important" chrome might just add CSS
classes that enables a particular field to be rendered in a particular
style.

Chrome can also be parameterized; for example:

{% form myform field name using autocomplete:"name_autocomplete" %}

might define a chrome that implements an Ajax autocomplete widget
using the named URL "name_autocomplete" as a data source. This has to
potential to start giving an answer to the "Why doesn't Django do
AJAX" monkey; Django won't provide an AJAX solution out of the box,
but packaging a chrome that implements AJAX should be a lot easier.

Chrome could also be programatically assigned as part of the form.
Using a formfield_callback() style setup, it should be possible to
automatically assign certain chromes to certain field types (e.g.,
always apply the calendar chrome to DateFields).

I also hope to be able to provide a solution for the problem of
putting all the javascript for a form at the bottom of the page. At
present, widget rendering allows you to insert a <script> tag
immediately before or after a form element, but best practice says it
should be contained in a single block at the end of your page.

By rendering a widget using a template tag, we get the ability to set
variables in the context. When a form field is rendered using a
particular piece of chrome, that chrome can register that it requires
a specific javascript trigger; a {% form triggers %} call can then be
used to retrieve all the triggers that have been registered, and embed
them in a <script> call at the end of the page.

Again, I'm still working on details here, but the intention is to make
chrome easy to define -- hopefully just a class with some fairly
simple entry points.

Doctypes
--------

The third problem is doctype support. To solve this, I propose two changes.

 1) We introduce a 'doctype' argument to the render() method on
widgets. Widgets would then be expected to generate HTML output that
is compatible with the supplied doctype. For backwards compatibility,
internal calls to widget.render() would need to introspect for the
existence of the 'doctype' kwarg on the specific widget, and raise
warnings if the kwargs isn't available.

 2) We introduce a number of new default widgets to support the new
HTML5 input types. At present, EmailField uses a TextInput by default.
Under the new scheme, we would introduce an EmailInput, and EmailField
would use EmailInput as the default widget. When rendered using the
HTML4/XHTML1 doctype, this would output <input type="text" ...; under
HTML5, this would output <input type="email".... Again, the doctype
argument to render() would be used to determine which input type is
used.

Once these two changes are in place, we use the form template tag
specify the doctype that is passed to the widget render call. A
'html5_p_forms' library will pass 'html5' as the doctype when
rendering fields, and get HTML5-compliant form fields; the
'xhml1_p_forms' library will pass 'xhtml1', and get XHMTL1-compliant
form fields.

Summary
-------

In summary, I'm proposing:
 - A new {% form %} template tag with a fairly simple interface for overriding
 - A couple of default implementations of the {% form %} template tag
 - Adding a chrome layer
 - Propagating the 'doctype' kwarg through the existing
form/field/widget infrastructure

It's important to note that all of this is backwards compatible. The
old {{ form }} and  {{ form.as_p }} approaches to rendering will
continue to work, as will the longhand {{ field.errors}} {{
field.label }} {{ field }} approach. What I'm proposing is a layer on
top of the primitives Django already provides. Hopefully, this new
layer will be sufficient to make the common use cases for form
customization slightly more automated, while providing hooks that
allow designers to customize the way forms are rendered without
needing to delve into view/form internals.

I appreciate that some of this is hard to judge without specifics --
in particular, the specifics of how end users will customize the Form
template tag rendering and specify chrome. I really wanted to propose
this with a sample implementation, but:

 1) I accidentally let the cat out of the bag on Twitter,
 2) I wasn't sure I was going to have a implementation ready for
comment before we draw the curtain on the 1.3 feature list,
 3) I wanted to get some feedback on the broad design goals before I
went too far down a rabbit hole
 4) I wanted to work out if there was any interest in the community in
helping out to achieve these goals

Comments? Criticisms? Queries?

Yours,
Russ Magee %-)

-- 
You received this message because you are subscribed to the Google Groups 
"Django developers" group.
To post to this group, send email to django-develop...@googlegroups.com.
To unsubscribe from this group, send email to 
django-developers+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/django-developers?hl=en.

Reply via email to