Resolve form field arguments dynamically when a form is instantiated

Overview

django-forms-dynamic

Resolve form field arguments dynamically when a form is instantiated, not when it's declared.

Tested against Django 2.2, 3.2 and 4.0 on Python 3.6, 3.7, 3.8, 3.9 and 3.10

Build Status pypi release

Installation

Install from PyPI

pip install django-forms-dynamic

Usage

Passing arguments to form fields from the view

The standard way to change a Django form's fields at runtime is override the form's __init__ method, pass in any values you need from the view, and poke around in self.fields:

class SelectUserFromMyTeamForm(forms.Form):
    user = forms.ModelChoiceField(queryset=User.objects.none())

    def __init__(self, *args, **kwargs):
        team = kwargs.pop("team")
        super().__init__(*args, **kwargs)
        self.fields["user"].queryset = User.objects.filter(team=team)
def select_user_view(request):
    form = SelectUserFromMyTeamForm(team=request.user.team)
    return render("form.html", {"form": form})

This works, but it doesn't scale very well to more complex requirements. It also feels messy: Django forms are intended to be declarative, and this is very much procedural code.

With django-forms-dynamic, we can improve on this approach. We need to do two things:

  1. Add the DynamicFormMixin to your form class (before forms.Form).
  2. Wrap any field that needs dynamic behaviour in a DynamicField.

The first argument to the DynamicField constructor is the field class that you are wrapping (eg forms.ModelChoiceField). All other arguments (with one special-cased exception detailed below) are passed along to the wrapped field when it is created.

But there's one very important difference: any argument that would normally be passed to the field constructor can optionally be a callable. If it is a callable, it will be called when the form is being instantiated and it will be passed the form instance as an argument. The value returned by this callable will then be passed into to the field's constructor as usual.

Before we see a code example, there's one further thing to note: instead of passing arbitrary arguments (like team in the example above) into the form's constructor in the view, we borrow a useful idiom from Django REST framework serializers and instead pass a single argument called context, which is a dictionary that can contain any values you need from the view. This is attached to the form as form.context.

Here's how the code looks now:

from dynamic_forms import DynamicField, DynamicFormMixin


class SelectUserFromMyTeamForm(DynamicFormMixin, forms.Form):
    user = DynamicField(
        forms.ModelChoiceField,
        queryset=lambda form: User.objects.filter(team=form.context["team"]),
    )
def select_user_view(request):
    form = SelectUserFromMyTeamForm(context={"team": request.user.team})
    return render("form.html", {"form": form})

This is much nicer!

Truly dynamic forms with XHR

But let's go further. Once we have access to the form, we can make forms truly dynamic by configuring fields based on the values of other fields. This doesn't really make sense in the standard Django request/response approach, but it does make sense when we bring JavaScript into the equation. A form can be loaded from the server multiple times (or in multiple pieces) by making XHR requests from JavaScript code running in the browser.

Implementing this "from scratch" in JavaScript is left as an exercise for the reader. Instead, let's look at how you might do this using some modern "low JavaScript" frameworks.

HTMX

To illustrate the pattern we're going to use one of the examples from the HTMX documentation: "Cascading Selects". This is where the options available in one <select> depend on the value chosen in another <select>. See the HTMX docs page for full details and a working example.

How would we implement the backend of this using django-forms-dynamic?

First, let's have a look at the form:

class MakeAndModelForm(DynamicFormMixin, forms.Form):
    MAKE_CHOICES = [
        ("audi", "Audi"),
        ("toyota", "Toyota"),
        ("bmw", "BMW"),
    ]

    MODEL_CHOICES = {
        "audi": [
            ("a1", "A1"),
            ("a3", "A3"),
            ("a6", "A6"),
        ],
        "toyota": [
            ("landcruiser", "Landcruiser"),
            ("tacoma", "Tacoma"),
            ("yaris", "Yaris"),
        ],
        "bmw": [
            ("325i", "325i"),
            ("325ix", "325ix"),
            ("x5", "X5"),
        ],
    }

    make = forms.ChoiceField(
        choices=MAKE_CHOICES,
        initial="audi",
    )
    model = DynamicField(
        forms.ChoiceField,
        choices=lambda form: form.MODEL_CHOICES[form["make"].value()],
    )

The key bit is right at the bottom. We're using a lambda function to load the choices for the model field based on the currently selected value of the make field. When the form is first shown to the user, form["make"].value() will be "audi": the initial value supplied to the make field. After the form is bound, form["make"].value() will return whatever the user selected in the make dropdown.

