One of my goals, beyond the constant improvement of the ORM in Django, is to make django.newforms easier for all of the lazy people, like myself, which I know are plentiful. I personally hate the repetitive task of creating templates for your forms, and the as_X methods are extremely limited. So let’s get down to business.
What’s Missing
Several things have tickets open, and several may even be about to go into trunk, but we’re going to ignore all those facts, so we can address the current issues (that I have).
- No field ordering.
- Using as_X outputs an entire form.
- There’s no way to do as_X on a specific field.
- Radio button and Checkbox widgets need to render differently than the rest.
- Class names need attached, as well as container names, to every row.
Now that we have addressed what the current issues are, let’s address how we can solve them.
Field Ordering
This is one of the few things that I know is currently being addressed. The general idea is that you have a Meta class on your form (as you do on Models and ModelForms) and you set a variable to provide field_ordering.
e.g.
class Form(forms.Form): class Meta: ordering = ('field_1', 'field_2')
This is ideal for me, and I don’t see any problems with the solution that will hopefully make it into trunk (although the field name may not be ordering). So let’s move on to the next issue.
Outputting Single Fields
The one thing I love about newforms, is that I can say myform.as_X which will output most of the form HTML for me so I don’t need to deal with displaying errors and other tedious html bits. The limitation though, is that a lot of times you have one or two fields that need to be broken off from that automatic render. Usually this happens because they have a lot of extra help text or there’s some kind of JavaScript interface.
A proposed solution to this would be to add render on the base widget class. This method would act a lot like the as_X methods do on the Form base class, except it would have 5 parameters: start_token, end_token, start_group_token, end_group_token, groups. An example of this would be:
def as_div(self): output = [] for f in self.fields: output.append(f.render(start_token='div', end_token='div')) def as_table(self): output = [] for f in self.fields: f.render(start_token='tr', end_token='</tr>', start_group_token='<td>', end_group_token='</td>', groups=(('%(label)s',), (%(field)s%(help_text)s%(errors)s',)),))
Let me first say, I haven’t completely thought this through. But the idea would be a generic handler that you could easily extend to your needs. To take it one step further, you could even remove variable interpolation and have a callback method, which would be far more flexible, but increase overhead.
If a solution like this was present, you could then add the ability to do form.field.as_p and output row by row until you hit the row you needed to customize.
Custom HTML Output
Now let’s talk about what we can do without support in Django for what we want to accomplish. The solution I created for a few projects I’ve been working on is a simple template filter. It works in the following syntax: form_instance|render_form. The filter includes three arguments, include_fields, exclude_fields, and ordering. This allows you to slice up your form anyway you could possibly need in your template, without having to write out any of that tedious HTML. The filter also adds some very useful class names and wrapper container names.
You can grab the source from PasteThat. Please note, the widget is built as a Jinja Filter as I don’t use Django’s template engine. If you convert the filter to Django please post it as a follow-up on PasteThat and throw up a comment here.
from django.newforms.forms import BoundField from django.template.defaultfilters import escape from django.utils.encoding import force_unicode from django.utils.safestring import mark_safe from django.contrib.jinja import register def render_form(include_fields=None, exclude_fields=None, ordering=None): def wrapped(env, context, form): normal_row = u'<div id="%(row_id)s" class="formRow %(class_names)s">%(label)s %(field)s%(help_text_wrapped)s%(errors)s</div>' inline_row = u'<div class="formRow %(class_names)s"><label>%(field)s %(help_text)s</label>%(errors)s</div>' error_row = u'<div class="formRow fInputErrorRow">%s</div>' class_prefix = 'f' row_ender = '</div>' top_errors = form.non_field_errors() # Errors that should be displayed above all fields. output, hidden_fields = [], [] if include_fields: if exclude_fields: _fields = ((name, field) for name, field in form.fields.iteritems() if name in include_fields and name not in exclude_fields) else: _fields = ((name, field) for name, field in form.fields.iteritems() if name in include_fields) elif exclude_fields: _fields = ((name, field) for name, field in form.fields.iteritems() if name not in exclude_fields) else: _fields = form.fields.iteritems() if ordering: _fields = tuple(_fields) _fields = (f for f in ordering if f in _fields) for name, field in _fields: bf = BoundField(form, field, name) bf_errors = form.error_class([escape(error) for error in bf.errors]) # Escape and cache in local variable. if bf.is_hidden: if bf_errors: top_errors.extend([u'(Hidden field %s) %s' % (name, force_unicode(e)) for e in bf_errors]) hidden_fields.append(unicode(bf)) else: if bf.label: label_text = escape(force_unicode(bf.label)) # Only add the suffix if the label does not end in # punctuation. if form.label_suffix: if label_text[-1] not in ':?.!': label_text += form.label_suffix else: label_text = '' params = { 'errors': force_unicode(bf_errors), 'row_id': 'id_%s_wrap' % (name), 'label': force_unicode(bf.label_tag(label_text) or ''), 'field': bf, 'help_text': force_unicode(field.help_text or ''), 'help_text_wrapped': field.help_text and '<small class="helptext">%s</small>' % field.help_text or '', } widget = unicode(bf.field.widget.__class__.__name__) class_names = [class_prefix + widget + 'Row'] if field.required: class_names.append(class_prefix + 'Required') if bf.errors: class_names.append(class_prefix + 'Errors') params['class_names'] = ' '.join(class_names) # XXX: Bad Hack if widget in ('CheckboxInput', 'RadioInput') and params['help_text']: output.append(inline_row % params) else: output.append(normal_row % params) if top_errors: output.insert(0, error_row % force_unicode(top_errors)) if hidden_fields: # Insert any hidden fields in the last row. str_hidden = u''.join(hidden_fields) if output: last_row = output[-1] # Chop off the trailing row_ender (e.g. '</td></tr>') and # insert the hidden fields. output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender else: # If there aren't any rows in the output, just append the # hidden fields. output.append(str_hidden) return mark_safe(u'\n'.join(output)) return wrapped register.filter(render_form, 'render_form')
In Closing
What I’d like to see is a very easy to use newforms rendering engine, for the frontend specifically. It’s something that you tend to spend a lot of time on for most user-submission based websites, and it’s something that really has room to improve. I personally will be creating more tools to interact with newforms, such as a basic AJAX library to handle some of the repetitive tasks (validation, auto completion, etc.), but beyond that the changes need to happen at the core.
</End of Rant>

6 Responses to "Making Django Newforms Useful"
Thank you. I am glad I got into django before there was an option. Makes things much simpler.
Best,
J
Really good.
Please, could you inform the dates you are planning to put these changes in trunk?
Thanks
Of these 5 points you listed, I think two (field ordering and rendering of individual fields) would be very nice to have for anyone who does any serious work with new forms.
I think you misunderstood Bosco. These change’s most likely won’t go into trunk, at least not by my hand.
Both newforms.forms.BaseForm._html_output and your custom rendering function (which I guess is based on _html_output) need some refactoring to be more simple and easy to understand.
Ajax integration with newforms will be really appreciated.
You can order the fields in Newforms. Just add “self.fields.keyOrder = ['field1', 'field2', 'field3']” to your form’s __init__ function:
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['field1', 'field2', 'field3']
Leave A Reply