11. Form Renderers

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.

11.1. Form Grid Example

Say, we have a form to ask for the recipient’s address:

from django.forms import forms, fields

class AddressForm(forms.Form):
    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,
    )

this form, typically be rendered using a template such as

{% load formsetify %}

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

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. We therefore have to add the CSS classes row and col-XX to the wrapping elements, while rendering the form. One possibility would be to create a template and style each field individually; this is the method described in Rendering a Django Form Field-by-Field. This however requires creating a template for each form, which contradicts the DRY-principle.

We therefore parametrize the provided renderer class. For each supported CSS framework, there is a Form Renderer. For Bootstrap, that class can be found at formset.renderers.bootstrap.FormRenderer.

We now can add that renderer to the above form class and parametrize it as follows

from formset.renderers.bootstrap import FormRenderer

class AddressForm(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'},
    )

    # form fields as above

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

Address Form

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.

11.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

Inlined Form

The same effect can be achieved by rendering this form, parametrizing our well known templatetag:

<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" click="submit -> proceed" class="btn btn-primary">Submit</button>
    <button type="button" click="reset" class="ms-2 btn btn-warning">Reset to initial</button>
  </div>
</django-formset>