Package: django-forms-dynamic
django-forms-dynamicResolve 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.10InstallationInstall from PyPIpip install django-forms-dynamicUsagePassing arguments to form fields from the viewThe 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 inself.fields:classSelectUserFromMyTeamForm(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)defselect_user_view(request):form=SelectUserFromMyTeamForm(team=request.user.team)returnrender("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.Withdjango-forms-dynamic, we can improve on this approach. We need to do two things:Add theDynamicFormMixinto your form class (beforeforms.Form).Wrap any field that needs dynamic behaviour in aDynamicField.The first argument to theDynamicFieldconstructor is the fieldclassthat you are wrapping (egforms.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 acallable. If it is a callable, it will be calledwhen the form is being instantiatedand it will be passed the forminstanceas 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 (liketeamin the example above) into the form's constructor in the view, we borrow a useful idiom from Django REST framework serializers and instead pass asingleargument calledcontext, which is a dictionary that can contain any values you need from the view. This is attached to the form asform.context.Here's how the code looks now:fromdynamic_formsimportDynamicField,DynamicFormMixinclassSelectUserFromMyTeamForm(DynamicFormMixin,forms.Form):user=DynamicField(forms.ModelChoiceField,queryset=lambdaform:User.objects.filter(team=form.context["team"]),)defselect_user_view(request):form=SelectUserFromMyTeamForm(context={"team":request.user.team})returnrender("form.html",{"form":form})This is much nicer!Truly dynamic forms with XHRBut let's go further. Once we have access to theform, we can make forms truly dynamic by configuring fields based on the values ofotherfields. This doesn't really make sense in the standard Django request/response approach, but itdoesmake 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.HTMXTo 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>. Seethe HTMX docs pagefor full details and a working example.How would we implement the backend of this usingdjango-forms-dynamic?First, let's have a look at the form:classMakeAndModelForm(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=lambdaform: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 themodelfield based on the currently selected value of themakefield. When the form is first shown to the user,form["make"].value()will be"audi": theinitialvalue supplied to themakefield. After the form is bound,form["make"].value()will return whatever the user selected in themakedropdown.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 returnjustthe HTML for themodelfield. The latter will be loaded whenever themakefield changes, and will return the availablemodelsfor the chosenmake.Here are the two views:defhtmx_form(request):form=MakeAndModelForm()returnrender(request,"htmx.html",{"form":form})defhtmx_models(request):form=MakeAndModelForm(request.GET)returnHttpResponse(form["model"])Remember that the string representation ofform["model"](the bound field) is the HTML for the<select>element, so we can return this directly in theHttpResponse.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 usingdjango-widget-tweaksto add the necessaryhx-attributes to themakefield right in the template.{%loadwidget_tweaks%}<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.6.1"></script></head><body><form method="POST"><h3>Pick a make/model</h3>{%csrf_token%}<div>{{form.make.label_tag}}{%render_fieldform.makehx-get="/htmx-form/models/"hx-target="#id_model"%}</div><div>{{form.model.label_tag}}{{form.model}}</div></form></body></html>UnpolyLet'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!defunpoly_form(request):form=MakeAndModelForm(request.POSTorNone)returnrender(request,"unpoly.html",{"form":form})urlpatterns=[path("unpoly-form/",unpoly_form),]And the template is even more simple:{%loadwidget_tweaks%}<!DOCTYPE html><html><head><script src="https://unpkg.com/unpoly@2.5.0/unpoly.min.js"></script></head><body><form method="POST"><h3>Pick a make/model</h3>{%csrf_token%}<div>{{form.make.label_tag}}{%render_fieldform.makeup-validate="form"%}</div><div>{{form.model.label_tag}}{{form.model}}</div></form></body></html>TheincludeargumentThere'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, theDynamicFieldconstructor 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:Trueif the field should be included in the form,Falseotherwise. Here's an example:classCancellationReasonForm(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=lambdaform:form["cancellation_reason"].value()=="other",)Known gotcha: callable argumentsOne thing that might catch you out: if the object you're passing in to your form field's constructor isalreadya callable, you will need to wrap it in another callable that takes theformargument and returns theactualcallable 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:classCancellationReasonForm(DynamicFormMixin,forms.Form):...# other fieldsreason_if_other=DynamicField(forms.CharField,include=lambdaform:form["cancellation_reason"].value()=="other",widget=lambda_:forms.TextArea,)Why the awkward name?Becausedjango-dynamic-formswas already taken.Code of conductFor guidelines regarding the code of conduct when contributing to this repository please reviewhttps://www.dabapps.com/open-source/code-of-conduct/