Personal blog of Miguel Araujo

New kung-fu in django-uni-form 0.8.0

This won’t be a new official release announcement as Daniel already did the honors. I’m now the co-lead developer of django-uni-form and in this post I will try to go over some examples that teach you new kung-fu techniques. But before doing so, let me emphasize how cool this project is and how many new nice features are packed in the new version 0.8.0. If you like DRY principles and don’t think your Django forms are DRY enough using built-in filters, you should check it! if you haven’t yet.

To explain some of the new features, let’s use a real form and a real example, so it’s easier to follow how django-uni-form works and what has been enhanced.

The following form will be used from now onwards to exemplify the new features in django-uni-form. I’m using different types of form fields to demonstrate how django-uni-form can work with any built-in widget or form field or any other custom field that you have defined. That is, django-uni-form plugs well with other form apps.

from django import forms

class ArticleFeedbackForm(forms.Form):
    like_example = forms.TypedChoiceField(
        label = _("¿Do you like this article?"),
        choices = ((1, "Yes"), (0, "No")),
        coerce = lambda x: bool(int(x)),
        widget = forms.RadioSelect,
        initial = '1',
        required = True,
    )

    reason = forms.CharField(
        label = "Why did you like it?",
        max_length = 80,
        required = True,
    )
 
    rating = forms.IntegerField(
        label = "Rating (from 0 to 10)",
        min_value = 0,
        max_value = 10,
        required = False,
    )

    notes = forms.CharField(
        label = "Additional notes or feedback",
        max_length = 200,
        widget = forms.Textarea,
        required = False,
    )

To load django-uni-form template tags in our template we will have to add:

{% load uni_form_tags %}

Django-uni-form still has |as_uni_form filter, which is used exactly the same way. The only difference is that it now supports rendering formsets. This is how you would use it, supposing we have the form in the template context as example_feedback_form:

<form ...>
    {{ example_feedback_form|as_uni_form }}
</form>

In my opinion, the real power in django-uni-form comes from its {% uni_form %} tag and being able to set layouts. But let’s go step by step to avoid confusion.

Django-uni-form implements a class called FormHelper which is attached to the form. This class defines the form rendering behavior.

Let’s add to our form an instance-level form helper variable:

from django import forms

from uni_form.helpers import FormHelper

class ExampleFeedbackForm(forms.Form):
    like_example = forms.TypedChoiceField(
        label = _("¿Do you like this example?"),
        choices = ((1, "Yes"), (0, "No")),
        coerce = lambda x: bool(int(x)),
        widget = forms.RadioSelect,
        initial = '1',
        required = True,
    )

    reason = forms.CharField(
        label = "Why did you like it?",
        max_length = 80,
        required = True,
    )
 
    rating = forms.IntegerField(
        label = "Rating (from 0 to 10)",
        min_value = 0,
        max_value = 10,
        required = False,
    )

    notes = forms.CharField(
        label = "Additional notes or feedback",
        max_length = 200,
        widget = forms.Textarea,
        required = False,
    )

    def __init__(self, *args, **kwargs):
        self.helper = FormHelper()
        self.helper.form_id = 'example_feedback_form'
        return super(ExampleFeedbackForm, self).__init__(*args, **kwargs)

Now that we have a helper, we can use helper methods to set how the form should render. In this case I’ve just set the form DOM id to ‘example_feedback_form’.

New FormHelper attributes

Version 0.8.0 introduces new attributes that can be set in a helper:

  • form_error_title: If you are rendering a form using {% uni_form %} tag and it has non_field_errors to display, they are rendered in a div. You can set the title of the div with this attribute. Example: “Form Errors”.
  • formset_error_title: If you are rendering a formset using {% uni_form %} tag and it has non_form_errors to display, they are rendered in a div. You can set the title of the div with this attribute. Example: “Formset Errors”.
  • form_style: If you are using uni-form CSS, it has two different form styles built-in. You can choose which one to use, setting this variable to “default” or “inline”.

Creating a Layout

Django-uni-form defines another powerful class called Layout. You can create your Layout to define how the form fields should be rendered. With “how” I mean things like the order of the fields, wrap them in divs or other html structures with ids or classes set to whatever you want, add things in between, etc. And all that without writing a custom template, rather fully reusable without writing it twice.

A Layout is constructed by what I like to call layout objects, which can be thought of as form components. You assemble your layout using those. For the time being, your choices are: Button, ButtonHolder, Column, Fieldset, HTML, Hidden, MultiField, Reset, Row and Submit.

All these components are explained in helper API docs. What you need to know now about them is that every component renders a different template. Let’s write a couple of different layouts for our form, continuing with our form class example (note that I’m not showing the full form again):

from uni_form.helpers import Fieldset, Submit

(...)

def __init__(self, *args, **kwargs):
    self.helper = FormHelper()
    self.helper.form_id = 'example_feedback_form'
    layout = Layout(
        Fieldset('The first arg is the legend of the fieldset', 
            'like_example',
            'reason',
            'rating',
            'notes',
        )
    )
    self.helper.add_layout(layout)
    self.helper.add_input(Submit('register', 'Register'))
    return super(ExampleFeedbackForm, self).__init__(*args, **kwargs)

Now if we want to render our form example_feedback_form using what we’ve set in the helper and our layout, we would do in the template:

{% uni_form example_feedback_form example_feedback_form.helper %}

This is exactly the form that we would get rendered using the previous tag:

