Forms

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

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

Major features compared to Django forms:

  • Nice rendering to HTML out of the box. Default bootstrap but more built in and can be adapted to your design system.

  • AJAX-backed select widgets for your foreign key relationships.

  • Supports __ syntax for going across table/object boundaries, similar to how Django does with QuerySets.

  • Send in a callable that is late evaluated to determine if a field should be displayed (include). This is very handy for showing a slightly different form to administrators for example.

  • Easily add a CSS class or style to just the thing you need just now.

  • Easy configuration without writing entire classes that are only used in one place anyway.

Read the full documentation and the Cookbook for more.

iommi pre-packages sets of defaults for common field types as ‘shortcuts’. Some examples include Field.boolean, Field.integer and Field.choice. The full list of shortcuts can be found in the API documentation for Field.

iommi also comes with full edit, create and delete views. See below for an example of Form.edit.

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:
        @staticmethod
        def actions__submit__post_handler(form, **_):
            if not form.is_valid():
                return  # pragma: no cover

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

def edit_user_view(request, username):
    user = User.objects.get(username=username)
    return UserForm(instance=user)
▼ Hide result

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

Fully automatic forms

You can also generate forms from Django models automatically (but still customize the behavior!). The above example is equivalent to:

def edit_user_view(request, username):
    return Form(
        auto__instance=User.objects.get(username=username),
        # the field 'first_name' is generated automatically and
        # we are fine with the defaults
        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,
        actions__submit__post_handler=edit_user_save_post_handler,
    )

or even better: use Form.edit:

def edit_user_view(request, username):
    return Form.edit(
        auto__instance=User.objects.get(username=username),
        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,
    )

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.

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

Customization of save behavior on Form.create/edit

There are some useful hooks for customizing the save behavior on Form.create and Form.edit. The most common use case is to set some hardcoded value for a field that is not in the form. This is best done by using Field.hardcoded, so that should be your first option.

Saving a model in Django models SQL quite closely and iommi have hooks for all the steps in a multi-step commit.

The callbacks are executed in this order:

  • extra__new_instance: This is called to create a new instance of the model. By default it just calls form.model().

  • extra__pre_save_all_but_related_fields (only called for Form.create)

  • extra__on_save_all_but_related_fields (only called for Form.create)

  • extra__pre_save (before instance.save())

  • extra__on_save (after instance.save())

After a POST is completed, the extra__redirect callback is executed if present, otherwise extra__redirect_to is used to determine where to redirect to.