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.