<form action="." class="uniForm" method="post" id="example_feedback_form">
    <fieldset>
        <legend>The first arg is the legend of the fieldset</legend>
        <div id="div_id_like_example" class="ctrlHolder ">
            <label for="id_like_example" class="requiredField">
                ¿Do you like this article?<span class="asteriskField">*</span>
            </label>
            <ul>
                <li><label for="id_like_example_0"><input checked="checked" name="like_example" value="1" id="id_like_example_0" type="radio" class="radioselect" /> Yes</label></li>
                <li><label for="id_like_example_1"><input value="0" type="radio" class="radioselect" name="like_example" id="id_like_example_1" /> No</label></li>
            </ul>    
        </div>

        <div id="div_id_reason" class="ctrlHolder ">        
            <label for="id_reason" class="requiredField">
                Why did you like it?<span class="asteriskField">*</span>
            </label>
            <input id="id_reason" class="textinput textInput" type="text" name="reason" maxlength="80" />
        </div>

        <div id="div_id_rating" class="ctrlHolder ">        
            <label for="id_rating" >
                Rating (from 0 to 10)
            </label>
            <input id="id_rating" type="text" name="rating" class="textinput textInput" />
        </div>

        <div id="div_id_notes" class="ctrlHolder ">
            <label for="id_notes" >
                Additional notes or feedback
            </label>
            <textarea id="id_notes" class="textarea" rows="10" cols="40" name="notes"></textarea>
        </div>
    </fieldset>
    
    <div class="buttonHolder">
        <input type="submit" name="register" value="Register" class="submit submitButton" id="submit-id-register"/> 
    </div>
</form>

Note that every field is wrapped by default in a div with an id. This makes it very easy to style any field using CSS.

Buttons are now Layout objects

Now, I will show another layout example that would go in the same place shown before:

from uni_form.helpers import Fieldset, Submit, Row, ButtonHolder, HTML

(...)

Layout(
    Row(
    HTML("""<p>Did you know that some layout objects 
        can hold others within?</p>""")
    'like_example',
        'reason',
    ),
    MultiField("Additional information",
        'rating',
        'notes',
    ),
    ButtonHolder(
        HTML("""
            {% if message %}
                <span class="notification">{{ message }}</span>
            {% endif %}
        """),
        Submit('save', 'Save'),
    )
)

You can wrap fields in divs, like Row. MultiField is a layout object that comes from a uni-form adaptation: instead of rendering errors surrounding inputs in boxes, it renders them in a list and boxes all inputs affected. Sometimes the best way to familiarize yourself with these layout objects is getting your hands dirty writing and rendering some layouts to see what they do. They are harder to explain than to understand once you see them working.

Note that HTML is a layout object that can be used to input arbitrary HTML code in the layout.

In this version of django-uni-form you can have buttons (Button, Hidden, Reset and Submit) as layout objects. The recommended way of doing so is having them wrapped in a ButtonHolder which renders to a <div class="buttonHolder">, that is, a div that uni-form CSS magically knows how to position.

Django-uni-form is more context-aware

In the last example, you’ve seen how HTML can now be part of a Django template while being fully context-aware, which means that it’s got access to the full context of the template in which it’s being rendered. This gives you some really nice extra power that I find very useful.

However, not only HTML is context-aware. Fieldset legends behave the same way:

Fieldset("Please fill in your data {{ user.username }}",
    'like_example', [...]
)

Setting DOM ids and classes in Layout objects

Now you can set DOM ids and classes in every layout object, using css_id and css_class. Let’s see a couple of examples:

Submit('Save', 'Guardar', css_class='button white')

Fieldset("legend", 
    'field1', 
    'field2', 
    css_id="iLoveThisFieldset", 
    css_class="fieldsets_that_rock"
)

Also, the ids and classes are left untouched; they are not slugified anymore so you can use camelCase or whatever you want.

Formsets support

Now you can render formsets using django-uni-form. Let’s see how:

from django.forms.models import formset_factory

ExampleFeedbackFormSet = formset_factory(ExampleFeedbackForm, extra = 3)
formset = ExampleFeedbackFormSet()

You can use {% uni_form %} tag the same way as with a form:

{% uni_form formset formset.form.helper %}

Note that you can still use a helper (in this case we are using the helper of the form used for building the formset). The main difference here is that helper attributes are applied to the form structure, while the layout is applied to the formset’s forms. Rendering formsets injects some extra context in the layout rendering so that you can do things like:

HTML("{% if forloop.first %}Message displayed only in the first form of a formset forms list{% endif %}",

Fielset("Item {{ forloop.counter }}", 'field-1', [...])

Django-uni-form fails silently

When django-uni-form encounters errors it failed in a very intrusive way. Now, by default, it will log errors and continue working if possible. A settings variable called UNIFORM_FAIL_SILENTLY has been added so that you can control this behavior. If you want django-uni-form to raise errors instead of logging, telling you what’s going on when you are developing in debug mode, you can set it to:

UNIFORM_FAIL_SILENTLY = not DEBUG

Help texts are wrapped in divs

This means that in your form fields you can set your help_text to things like:

help_text = "<ul><li>First help note</li><li>Second help note<li></ul>"

Layouts and Layout objects are now dynamic

The first steps have been taken to make adapting a Layout or a Layout object on the go possible, so that django-uni-form plays nice with dynamic form generation. Version 0.8.1 will hopefully come with an API that makes handling this a breeze.

Conclusions

Django-uni-form is in active development. We are aiming to build the DRY-est form building solution for Django. It comes with better docs in RTD, better error messages, a new full testing suite with high coverage and new code carefully cleaned and optimized for performance.

We are already working on version 0.8.1 which will be released in less than a month from now, packed with new killer features.

Special thanks go to Javier Maestro for reviewing my writing in this article.

blog comments powered by Disqus