HTMX tends to encourage a pattern of splitting your UI into lots of small endpoints that return fragments of HTML. So we need two views: one to return the entire form on first page load, and one to return just the HTML for the model field. The latter will be loaded whenever the make field changes, and will return the available models for the chosen make.

Here are the two views:

def htmx_form(request):
    form = MakeAndModelForm()
    return render(request, "htmx.html", {"form": form})


def htmx_models(request):
    form = MakeAndModelForm(request.GET)
    return HttpResponse(form["model"])

Remember that the string representation of form["model"] (the bound field) is the HTML for the <select> element, so we can return this directly in the HttpResponse.

These can be wired up to URLs like this:

urlpatterns = [
    path("htmx-form/", htmx_form),
    path("htmx-form/models/", htmx_models),
]

And finally, we need a template. We're using django-widget-tweaks to add the necessary hx- attributes to the make field right in the template.

{% load widget_tweaks %}
<!DOCTYPE html>

<html>
  <head>
    <script src="https://unpkg.com/[email protected]"></script>
  </head>
  <body>
    <form method="POST">
      <h3>Pick a make/model</h3>
      {% csrf_token %}
      <div>
        {{ form.make.label_tag }}
        {% render_field form.make hx-get="/htmx-form/models/" hx-target="#id_model" %}
      </div>
      <div>
        {{ form.model.label_tag }}
        {{ form.model }}
      </div>
    </form>
  </body>
</html>

Unpoly

Let's build exactly the same thing with Unpoly. Unpoly favours a slightly different philosophy: rather than having the backend returning HTML fragments, it tends to prefer the server to return full HTML pages with every XHR request, and "plucks out" the relevant element(s) and inserts them into the DOM, replacing the old ones.

When it comes to forms, Unpoly uses a special attribute [up-validate] to mark fields which, when changed, should trigger the form to be submitted and re-validated. The docs for [up-validate] also describe it as "a great way to partially update a form when one field depends on the value of another field", so this is what we'll use to implement our cascading selects.

The form is exactly the same as the HTMX example above. But this time, we only need one view!

def unpoly_form(request):
    form = MakeAndModelForm(request.POST or None)
    return render(request, "unpoly.html", {"form": form})
urlpatterns = [
    path("unpoly-form/", unpoly_form),
]

And the template is even more simple:

{% load widget_tweaks %}
<!DOCTYPE html>

<html>
  <head>
    <script src="https://unpkg.com/[email protected]/unpoly.min.js"></script>
  </head>
  <body>
    <form method="POST">
      <h3>Pick a make/model</h3>
      {% csrf_token %}
      <div>
        {{ form.make.label_tag }}
        {% render_field form.make up-validate="form" %}
      </div>
      <div>
        {{ form.model.label_tag }}
        {{ form.model }}
      </div>
    </form>
  </body>
</html>

The include argument

There's one more feature we might need: what if we want to remove a field from the form entirely unless another field has a particular value? To accomplish this, the DynamicField constructor takes one special argument that isn't passed along to the constructor of the wrapped field: include. Just like any other argument, this can be a callable that is passed the form instance, and it should return a boolean: True if the field should be included in the form, False otherwise. Here's an example:

class CancellationReasonForm(DynamicFormMixin, forms.Form):
    CANCELLATION_REASONS = [
        ("too-expensive", "Too expensive"),
        ("too-boring", "Too boring"),
        ("other", "Other"),
    ]

    cancellation_reason = forms.ChoiceField(choices=CANCELLATION_REASONS)
    reason_if_other = DynamicField(
        forms.CharField,
        include=lambda form: form["cancellation_reason"].value() == "other",
    )

Known gotcha: callable arguments

One thing that might catch you out: if the object you're passing in to your form field's constructor is already a callable, you will need to wrap it in another callable that takes the form argument and returns the actual callable you want to pass to the field.

This is most likely to crop up when you're passing a custom widget class, because classes are callable:

class CancellationReasonForm(DynamicFormMixin, forms.Form):
    ...  # other fields

    reason_if_other = DynamicField(
        forms.CharField,
        include=lambda form: form["cancellation_reason"].value() == "other",
        widget=lambda _: forms.TextArea,
    )

Why the awkward name?

Because django-dynamic-forms was already taken.

Code of conduct

For guidelines regarding the code of conduct when contributing to this repository please review https://www.dabapps.com/open-source/code-of-conduct/

You might also like...
A drop-in replacement for django's ImageField that provides a flexible, intuitive and easily-extensible interface for quickly creating new images from the one assigned to the field.

