15. Form Renderer

Since Django-4.0 each form can specify its own renderer. This is important, because it separates the representation layer from the logical layer of forms. And it allows us to render the same form for different CSS frameworks without modifying a single field. The only thing we have to do, is to replace the default form renderer with an alternative one.

15.1. Form Grid Example

Say, we have a Django form to ask for the recipient’s address, consisting of three fields: recipient, postal_code and city. Usually we prefer to keep the postal code and the destination city on the same row. When working with the Bootstrap framework, we therefore want to use the form grid for form layouts that require multiple columns, varied widths, and additional alignment options.

To properly render this form, we therefore have to add the CSS classes row and col-XX to the wrapping elements. One possibility is to create a template and style each field individually; this is the procedure described in Rendering a Django Form Field-by-Field. This however requires creating a template for each form, which contradicts the DRY-principle.

Rendering the form using our well known templatetag {% render_form form %} unfortunately does not work here, because we can not add different CSS classes to the three given fields. Using that templatetag only allows us to generically specify CSS classes for labels, field groups and input fields, but not on an individual level.

We therefore parametrize the provided renderer class. For each supported CSS framework, there is a specialized FormRenderer class. For Bootstrap, that class can be found at formset.renderers.bootstrap.FormRenderer. The form to be rendered, hence requires a parametrized renderer. Since django-formset renders forms using a different notation for field names, that form must additionally inherit from the special mixin formset.utils.FormMix. It would thus be written as:

forms.py
from django.forms import forms, fields
from formset.renderers.bootstrap import FormRenderer
from formset.utils import FormMixin

class AddressForm(FormMixin, forms.Form):
    default_renderer = FormRenderer(
        form_css_classes='row',
        field_css_classes={
            '*': 'mb-2 col-12',
            'postal_code': 'mb-2 col-4',
            'city': 'mb-2 col-8'
        },
    )

    recipient = fields.CharField(label="Recipient", max_length=100)
    postal_code = fields.CharField(label="Postal Code", max_length=8)
    city = fields.CharField(label="City", max_length=50)

Since that form now knows how to render itself, it does not require the templatetag render_form anymore. It instead can be rendered just by string expansion. The template to render that form hence simplifies down to:

<django-formset endpoint="{{ request.path }}" csrf-token="{{ csrf_token }}">
  {{ form }}
  <p class="mt-3">
    <button type="button" df-click="submit -> proceed" class="btn btn-primary">Submit</button>
    <button type="button" df-click="reset" class="ms-2 btn btn-warning">Reset to initial</button>
  </p>
</django-formset>

When rendered in a Bootstrap-5 environment, that form will look like

Here we pass a few CSS classes into the renderer. In form_css_classes we set the CSS class added to the <form> element itself. In field_css_classes we set the CSS classes for the field groups. If this is a string, the given CSS classes are applied to each field. If it is a dictionary, then we can apply those CSS classes to each field individually, by using the field’s name as a dictionary key. The key * stands for the fallback and its value is applied to all fields which are not explicitly listed in that dictionary.

15.2. Inline Form Example

By using slightly different parameters, a form can be rendered with labels and input fields side by side, rather than beneath each other. This can simply be achieved by replacing the form renderer using these parameters.

from formset.renderers.bootstrap import FormRenderer

class AddressForm(forms.Form):
    default_renderer = FormRenderer(
        field_css_classes='row mb-3',
        label_css_classes='col-sm-3',
        control_css_classes='col-sm-9',
    )

    # form fields as above

When rendered in a Bootstrap-5 environment, that form will look like:

In this example we don’t use any field specific CSS classes, therefore we can achieve the same effect by rendering this form using our well known templatetag render_form with these parameters:

<django-formset endpoint="{{ request.path }}" csrf-token="{{ csrf_token }}">
  {% render_form form "bootstrap" field_classes="row mb-3" label_classes="col-sm-3" control_classes="col-sm-9" %}
  <div class="offset-sm-3">
    <button type="button" df-click="submit -> proceed" class="btn btn-primary">Submit</button>
    <button type="button" df-click="reset" class="ms-2 btn btn-warning">Reset to initial</button>
  </div>
</django-formset>

15.3. Rendering Collections

When rendering form collections we have to specify at least one default renderer, otherwise all member forms will be rendered unstyled.

Say that we have a collection with two forms, one to ask for the users names, the other for its preferences.

from django.forms import fields, forms, widgets
from formset.collection import FormCollection
from formset.renderers.bootstrap import FormRenderer
from formset.views import FormCollectionView

class UserForm(forms.Form):
    legend = "Assigned License"
    first_name = fields.RegexField(
        r"^[A-Z][a-z -]+$",
        label="First name"
    )
    last_name = fields.CharField(
        label="Last name",
        min_length=2,
        max_length=50
    )

class PreferencesForm(forms.Form):
    eating = fields.ChoiceField(
        choices=[("🥗", "Vegan"), ("🧀", "Vegetarian"), ("🍗", "Carnivore")],
        widget=widgets.RadioSelect,
    )
    drinking = fields.MultipleChoiceField(
        choices=[
            ("🚰", "Water"), ("🥛", "Milk"),
            ("☕️", "Coffee"), ("🍵", "Tee"),
            ("🍺", "Beer"), ("🥃", "Whisky"),
            ("🥂", "White wine"), ("🍷", "Red wine"),
        ],
        widget=widgets.CheckboxSelectMultiple,
    )

class DoubleCollection(FormCollection):
    default_renderer = FormRenderer(field_css_classes='mb-3')
    user = UserForm()
    preferences = PreferencesForm()

class DoubleCollectionView(FormCollectionView):
    collection_class = DoubleCollection
    template_name = "form-collection.html"
    success_url = "/success"

These two forms are rendered using the Bootstrap FormRenderer class as defined through the argument default_renderer.

15.4. Using multiple Renderers

Sometimes using the same renderer for all form members does not produce the wanted results. We then can overwrite the default renderer on a per member class as shown in this example:

class AlternativeCollection(FormCollection):
    user = UserForm(renderer=FormRenderer(field_css_classes='mb-3'))
    preferences = PreferencesForm(
        renderer=FormRenderer(form_css_classes='row', field_css_classes='col'),
    )

Here instead of using a default form renderer for the collection AlternativeCollection, we pass individually configured renderers to each form member of that collection. This also works for collection members and can be applied to nested collections as well.