3. Working with a single Form

In Django we typically assign a single form to a FormView class. Requests arriving with method GET will create an empty or prefilled form instance and render it using the template specified in the view class. This view class then is connected to our URL router:

urls.py
from formset.views import FormView

from myproject.forms import RegisterPersonForm

urlpatterns = [
    ...
    path('register_person', FormView.as_view(
        form_class=RegisterPersonForm,
        template_name='native-form.html',
        success_url=success_url,
    )),
    ...
]

When we navigate to the given URL, our form will be rendered by the class FormView. Until here, there is no difference on how Django renders a form. In case your project already defined a proprietary class inheriting from FormView which can not be refactored, django-formset provides a special mixin class named formset.views.FormViewMixin to be inherited by that view.

The difference to a classic Django form appears when the view receives data sent by a POST request. First of all, received data now is encoded as application/json, instead of multipart/form-data, as with standard form submissions. And secondly, the response of that processed view is neither an HTTP redirect nor a HTML page, but just a data object, again encoded in JSON. If that form validates successfully, that response object just contains the success URL with a status code of 200. On the other hand, if the form does not validate, then that response object contains the error messages of the fields submitting invalid data, indexed by their field names. The status code of such a response then is 422, which stands for “Unprocessable Entity”. Having the server respond with a status code indicating an error, makes it easier for the client to distinguish between validated and invalid forms.

For invalid forms, the client’s JavaScript code handling the web component <django-formset>, then fills the placeholders near the invalidated input fields with those error messages. It also puts the HTML form element into an invalid state, so that it can not be re-submitted before editing. The response on validated forms can be used to update the database or do other processing before telling the browser where to go next. Instead of sending a HTTP redirect, the server now sends the success URL wrapped in a JavaScript object. When receiving this response, the browser just loads the page with that URL. This also prevents users to accidentally submit the form data twice, in case they click on the browser’s reload button.

A Django form using django-formset can be rendered using three different methods:

3.1. Using a Native Django Form

Working with a native Django form, presumably is the most frequent use case. Here we add an instantiation of that form to the rendering context. Then that form instance is rendered using the special template tag render_form. The template responsible for rendering shall be written as:

{% load render_form from formsetify %}

<django-formset endpoint="{{ request.path }}" csrf-token="{{ csrf_token }}">
  {% render_form form field_classes="mb-2" form_classes="rounded-xl" %}
  <button type="button" click="submit -> proceed">Submit</button>
</django-formset>

What we see here is that in addition to the form object, we pass a few CSS classes to the renderer. These are named field_classes and form_classes. Let’s explain how we can use them to style our form. When rendered, the above form will roughly turn into HTML such as:

<django-formset endpoint="/path/to/form-view" csrf-token="{{ csrf_token }}">
  <form id="id_registerpersonform"></form>
  <div class="rounded-xl dj-form">
    <div class="dj-form-errors"><ul class="dj-errorlist"></ul></div>
    <django-field-group class="mb-5 dj-required">
      <label class="formset-label">First name:</label>
      <input class="formset-text-input" form="id_registerpersonform" type="text" name="first_name" required="" pattern="^[A-Z][a-z -]+$">
      <div role="alert" class="dj-field-errors">
        <django-error-messages value_missing="This field is required." type_mismatch="A first name must start in upper case." pattern_mismatch="A first name must start in upper case." bad_input="Null characters are not allowed."></django-error-messages>
        <ul class="dj-errorlist"><li class="dj-placeholder"></li></ul>
      </div>
    </django-field-group>
    <!-- other form fields snipped away -->
  </div>
  <button type="button" click="submit -> proceed">Submit</button>
</django-formset>

Compared to the way the native Django form renderer works, we see a few differences here: The most obvious one is that input fields are not wrapped into their <form>-element. Instead they refer to the form they belong to by ID using the attribute form="id_registerpersonform". This is so that forms can logically be nested into each other. Remember: It is invalid HTML to nest one <form>-element into another one, but using that trick we can mimic that behviour.

