1. Introduction

django-formset attempts to solve a problem, which occurs in almost every Django project which implements forms to collect data from the user.

In Django, this typically consists of creating a form instance, a view instance, and a template rendering that form. The user then can enter data into the fields of that form, which during submission are sent to the server for validation. If one or more of those fields fail to validate, the form is re-rendered, annotating the fields containing invalid data with a meaningful error message. The latter requires to fully reload the whole page. From a usability point of view, this approach is far from being contemporary.

An often used apporach to improve the user experience is to combine a popular JavaScript framework with Django REST framework. Those JavaScript frameworks however impose their own way of getting stuff done and usually don’t share the same mindset with Django. For instance, in Django we distinguish between bound and unbound forms. This concept however doesn’t make sense in most JavaScript frameworks, and hence is not implemented. We therefore often must work around those problems, which leads to cumbersome and un-DRY solutions.

By using django-formset, we can use our well known Django form and view implementations and gain a contemporary user experience. Legacy implementations can be ported easily, because one has to apply only very few changes to the existing code base.

With django-formset we get a web component explicitly written to handle Django forms and collections of forms (hence “formset”). This means that fields can be pre-validated by the client, giving immediate feedback on invalid field values. If the form’s content then is sent to the server and fails to validate there, those error messages are sent back to the client and show up nearby the fields containing invalid or missing data.

Giving feedback on a form which did not validate doesn’t require a page reload anymore. The nice thing about this approach is, that we can reuse all of our current Django forms (unaltered), can use our existing Django views (with a small modification), but neither have to add any extra code nor endpoints to the URL routing of our application.

1.1. Use Forms as Logical Entities

The django-formset library separates the logical layer of a Django Form from their HTML entity <form>.

What does that mean? In Django we can define a form as a group of fields with certain data-types. Often these forms are derived from a Django model. On the client, this form then is rendered, can be filled with data and submitted back to the server.

Typically there is one form per page, because the HTML standard does not allow you to submit more than one form in one submission. With the introduction of FormSets, Django provides a workaround for this use-case. It however relies on prefixing each field from the forms making up a “FormSet” with a unique identifier, so that those Django forms can be wrapped into one HTML <form>-element. This makes the handling of multiple forms per page cumbersome and difficult to understand.

By using django-formset on the other hand, each Django form corresponds to its own self-contained <form>-element. Inside each of these forms, all field names remain unmodified and on submission, each form introduces its own namespace, so that the form data is submitted as a dictionary of field-value-pairs, where each value can be a subdictionary or array. By doing so, we can nest forms deeply, something currently not possible with Django FormSets.

Example

Consider a simple form to ask a user for its first- and last name. Additionally we apply some contraints on how these names have to be written. This form then is rendered and controlled by a slightly modified Django view:

Interacting with a form shows validation errors immediately.
from django.core.exceptions import ValidationError
from django.forms import forms, fields
from formset.views import FormView

class PersonForm(forms.Form):
    first_name = fields.RegexField(
        r'^[A-Z][\-a-z ]+$',
        label="First name",
        error_messages={'invalid': "A first name must start in upper case."},
        help_text="Must start in upper case followed by one or more lowercase characters.",
    )

    last_name = fields.CharField(
        label="Last name",
        min_length=2,
        max_length=50,
        help_text="Please enter at least two, but no more than 50 characters.",
    )

    def clean(self):
        cd = self.cleaned_data
        if cd.get("first_name", "").lower().startswith("john") \
            and cd.get("last_name", "").lower().startswith("doe"):
            raise ValidationError("John Doe is an undesirable person")
        return cd

class PersonFormView(FormView):
    form_class = PersonForm
    template_name = "form.html"
    success_url = "/success"
Must start in upper case followed by one or more lowercase characters.
Please enter at least two, but no more than 50 characters.

It should be mentioned that this view must be rendered by wrapping the form inside the web component <django-formset>. This web component then controls the client side functionality, such as pre- and post-validation, submission, etc. The content of the two form fields is submitted to the endpoint="{{ request.path }}". Here this is the same Django view, which also is responsible for rendering that form.

form.html
{% load formsetify %}

<django-formset endpoint="{{ request.path }}" csrf-token="{{ csrf_token }}">
  {% render_form form "tailwind" %}
  <button type="button" df-click="submit">Submit</button>
  <button type="button" df-click="reset">Reset to initial</button>
</django-formset>

When looking at the rendered HTML code, there are a few things, which admittedly, may seem unusual to us:

  • The <form> tag neither contains a method nor an action attribute.

  • The CSRF-token is not added to <django-formset> instead of a hidden input field.

  • The “Submit” and “Reset” buttons are located outside of the <form> element.

Note

When using Django’s internal formset, the field names have to be prefixed with identifiers to distinguish their form affiliation. This is cumbersome and difficult to debug. By using django-formset, we can keep the field names, since our wrapper groups them into plain JavaScript objects.

In this example, the form is rendered by the special templatetag {% render_form form "tailwind" %}. This templatetag can be parametrized to use the correct style-guide for each of the supported CSS frameworks. It can also be used to pass in our own CSS classes for labels, fields and field groups. More on this can be found in chapter Using a Native Django Form.

It also is possible to render the form using the classic approach with mustaches, ie. {{ form }}. Then however the form object can’t be a native Django form. Instead it has to be transformed using a special mixin class. More on this can be found in chapter Using an Extended Django Form.

Another approach is to render the form field-by-field. Here we gain full control over how each field is rendered, since we render them individually. More on this can be found in chapter Rendering a Django Form Field-by-Field.

1.2. What are Web Components?

According to webcomponents.org, web components are a set of web platform APIs that allow you to create new custom, reusable, encapsulated HTML tags to use in web pages and web apps. Custom components and widgets built upon the web component standards, will work across modern browsers, and can be used with any JavaScript library or framework that works with HTML.

Web components are based on existing web standards. Features to support web components are currently being added to the HTML and DOM specs, letting web developers easily extend HTML with new elements with encapsulated styling and custom behavior.

The JavaScript behind this component now handles the following functions:

  • Client-side validation of our form fields using the constraints defined by our form.

  • Serializes the data entered into our form fields.

  • Handles the submission of that data, by sending it to the server’s endpoint.

  • Receives server-side validation annotations and marks all fields containing incorrect data.

  • On success, performs a different action, usually a redirect onto a success page.

  • Handles various actions after the user clicked on the button. This is useful to make the button behave more interactively.

Note

Form data submitted by the web component <django-formset> is not send using the default enctype application/x-www-form-urlencoded. Instead the data from all forms is packed together into a JavaScript object and submitted to the server using enctype application/json. This means that our Django view receiving the form data, must be able to process that data using a slightly modified handler.

1.3. Annotation

When designing this library, one of the main goals was to keep the programming interface as near as possible to the way Django handles forms, models and views. It therefore is possible to reuse existing Django form declarations with a minimal modification to existing code.

For details on why this project exists, please refer to section about the History of django-formset.

1.4. License

django-formset is licensed under the MIT public license. Please consult the the appropriate file in the repository for details.

1.5. Contributing

Please read chapter Contributing to the Project before opening issues or pull requests.