django-versatileimagefield A drop-in replacement for django's ImageField that provides a flexible, intuitive and easily-extensible interface for creat

A pickled object field for Django

django-picklefield About django-picklefield provides an implementation of a pickled object field. Such fields can contain any picklable objects. The i

Full control of form rendering in the templates.

django-floppyforms Full control of form rendering in the templates. Authors: Gregor Müllegger and many many contributors Original creator: Bruno Renié

Twitter Bootstrap for Django Form

Django bootstrap form Twitter Bootstrap for Django Form. A simple Django template tag to work with Bootstrap Installation Install django-bootstrap-for

Full control of form rendering in the templates.

django-floppyforms Full control of form rendering in the templates. Authors: Gregor Müllegger and many many contributors Original creator: Bruno Renié

Twitter Bootstrap for Django Form - A simple Django template tag to work with Bootstrap

Twitter Bootstrap for Django Form - A simple Django template tag to work with Bootstrap

This is a sample Django Form.

Sample FORM Installation guide Clone repository git clone https://github.com/Ritabratadas343/SampleForm.git cd to repository. Create a virtualenv by f

Basic Form Web Development using Python, Django and CSS

thebookrain Basic Form Web Development using Python, Django and CSS This is a basic project that contains two forms - borrow and donate. The form data

MAC address Model Field & Form Field for Django apps

django-macaddress MAC Address model and form fields for Django We use netaddr to parse and validate the MAC address. The tests aren't complete yet. Pa

Tweak the form field rendering in templates, not in python-level form definitions. CSS classes and HTML attributes can be altered.

django-widget-tweaks Tweak the form field rendering in templates, not in python-level form definitions. Altering CSS classes and HTML attributes is su

Tweak the form field rendering in templates, not in python-level form definitions. CSS classes and HTML attributes can be altered.

django-widget-tweaks Tweak the form field rendering in templates, not in python-level form definitions. Altering CSS classes and HTML attributes is su

:package: :fire: Python project management. Manage packages: convert between formats, lock, install, resolve, isolate, test, build graph, show outdated, audit. Manage venvs, build package, bump version.
:package: :fire: Python project management. Manage packages: convert between formats, lock, install, resolve, isolate, test, build graph, show outdated, audit. Manage venvs, build package, bump version.

THE PROJECT IS ARCHIVED Forks: https://github.com/orsinium/forks DepHell -- project management for Python. Why it is better than all other tools: Form

Django query profiler - one profiler to rule them all.  Shows queries, detects N+1 and gives recommendations on how to resolve them
Django query profiler - one profiler to rule them all. Shows queries, detects N+1 and gives recommendations on how to resolve them

Django Query Profiler This is a query profiler for Django applications, for helping developers answer the question "My Django code/page/API is slow, H

Django query profiler - one profiler to rule them all.  Shows queries, detects N+1 and gives recommendations on how to resolve them
Django query profiler - one profiler to rule them all. Shows queries, detects N+1 and gives recommendations on how to resolve them

Django Query Profiler This is a query profiler for Django applications, for helping developers answer the question "My Django code/page/API is slow, H

Automatically resolve RidderMaster based on TensorFlow & OpenCV
Automatically resolve RidderMaster based on TensorFlow & OpenCV

AutoRiddleMaster Automatically resolve RidderMaster based on TensorFlow & OpenCV 基于 TensorFlow 和 OpenCV 实现的全自动化解御迷士小马谜题 Demo How to use Deploy the ser

A raw implementation of the nearest insertion algorithm to resolve TSP problems in a TXT format.

TSP-Nearest-Insertion A raw implementation of the nearest insertion algorithm to resolve TSP problems in a TXT format. Instructions Load a txt file wi

A Project to resolve hostname and receive IP

hostname-resolver A Project to resolve hostname and receive IP Installation git clone https://github.com/ihapiw/hostname-resolver.git Head into the ho

A django model and form field for normalised phone numbers using python-phonenumbers

django-phonenumber-field A Django library which interfaces with python-phonenumbers to validate, pretty print and convert phone numbers. python-phonen

A django model and form field for normalised phone numbers using python-phonenumbers

django-phonenumber-field A Django library which interfaces with python-phonenumbers to validate, pretty print and convert phone numbers. python-phonen

Releases(1.0.0)
Owner
DabApps
We design and build bespoke web and mobile applications that help our clients succeed
DabApps
GeoDjango provides geospatial extensions to the Django web dev framework

Django is a high-level Python Web framework that encourages rapid development and clean, pragmatic design. All documentation is in the "docs" directo

Paul Smith 20 Sep 20, 2022
Phoenix LiveView but for Django

