I am struggling with formsets when trying to dynamically add forms using htmx.
In the following example, I can’t get the formset to read the data from the added rows.
SubjectFormSet = formset_factory(SubjectForm, extra=1)
def grade(request):
result = None
weight_error = None
formset = SubjectFormSet(request.POST or None)
if request.headers.get("HX-Request") and request.GET.get("add_form") == "1":
new_index = formset.total_form_count()
empty_form = SubjectForm(prefix=f'form-{new_index}')
return render(request, "app/grade-calculator.html", {"form": empty_form, "is_partial": True})
if request.method == "POST":
if formset.is_valid():
total_weight = sum(form.cleaned_data.get("weight", 0) for form in formset)
print(total_weight)
if total_weight != 100:
weight_error = "Total weight must equal 100%"
else:
weighted_sum = sum(
form.cleaned_data["marks"] * form.cleaned_data["weight"] / 100
for form in formset
)
avg = weighted_sum
grade_value = (
"A" if avg >= 90 else
"B" if avg >= 80 else
"C" if avg >= 70 else
"D" if avg >= 60 else "F"
)
result = {"average": avg, "grade": grade_value}
return render(request, "app/grade-calculator.html", {
"formset": formset,
"result": result,
"weight_error": weight_error,
"is_partial": False,
})
Are you updating the TOTAL_FORMS value in the ManagementForm?
Have you verified the HTML that has been rendered and returned to the browser is complete and being added in the right spot?
Yes, I believe I am updating it. The HTML looks fine and in the right spot.
<form method="post" class="space-y-4" >
{% csrf_token %}
{{ formset.management_form }}
<div id="subjects" class="space-y-3">
{% for form in formset %}
{% partial subject_row %}
{% endfor %}
</div>
<!-- Add Subject Button -->
<button type="button"
class="px-3 py-1 bg-green-500 text-white rounded"
hx-get="{% url 'grade' %}?add_form=1"
hx-target="#subjects"
hx-swap="beforeend"
hx-on="htmx:afterSwap: document.getElementById('id_form-TOTAL_FORMS').value = parseInt(document.getElementById('id_form-TOTAL_FORMS').value) + 1">
+ Add Subject
</button>
{% if weight_error %}
<p class="text-red-500 font-semibold">{{ weight_error }}</p>
{% endif %}
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded" >
Calculate
</button>
</form>
Please post the HTML for the formset as it exists in the browser after a form has been added.
I added one form and copied the body of the html, I hope that is what you needed.
<body>
<!-- Partial for a single subject row -->
<div class="max-w-xl mx-auto bg-white p-6 rounded shadow">
<h2 class="text-xl font-bold mb-4">Weighted Grade Calculator</h2>
<form method="post" class="space-y-4">
<input type="hidden" name="csrfmiddlewaretoken" value="AEvQWwPra2dooyNthk1FUOYbI6mCvm5CrEDmPzzhODdjH17XExHRtigtWfTrNxcB">
<input type="hidden" name="form-TOTAL_FORMS" value="1" id="id_form-TOTAL_FORMS"><input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS"><input type="hidden" name="form-MIN_NUM_FORMS" value="0" id="id_form-MIN_NUM_FORMS"><input type="hidden" name="form-MAX_NUM_FORMS" value="1000" id="id_form-MAX_NUM_FORMS">
<div id="subjects" class="space-y-3">
<div class="grid grid-cols-3 gap-4 bg-gray-50 p-3 rounded shadow subject-row">
<input type="text" name="form-0-subject" class="w-full rounded border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500" maxlength="50" id="id_form-0-subject">
<input type="number" name="form-0-marks" class="w-full rounded border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500" min="0" max="100" id="id_form-0-marks">
<div class="relative">
<input type="number" name="form-0-weight" class="w-full rounded border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 pr-6 text-right" min="0" max="100" step="any" id="id_form-0-weight">
<span class="absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none">%</span>
</div>
</div>
<!-- Partial for a single subject row -->
<div class="grid grid-cols-3 gap-4 bg-gray-50 p-3 rounded shadow subject-row">
<input type="text" name="form-1-subject" class="w-full rounded border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500" maxlength="50" required="" id="id_form-1-subject">
<input type="number" name="form-1-marks" class="w-full rounded border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500" min="0" max="100" required="" id="id_form-1-marks">
<div class="relative">
<input type="number" name="form-1-weight" class="w-full rounded border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 pr-6 text-right" min="0" max="100" step="any" required="" id="id_form-1-weight">
<span class="absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none">%</span>
</div>
</div>
<!-- Partial for a subject row -->
<script src="https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.js"></script>
</div>
<!-- Add Subject Button -->
<button type="button" class="px-3 py-1 bg-green-500 text-white rounded" hx-get="/grade-calculator?add_form=1" hx-target="#subjects" hx-swap="beforeend" hx-on="htmx:afterSwap: document.getElementById('id_form-TOTAL_FORMS').value = parseInt(document.getElementById('id_form-TOTAL_FORMS').value) + 1">
+ Add Subject
</button>
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded">
Calculate
</button>
</form>
<div id="results">
</div>
</div>
<!-- Partial for a subject row -->
<script src="https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.js"></script>
</body>
.
You’ve got two forms:
But your TOTAL_FORMS is still only showing 1.
You are absolutely correct. I can’t get it to increment for some reason.
I got it to work after some tinkering. Much appreciated as always!
jrief
8
If you want to try an alternative approach avoiding HTMX, you may use form collections with siblings:
12. Form Collectionsdjango-formset