Also note that each input field is wrapped into a <django-field-group>-element. Even though this tag may look like another web component, it just is a non-visual HTML element. Its purpose is to group one or more input elements (in Django we name them widgets) belonging to one field together. Remember that in HTML radios and multiple checkboxes have more than one input element, but in Django they are considered as a single form field.

Moreover, CSS frameworks such as Bootstrap require to group the label and their input fields into one HTML element, typically a <div>. This is what the <django-field-group> does, in addition to grouping the input elements. When adding the parameter field_classes="mb-5" to the templatetag render_form, that CSS class is added to each instance of the group, ie. it then is rendered as <django-field-group class="mb-5">.

Another unknown HTML-element in the rendered form is <django-error-messages ...>. This element simply keeps all the potential error messages, in case a field validation fails on the client. Remember that HTML5 introduced a bunch of form controls which are mapped to their Django counterparts. If, for instance, the pattern of an input field of type="text" does not match the specified regular expression, then the text from attribute type_mismatch is shown as an error below that field.

3.2. Using an Extended Django Form

One of the tasks the templatetag render_form must do, is to modify the signature of the given form class. This is required, because the layout of the rendered HTML differs substantially from the default by the Django form field renderers. Sometimes however, we may prefer to render the complete form instance using its built-in __str__()-method. In this use case, our form class has to additionally inherit from formset.utils.FormMixin. Such a form could for instance be defined as:

from django.forms import forms, fields
from formset.utils import FormMixin

class RegisterPersonForm(FormMixin, forms.Form):
    first_field = ...

The template required to render such a form then shall look like:

<django-formset endpoint="{{ request.path }}" csrf-token="{{ csrf_token }}">
  {{ form }}
  <button type="button" click="submit -> proceed">Submit</button>
</django-formset>

Let’s discuss these lines of HTML code step by step:

Since the JavaScript implementing web component <django-formset> communicates via Ajax with the server, having a hidden field containing the CSRF-token doesn’t make sense. Instead we pass that token value as an attribute to the web component <django-formset>. Since that value is available in the rendering context, we always add it as <django-formset csrf-token="{{ csrf_token }}">.

Having set up the form’s template this way, allows us to render the form instance as a string. This is what {{ form }} does. On the first sight, this may seem more cumbersome than the solution shown before when Using a Native Django Form. In some situations however, it might be simpler to change the signature of the form class in Python code, rather than changing the code in the template.

Another use case would be to have a form with more than one renderer, each configured differently. We then can reuse a Django template but for instance render the form with different CSS classes. If such a setup might make sense in your project, please check the section about Form Renderers.

3.3. Rendering a Django Form Field-by-Field

In some occasions, we need an even more fine grained control over how fields shall be rendered. Here we iterate over the form fields in our own loop or by accessing the fields by name. This way we can render field by field and depending on the field’s name or type, we could render it in different manners. Let’s have a look at such a template:

{% load formsetify %}
...
{% formsetify form %}
<django-formset endpoint="{{ request.path }}" csrf-token="{{ csrf_token }}">
  <form id="{{ form.form_id }}"></form>
  {% include "formset/non_field_errors.html" %}
  {% for field in form %}
    {% if field.is_hidden %}
      {{ field }}
    {% elif field.name == "my_special_field" %}
      {% include "myproject/my_special_field.html" %}
    {% else %}
      {% include "formset/default/field_group.html" %}
    {% endif %}
  {% endfor %}
  <button type="button" click="submit -> proceed">Submit</button>
</django-formset>

Let’s discuss these lines of HTML code step by step:

First we have to “formsetify” our form. This is required in order to change the signature of the form class as described in the previous section. If the form instance already inherits from formset.utils.FormMixin, then this operation can be skipped.

We then iterate over all form fields. Here we must distinguish between hidden and visible input fields. While the latter shall be wrapped inside a <django-field-group> each, the former shall not. We can then further specialize our rendering logic, depending on which field we want to render.

Rendering a form field-by-field shall only be used as a last resort, because it inhibits the reuse of the rendering templates. If fields have to be styled explicitly, for instance to place the input field for the postal code on the same line as the input field for the “city”, then a better approach is to adopt the Form Renderers.