Forms

iommi forms is an alternative forms system for Django. It is inspired by the standard Django forms, while improving on its weaknesses.

This page explains what iommi forms are and how they differ from Django’s. If you want to do something specific, go to the form cookbook; for the exhaustive list of options see Form and Field.

Where iommi forms differ from Django’s

  • They render to HTML nicely out of the box. The default is bootstrap, several other styles ship with iommi, and you can adapt them to your own design system.

  • Foreign key relationships get AJAX-backed select widgets, with the endpoint wired up on the same URL as the view. Because it’s the same view, it’s covered by the same permission checks.

  • __ works for going across table/object boundaries, the same way Django does it for QuerySets.

  • Anywhere you can put a value you can put a callable instead, evaluated late. include=lambda request, **_: request.user.is_staff is how you show a slightly different form to administrators, without a second form class.

  • You can add a CSS class or attribute to exactly one thing without copying a template.

  • Configuration doesn’t require writing a class that’s only used in one place.

A Form is also a complete view. Form.create, Form.edit and Form.delete give you the post handler and the redirect too, so there’s no template and no view function to write. See Views for the CRUD set built on top of these.

Instead of Field subclasses, iommi pre-packages sets of defaults as shortcutsField.boolean, Field.integer, Field.choice and so on. The difference matters: a shortcut’s config is defaults, not hard coded behavior, so you can start from one and refine it without subclassing. See Philosophy for why, and Field for the full list.

Three ways to say the same thing

The next three sections build the same form automatically, declaratively and programmatically. They are not three different features to choose between: they are one API seen from three angles, and you can mix them freely. Which one reads best depends on how much you know at import time. Equivalence spells out the mapping between them.

Fully automatic forms

Generating forms from Django models automatically is the most powerful and common use for iommi forms:

form = Form.create(auto__model=Album)
▼ Hide result
Toggle structure

Forms in iommi scale with higher complexity:

edit_user_form = Form.edit(
    auto__model=User,
    instance=lambda user_pk, **_: User.objects.get(pk=user_pk),
    fields__username__is_valid=
        lambda parsed_data, **_: (
            parsed_data.startswith('demo_'),
            'needs to start with demo_'
        ),
    fields__is_staff__label__template='tweak_label_tag.html',
    # show only for staff
    fields__is_staff__include=lambda request, **_: request.user.is_staff,
)

Install like this:

urlpatterns = [
    path('users/<user_pk>/edit/', edit_user_form.as_view()),
]
▼ Hide result
Toggle structure

In this case the default behavior for the post handler for Form.edit is a save function like the one we had to define ourselves in the previous example.

Declarative forms

You can create forms declaratively, similar to Django forms. There are some important differences between iommi forms and Django forms in this mode, maybe the most important being that in iommi you can pass a callable as a parameter to late evaluate what the value of something is. This is used to restrict a field for staff users in this example:

class UserForm(Form):
    first_name = Field.text()
    username = Field.text(
        is_valid=lambda parsed_data, **_: (
            parsed_data.startswith('demo_'),
            'needs to start with demo_'
        )
    )
    is_staff = Field.boolean(
        # show only for staff
        include=lambda request, **_: request.user.is_staff,
        label__template='tweak_label_tag.html',
    )

    class Meta:
        instance = lambda params, **_: User.objects.get(pk=params.user_pk)

        @staticmethod
        def actions__submit__post_handler(user, form, **_):
            if not form.is_valid():
                return  # pragma: no cover

            form.apply(user)
            user.save()
            return HttpResponseRedirect('..')

Install like this:

urlpatterns = [
    # Note `UserForm()`, not `UserForm`!
    path('users/<user_pk>/edit/', UserForm().as_view()),
]
▼ Hide result
Toggle structure

Note that we don’t need any template here.

Programmatic forms

The declarative style is very readable, but sometimes you don’t know until runtime what the form should look like. Creating forms programmatically in iommi is easy (and equivalent to doing it the declarative way):

def edit_user_save_post_handler(form, **_):
    if not form.is_valid():
        return  # pragma: no cover

    form.apply(form.instance)
    form.instance.save()
    return HttpResponseRedirect('..')

def edit_user_view(request, username):
    return Form(
        instance=User.objects.get(username=username),
        fields=dict(
            first_name=Field.text(),
            username=Field.text(
                is_valid=lambda parsed_data, **_: (
                    parsed_data.startswith('demo_'),
                    'needs to start with demo_'
                ),
            ),
            is_staff=Field.boolean(
                # show only for staff
                include=lambda request, **_: request.user.is_staff,
                label__template='tweak_label_tag.html',
            ),
        ),
        actions__submit__post_handler=edit_user_save_post_handler,
    )
▼ Hide result
Toggle structure

Post handlers

In the simplest cases, like in a create form, you only have one post handler. You can do this yourself in the classic Django way:

if form.is_valid() and request.method == 'POST':
    do_your_thing()

This is fine. But what if you have two buttons? What if you have two forms? What if there are two forms, one with two submit buttons, and a table with a bulk action? Suddenly writing the if statement above becomes very difficult. Post handlers in iommi handle this for you. iommi makes sure that the parts compose cleanly and the right action is called.

By default for create/edit/delete forms you get one post handler by the name submit. Adding more is easy:

def disable_action(form, **_):
    form.instance.disabled = True
    form.instance.save()
    return HttpResponseRedirect('.')

form = Form.edit(
    auto__instance=instance,
    actions__disable__post_handler=disable_action,
)

Post handlers can return a few different things:

  • a HttpResponse object which will get returned all the way up the stack

  • a bound Part of some kind. This could be a Table, Form, Page, etc. This is rendered into a HttpResponse

  • None will result in the page being rendered like normal

  • everything else iommi will attempt to json encode and return as a json response

Saving

Form.create and Form.edit come with a post handler that saves for you, so in the common case there is nothing to write. When you do need to intervene, you don’t subclass or override a save method: you refine one of the callbacks that correspond to the steps of a Django multi-step commit.

This mirrors the philosophy elsewhere in iommi – there is a named hook for each step, so you can replace just the step you care about and leave the rest of the default behavior alone.

For the list of callbacks and the order they run in, see Save callbacks on Form. For worked examples, see the cookbook.