7. Form Collections

A very powerful feature of django-formset is the ability to create a collection of forms. In Django we quite often create forms out of models and want to edit more than one of those forms on the same page and post them in a single submission. By using a prefix on each Django Form, it is possible to name the fields uniquely and on submission we can reassign the form data back to each individual form. This however is limited to one nesting level and in order to add extra forms dynamically, we must create our own JavaScript function, which is not provided by the Django framework.

In django-formset on the other hand, we can create a form collection and explicitly add existing forms as members of those collections. It’s even possible to add a collection as a member of another collection, in order to build a pseudo nested [2] structure of forms.

The interface for classes inheriting from formset.collection.FormCollection is intentionally very similar to that of a Django Form class. It can be filled with a data dictionary as received by a POST request. It also can be initialized with an initial dictionary. Since collections can be nested, the data and initial dictionaries must contain the same shape as the nested structure.

Furthermore, a FormCollection offers a clean()-method, which returns a cleaned representation of the data provided by a client’s submission.

7.1. Simple Collection

We use this kind of collection, if we just want to group two or more forms together.

my_forms.py
from formset.collection import FormCollection

class MyFormCollection(FormCollection):
    form1 = MyForm1()
    form2 = MyForm2()

Note

The above example will render the form with the default style. To render the form with a specific CSS framework you need to specify the default_renderer attribute on your FormCollection.

Example:

from formset.collection import FormCollection
from formset.renderers.bootstrap import FormRenderer

class MyFormCollection(FormCollection):
    default_renderer = FormRenderer()
    form1 = MyForm1()
    form2 = MyForm2()

All supported CSS frameworks define a FormRenderer that can be imported with a path similar to the one defined in the example, for instance formset.renderers.bulma.FormRenderer, formset.renderers.tailwind.FormRenderer, etc.

Collections must be rendered using the special View class formset.views.FormCollectionView: The template used to render this Form Collection must ensure that the CSRF-token is set; this is done by passing that CSRF token value as attribute to the web component <django-formset>. Otherwise this View just behaves like an ordinary Form View embedded in a django-formset.

my-collection.html
<django-formset endpoint="{{ request.path }}" csrf-token="{{ csrf_token }}">
  {{ form_collection }}
</django-formset>

Finally add a route to the View:

urls.py
from django.urls import path
from formset.views import FormCollectionView
from .my_forms import MyFormCollection

urlpatterns = [
    ...
    path('contact', FormCollectionView.as_view(
        template_name='my-collection.html'
        collection_class=MyFormCollection,
        success_url='/path/to/success',
    )),
    ...
]

7.2. Nested Collection

A Form Collection can not only contain other Django Forms, but also other Form Collections. This means that we can nest collections into each other up to currently 10 levels (this limit can be increased if required).

Just as with simple collections, form data sent by the browser is already structured using the same hierarchy as the collections themselves.

7.3. Collections with Siblings

If a class inheriting from formset.collection.FormCollection contains one of the attributes min_siblings, max_siblings or extra_siblings, it is considered as a collection with siblings. They then behave similar to what we already know as Django’s InlineModelAdmin objects. The difference though is, that we can use this feature outside of the Django-Admin, and moreover, that we can nest collections into each other recursively.

Whenever a collection is declared to have siblings, its member collections are rendered from zero, once or multiple times. For each collection with siblings there is one “Add” button, and for each of the child collections there is a “Remove” button. To avoid having too many “Remove” buttons, they are invisible by default and only become visible when moving the cursor over that collection.

Legend

Just as HTML-elements of type <fieldset> can contain a legend, a form collection may optionally also contain a <legend>…</legend>-element. It is placed on top of the collection and shall be specified as attribute legend = "…" inside classes inheriting from formset.collection.FormCollection, or as a parameter when initializing the collection.

Help Text

A form collection may optionally render a <div>…</div>- or <p>…</p>-element (depending on the best practices of the CSS framework) at its end, containing a help text string. It shall be specified as attribute help_text = "…" inside classes inheriting from formset.collection.FormCollection, or as a parameter when initializing the collection.

Label for “Add” button

The parameter add_label shall contain a human readable string, telling the user what kind of collection to add as a sibling. If unset, the “Add” button just contains the + symbol.

Minimum Number of Siblings

The parameter min_siblings tells us how many collections the parent collection must contain as minimum. If unset, it defaults to 1.

Maximum Number of Siblings

The parameter max_siblings tells us how many collections the parent collection may contain as maximum. If unset, there is no upper limit.

Extra Siblings

The parameter extra_siblings tells us how many empty collections the parent collection starts with. If unset, it defaults to 0, which means that the user must explicitly add a new sibling by clicking on the “Add” button below the last sibling.

Note that a collection with siblings behaves differently, when deleting a child collection. If that child collection was initialized and thus loaded from the server, then it is rendered with a streaked background pattern, which signalizes to be removed on submission.

Marked for deletion

If on the other side that child collection was just added by clicking on the “Add” button below the last sibling, then that collection will be deleted immediately. This is because for initialized collections, while submitting we have to keep a placeholder in order to tell the server how to change the underlying model.

Ignore collections marked for removal

Adding the boolean parameter ignore_marked_for_removal to a class inheriting from formset.collection.FormCollection tells the clean()-method how to proceed with collections marked for removal. If unset or False (the default), such collections contain the special key value pair '_marked_for_removal_': True inside their returned cleaned_data structure. This information shall be used, when the backend has to locate the proper model in order to delete it. If ignore_marked_for_removal = True, then collections marked for removal do not even appear inside that cleaned_data structure returned by the clean()-method.

7.4. Sortable Collections with Siblings

Whenever we work with a list of form collections, it might make sense to reorder the given entities. This allows the user to sort the siblings of a collection. To achieve this, either add is_sortable = True when declaring the collection class, or instantiate the collection class by passing is_sortable=True to its constructor.

Form collections declared to by sortable, render a small drag area on their top right corner. By dragging that handle, the user can reorder the chosen collections. On form submission, that new order is reflected inside the list of transferred fields. When using a sortable collection to edit a (query-)set of models, it therefore is mandatory to include the primary key of each object as a hidden input field. Otherwise it will not be possible to reorder those objects afterwards in the database.

Sortable Collection

One must note that it is only possible to reorder collections inside its direct parent collection. It therefore is not possible to drag a sub collection into another collection.

Footnotes