I'm making a form with a nested dynamic formset using htmx i (want to evade usign JS, but if there's no choice...) to instance more formset fields in order to make a dynamic nested form, however when i POST, only the data from 1 instance of the Chlid formset
(the last one) is POSTed, the rest of the form POSTs correctly and the Child model
gets the relation to the Parent model
I read the django documentation on how to POST formset instances and tried to apply it to my code, also i got right how to POST both Parent
and Child
at the same time. For the formsets i'm making a htmx get request hx-get to a partial template that contains the child formset and that works great, the only problem is that this always returns a form-0
formset to the client side, so for the POST the data repeats x
times per field and only takes the data placed in the last instance, however i tried to change the extra=int
value on my formset to get more forms upright, this gave the expected result, one Child instance per form in extra=int
, so my problem is up with htmx and the way i'm calling the new Child formset
instances.
here's my code. (i plan to nest more child formsets inside this form so i call this sformset for conveniece)
****views.py****
def createPlan(request):#Requst for the Parent form
form = PlanForm(request.POST or None)
sformset = StructureFormset(request.POST or None) #Nesting the Child formset
context = {
'form':form,
'sformset':sformset,
}
if request.method == 'POST':
print(request.POST)
if form.is_valid() and sformset.is_valid():
plan = form.save(commit=False)
print(plan)
plan.save()
sform = sformset.save(commit=False)
for structure in sform:
structure.plan = plan
structure.save()
return render(request, 'app/plan_forms.html', context)
def addStructure(request):
sformset = StructureFormset(queryset=Structure.objects.none())#add a empty formset instance
context = {"sformset":sformset}
return render(request, 'app/formsets/structure_form.html', context)
****forms.py****
StructureFormset = modelformset_factory(Structure,
fields = (
'material_type',
'weight',
'thickness',
'provider'
))
****relevant part for plan_forms.html template****
<form method="POST">
{% csrf_token %}
<div class="col-12 px-2">
<div class="row px-3 py-1">
<div class="col-3 px-1">{{ form.format }}</div>
<div class="col-3 px-1">{{ form.pc }}</div>
<div class="col-3 px-1">{{ form.revission }}</div>
<div class="col-3 px-1">{{ form.rev_date }}</div>
</div>
<div class="row px-3 py-1">
<div class="col-3 px-1">{{ form.client }}</div>
<div class="col-3 px-1">{{ form.product }}</div>
<div class="col-3 px-1">{{ form.gp_code }}</div>
<div class="col-3 px-1">{{ form.code }}</div>
</div>
</div>
<div>
<table>
<tbody style="user-select: none;" id="structureforms" hx-sync="closest form:queue">
<!--Structure formset goes here-->
</tbody>
<tfoot>
<a href="" hx-get="{% url 'structure-form' %}" hx-swap="beforeend" hx-target="#structureforms">
Add structure <!--Button to call structure formset-->
</a>
</tfoot>
</table>
</div>
<div class="col-12 px-2">
<div class="row px-4 py-1">{{ form.observation }}</div>
<div class="row px-4 py-1">{{ form.continuation }}</div>
<div class="row px-4 py-1">{{ form.dispatch_conditions }}</div>
<div class="row px-3 py-1">
<div class="col-6 px-1">{{ form.elaborator }}</div>
<div class="col-6 px-1">{{ form.reviewer }}</div>
</div>
</div>
<button type="submit">Submit</button>
</form>
****formsets/structure_form.html****
<tr>
<td class="col-12 px-1">
{{ sformset }}
</td>
</tr>
**** relevant urls.py****
urlpatterns = [
path('create_plan/', views.createPlan, name='create_plan'),
path('htmx/structure-form/', views.addStructure, name='structure-form')]
Additionally, the form that i built in admin.py using fields and inlines is just exactly what i want as the raw product (except for the amount of initial formsets and styles)
To summarize the problem: At present, your code successfully brings in the new formset, but each new formset comes with a name
attribute of form-0-title
(ditto for id
and other attributes). In addition, after adding the new formset with hx-get
the hidden fields originally created by the ManagementForm
will no longer reflect the number of formsets on the page.
What's needed
After a new formset is added to the site, here's what I think needs to happen so Django can process the form submission.
Update the value
attribute in the input element with id="id_form-TOTAL_FORMS"
so the number matches the actual number of formsets on the page after hx-get
brings in the new formset.
Update the name
and id
of the new formset from form-0-title
to use whatever number reflects the current total number of formsets.
Update the labels' for
attributes in the same way.
You can do this with Javascript on the client side. Alternatively, you can do effectively the same thing with Django on the server side and then htmx can be the only javascript needed to do the rest. For that, I have used empty_form
to create the html content of a formset which can be altered as needed. That work is shown in the build_new_formset()
helper, below.
Example
Here's what I have working:
forms.py
from django import forms
from django.forms import formset_factory
class BookForm(forms.Form):
title = forms.CharField()
author = forms.CharField()
BookFormSet = formset_factory(BookForm)
views.py
from django.utils.safestring import mark_safe
from app2.forms import BookFormSet
def formset_view(request):
template = 'formset.html'
if request.POST:
formset = BookFormSet(request.POST)
if formset.is_valid():
print(f">>>> form is valid. Request.post is {request.POST}")
return HttpResponseRedirect(reverse('app2:formset_view'))
else:
formset = BookFormSet()
return render(request, template, {'formset': formset})
def add_formset(request, current_total_formsets):
new_formset = build_new_formset(BookFormSet(), current_total_formsets)
context = {
'new_formset': new_formset,
'new_total_formsets': current_total_formsets + 1,
}
return render(request, 'formset_partial.html', context)
# Helper to build the needed formset
def build_new_formset(formset, new_total_formsets):
html = ""
for form in formset.empty_form:
html += form.label_tag().replace('__prefix__', str(new_total_formsets))
html += str(form).replace('__prefix__', str(new_total_formsets))
return mark_safe(html)
Note re: build_new_formset()
helper: formset.empty_form
will omit the index numbers that should go on the id
, name
and label
attributes, and will instead use "__prefix__"
. You want to replace that "__prefix__"
part with the appropriate number. For example, if it's the second formset on the page its id
should be id_form-1-title
(changed from id_form-__prefix__-title
).
formset.html
<form action="{% url 'app2:formset_view' %}" method="post">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<p>{{ form }}</p>
{% endfor %}
<button type="button"
hx-trigger="click"
hx-get="{% url 'app2:add_formset' formset.total_form_count %}"
hx-swap="outerHTML">
Add formset
</button>
<input type="submit" value="Submit">
</form>
formset_partial.html
<input hx-swap-oob="true"
type="hidden"
name="form-TOTAL_FORMS"
value="{{ new_total_formsets }}"
id="id_form-TOTAL_FORMS">
<p>{{ new_formset }}</p>
<button type="button"
hx-trigger="click"
hx-get="{% url 'app2:add_formset' new_total_formsets %}"
hx-swap="outerHTML">
Add formset
</button>
Note re: the hidden input
: With every newly added formset, the value
of the input
element that has id="id_form-TOTAL_FORMS"
will no longer reflect the actual number of formsets on the page. You can send a new hidden input
with your formset and include hx-swap-oob="true"
on it. Htmx will then replace the old one with the new one.
Docs reference: https://docs.djangoproject.com/en/4.1/topics/forms/formsets/