Django Formset dynamic update

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!

If you want to try an alternative approach avoiding HTMX, you may use form collections with siblings:

12. Form Collectionsdjango-formset