Reactor, a LiveView library for Django Reactor enables you to do something similar to Phoenix framework LiveView using Django Channels. What's in the

Eddy Ernesto del Valle Pino 526 Jan 02, 2023
A Django app to accept payments from various payment processors via Pluggable backends.

Django-Merchant Django-Merchant is a django application that enables you to use multiple payment processors from a single API. Gateways Following gate

Agiliq 997 Dec 24, 2022
Exemplo de biblioteca com Django

Bookstore Exemplo de biblioteca feito com Django. Este projeto foi feito com: Python 3.9.7 Django 3.2.8 Django Rest Framework 3.12.4 Bootstrap 4.0 Vue

Regis Santos 1 Oct 28, 2021
Django API that scrapes and provides the last news of the city of Carlos Casares by semantic way (RDF format).

"Casares News" API Api that scrapes and provides the last news of the city of Carlos Casares by semantic way (RDF format). Usage Consume the articles

Andrés Milla 6 May 12, 2022
Log and View requests made on Django

Django Request Viewer Log and view requests made on your Django App Introduction Recently, @ichtrojan and @toniastro released horus, a request logger

Akere Mukhtar 26 May 29, 2022
Forward and backwards compatibility layer for Django 1.4, 1.7, 1.8, 1.9, 1.10, and 1.11

django-compat Forward and backwards compatibility layer for Django 1.4 , 1.7 , 1.8, 1.9, 1.10 and 1.11 Consider django-compat as an experiment based o

arteria GmbH 106 Mar 28, 2022
Coltrane - A simple content site framework that harnesses the power of Django without the hassle.

coltrane A simple content site framework that harnesses the power of Django without the hassle. Features Can be a standalone static site or added to I

Adam Hill 58 Jan 02, 2023
Inject an ID into every log message from a Django request. ASGI compatible, integrates with Sentry, and works with Celery

Django GUID Now with ASGI support! Django GUID attaches a unique correlation ID/request ID to all your log outputs for every request. In other words,

snok 300 Dec 29, 2022
A ToDO Rest API using Django, PostgreSQL and Docker

This Rest API uses PostgreSQL, Docker and Django to implements a ToDo application.

Brenno Lima dos Santos 2 Jan 05, 2022
GameStop clone with Django

GameStop clone with Django This is my side project with GameStop clone Author: HackerApe GitHub Profile: View Profile LinkedIn Profile: View Profile

Dmitriy Shin 2 Dec 26, 2021
Django Persistent Filters is a Python package which provide a django middleware that take care to persist the querystring in the browser cookies.

Django Persistent Filters Django Persistent Filters is a Python package which provide a django middleware that take care to persist the querystring in

Lorenzo Prodon 2 Aug 05, 2022
Django And React Notes App

Django & React Notes App Cloning the repository -- Clone the repository using the command below : git clone https://github.com/divanov11/Django-React

Dennis Ivy 136 Dec 27, 2022
Utilities to make function-based views cleaner, more efficient, and better tasting.

django-fbv Utilities to make Django function-based views cleaner, more efficient, and better tasting. 💥 📖 Complete documentation: https://django-fbv

Adam Hill 49 Dec 30, 2022
Django project starter on steroids: quickly create a Django app AND generate source code for data models + REST/GraphQL APIs (the generated code is auto-linted and has 100% test coverage).

Create Django App 💛 We're a Django project starter on steroids! One-line command to create a Django app with all the dependencies auto-installed AND

imagine.ai 68 Oct 19, 2022
Simply integrate Summernote editor with Django project.

django-summernote Summernote is a simple WYSIWYG editor. django-summernote allows you to embed Summernote into Django very handy. Support admin mixins

Summernote 936 Jan 02, 2023
Fully reponsive Chat Application built with django, javascript, materialUi, bootstrap4, html and css.

Chat app (Full Stack Frameworks with Django Project) Fully reponsive Chat Application built with django, javascript, materialUi, bootstrap4, html and

1 Jan 19, 2022
A Django Demo Project of Students Management System

Django_StudentMS A Django Demo Project of Students Management System. From NWPU Seddon for DB Class Pre. Seddon simplify the code in 2021/10/17. Hope

2 Dec 08, 2021
E-Commerce Platform

Shuup Shuup is an Open Source E-Commerce Platform based on Django and Python. https://shuup.com/ Copyright Copyright (c) 2012-2021 by Shuup Commerce I

Shuup 2k Jan 07, 2023
Django Rest Framework + React application.

Django Rest Framework + React application.

2 Dec 19, 2022