Django Formset Nested Structure Not Posting Correctly for Dynamic Fields

I’m working on a Django nested formset where users can:

  • Add multiple colors to a product.
  • For each color, add multiple sizes dynamically using JavaScript.
  • Each size should have its own size_name, stock, and price_increment field.

Issue

When submitting the form, Django is incorrectly grouping multiple size field values into lists instead of treating them as separate entries.

Expected Django POST Data (Correct Structure)

sizes-0-0-size_name = "Small"
sizes-0-0-stock = "100"
sizes-0-0-price_increment = "50"

sizes-0-1-size_name = "Medium"
sizes-0-1-stock = "150"
sizes-0-1-price_increment = "75"

Actual Django POST Data (Incorrect Structure)

sizes-0-0-size_name = ["Small", "Medium"]
sizes-0-0-stock = ["100", "150"]
sizes-0-0-price_increment = ["50", "75"]
  • Instead of separate fields for each size, Django is grouping values into a single list.
  • The sizes-0-TOTAL_FORMS field is appearing twice in the POST request, which might indicate a JavaScript duplication issue.

Debugging the Request Data (request.POST)

<QueryDict: {
    'colors-TOTAL_FORMS': ['1'],
    'sizes-0-TOTAL_FORMS': ['1', '1'],  # This should be a single value, not duplicated
    'sizes-0-0-size_name': ['Small', 'Medium'],
    'sizes-0-0-stock': ['100', '150'],
    'sizes-0-0-price_increment': ['50', '75']
}>

Potential Causes:

  1. JavaScript Issue:

    • Dynamic form addition might be incorrectly naming inputs, causing Django to interpret multiple values as a list.
    • TOTAL_FORMS for sizes might not be updated properly, leading to duplicate values.
  2. Django Formset Issue:

    • Django might not be detecting individual size inputs properly due to incorrect prefix handling.

Code Implementation

Forms (forms.py)

class ProductForm(forms.ModelForm):
    class Meta:
        model = VendorProduct
        fields = ['title', 'cagtegory', 'base_price']

class ProductColorForm(forms.ModelForm):
    class Meta:
        model = ProductColor
        fields = ['color_name', 'color_code']

class ProductSizeForm(forms.ModelForm):
    class Meta:
        model = ProductSize
        fields = ['size_name', 'stock', 'price_increment']

ProductColorFormSet = inlineformset_factory(
    VendorProduct, ProductColor, form=ProductColorForm, extra=1, can_delete=True
)
ProductSizeFormSet = inlineformset_factory(
    ProductColor, ProductSize, form=ProductSizeForm, extra=1, can_delete=True
)

View (views.py)

@login_required
def add_product(request):
    if request.method == 'POST':
        product_form = ProductForm(request.POST)
        color_formset = ProductColorFormSet(request.POST, prefix='colors')

        if product_form.is_valid() and color_formset.is_valid():
            product = product_form.save()
            for color_index, color_form in enumerate(color_formset):
                if color_form.cleaned_data.get('color_name'):
                    color = color_form.save(commit=False)
                    color.product = product
                    color.save()

                    # **Check if sizes are structured properly**
                    size_formset = ProductSizeFormSet(
                        request.POST, instance=color, prefix=f'sizes-{color_index}'
                    )
                    print(f"Processing sizes for color index {color_index}:")
                    print(request.POST)

                    if size_formset.is_valid():
                        size_formset.save()

            return redirect('vendorpannel:vendor_shop')

    else:
        product_form = ProductForm()
        color_formset = ProductColorFormSet(prefix='colors')
        color_size_formsets = [
            ProductSizeFormSet(instance=color_form.instance, prefix=f'sizes-{index}')
            for index, color_form in enumerate(color_formset.forms)
        ]

    return render(request, 'vendorpannel/add-product.html', {
        'product_form': product_form,
        'color_formset': color_formset,
        'color_size_formsets': color_size_formsets,
    })

JavaScript for Dynamic Form Handling (add_product.html)

document.addEventListener("DOMContentLoaded", function () {
    let colorIndex = document.querySelectorAll(".color-item").length;
    function addColor() {
        let totalForms = document.querySelector('[name="colors-TOTAL_FORMS"]');
        let newColor = document.querySelector(".color-item").cloneNode(true);
        newColor.querySelectorAll("input").forEach(input => {
            input.name = input.name.replace(/colors-\d+/g, `colors-${colorIndex}`);
            input.value = "";
        });

        let sizeContainer = newColor.querySelector(".sizeContainer");
        sizeContainer.innerHTML = "";

        let sizeTotalForms = document.createElement("input");
        sizeTotalForms.type = "hidden";
        sizeTotalForms.name = `sizes-${colorIndex}-TOTAL_FORMS`;
        sizeTotalForms.value = "0";
        sizeContainer.appendChild(sizeTotalForms);

        document.getElementById("colorContainer").appendChild(newColor);
        totalForms.value = colorIndex + 1;
        colorIndex++;
    }

    document.getElementById("addColorButton")?.addEventListener("click", addColor);
});

What I’ve Tried:

✅ Ensured sizes-{colorIndex}-TOTAL_FORMS exists before adding sizes dynamically.
✅ Used name.replace() correctly to update input names.
✅ Verified prefix usage in Django forms and formsets.


Question:

How can I ensure that each size input field gets a unique name instead of Django grouping multiple values into lists?


Full template which is rendering the formsets

{% extends 'vendorpannel/base.html' %}

{% load static %}

{% block title %}Dashboard{% endblock %}

{% load humanize %}

{% block content %}
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Product Upload Form</title>
</head>
<body>
   <div class="container1">
      <h1>Product Upload</h1>
      <form method="post" enctype="multipart/form-data">
         {% csrf_token %}

         <label for="id_title">Product Title</label>
         {{ product_form.title }}

         <label for="id_category">Category</label>
         {{ product_form.cagtegory }}

         <label for="id_base_price">Base Price</label>
         {{ product_form.base_price }}

         <!-- Color Section -->
        <div id="colorContainer">
            <h3>Colors</h3>
            {{ color_formset.management_form }}

            {% for color_form, size_formset in color_size_formsets %}
                <div class="dynamic-item color-item">
                    {{ color_form.color_name.label_tag }}
                    {{ color_form.color_name }}

                    {{ color_form.color_code.label_tag }}
                    {{ color_form.color_code }}

                    <!-- Size Section -->
                    <div class="sizeContainer">
                        <h4>Sizes</h4>
                        {{ size_formset.management_form }}
                        {% for size_form in size_formset %}
                            <div class="dynamic-item size-item">
                                {{ size_form.size_name.label_tag }}
                                {{ size_form.size_name }}

                                {{ size_form.stock.label_tag }}
                                {{ size_form.stock }}

                                {{ size_form.price_increment.label_tag }}
                                {{ size_form.price_increment }}
                            </div>
                        {% endfor %}
                    </div>

                    <button type="button" class="add-size-btn add-btn">Add Size</button>
                    <button type="button" class="remove-btn" onclick="removeColor(this)">Remove Color</button>
                </div>
            {% endfor %}
        </div>

         <button type="button" id="addColorButton" class="add-btn">Add Another Color</button>


         <!-- Additional Product Images -->
         <div id="imageContainer">
            <h3>Additional Product Images</h3>
            {{ image_formset.management_form }}
            {% for image_form in image_formset %}
               <div class="dynamic-item image-item">
                  {{ image_form.image }}
               </div>
            {% endfor %}
         </div>
         <button type="button" id="addImageButton" class="add-btn">Add Another Image</button>

         <!-- Submit -->
         <button type="submit">Submit Product</button>
      </form>
   </div>

    <script>
    document.addEventListener("DOMContentLoaded", function () {
        let colorIndex = document.querySelectorAll(".color-item").length;

        function addColor() {
            let colorContainer = document.getElementById("colorContainer");
            let totalForms = document.querySelector('[name="colors-TOTAL_FORMS"]');

            let newColor = document.querySelector(".color-item").cloneNode(true);
            newColor.querySelectorAll("input, select").forEach(input => {
                input.name = input.name.replace(/colors-\d+/g, `colors-${colorIndex}`);
                input.removeAttribute("id");
                input.value = "";
            });

            let sizeContainer = newColor.querySelector(".sizeContainer");
            sizeContainer.innerHTML = "";

            // 🔹 Ensure TOTAL_FORMS for sizes exists
            let sizeTotalForms = document.createElement("input");
            sizeTotalForms.setAttribute("type", "hidden");
            sizeTotalForms.setAttribute("name", `sizes-${colorIndex}-TOTAL_FORMS`);
            sizeTotalForms.setAttribute("value", "0");
            sizeTotalForms.classList.add("size-total-forms");
            sizeContainer.appendChild(sizeTotalForms);

            newColor.querySelector(".add-size-btn").addEventListener("click", function () {
                addSize(this, colorIndex);
            });

            colorContainer.appendChild(newColor);
            totalForms.value = colorIndex + 1;
            colorIndex++;
        }

        function addSize(button, colorIdx) {
            let colorItem = button.closest(".color-item");
            let sizeContainer = colorItem.querySelector(".sizeContainer");

            // 🔹 Ensure TOTAL_FORMS for this color exists
            let totalForms = sizeContainer.querySelector('.size-total-forms');
            if (!totalForms) {
                console.warn(`Creating missing totalForms field for sizes-${colorIdx}-TOTAL_FORMS`);
                totalForms = document.createElement("input");
                totalForms.setAttribute("type", "hidden");
                totalForms.setAttribute("name", `sizes-${colorIdx}-TOTAL_FORMS`);
                totalForms.setAttribute("value", "0");
                totalForms.classList.add("size-total-forms");
                sizeContainer.appendChild(totalForms);
            }

            let sizeIndex = parseInt(totalForms.value);
            let newSize = document.querySelector(".size-item").cloneNode(true);

            newSize.querySelectorAll("input, select").forEach(input => {
                let fieldType = input.getAttribute("name").split("-").pop();
                input.name = `sizes-${colorIdx}-${sizeIndex}-${fieldType}`;
                input.removeAttribute("id");
                input.value = "";
            });

            sizeContainer.appendChild(newSize);
            totalForms.value = sizeIndex + 1;
        }

        document.getElementById("addColorButton")?.addEventListener("click", addColor);
        document.querySelectorAll(".add-size-btn").forEach(button => {
            button.addEventListener("click", function () {
                let colorIdx = button.closest(".color-item").querySelector('[name^="colors-"]').name.match(/colors-(\d+)/)[1];
                addSize(this, colorIdx);
            });
        });
    });

    </script>



</body>
</html>
{% endblock %}

You need to group the color_size_formsets per color_formset, so:

@login_required
def add_product(request):
    if request.method == 'POST':
        # ...
    else:
        product_form = ProductForm()
        color_formset = ProductColorFormSet(prefix='colors')
        for index, color_form in enumerate(color_formset.forms):
            color_form.size_formset = ProductSizeFormSet(instance=color_form.instance, prefix=f'sizes-{index}')

    return render(request, 'vendorpannel/add-product.html', {
        'product_form': product_form,
        'color_formset': color_formset,
    })

this is important because otherwise you each time render all ColorSizeFormSets in the list per color_form.

The template then has:

{% for color_form in color_formset %}

    <div class="dynamic-item color-item">
        {{ color_form.color_name.label_tag }}
        {{ color_form.color_name }}

        {{ color_form.color_code.label_tag }}
        {{ color_form.color_code }}

        <!-- Size Section -->
        <div class="sizeContainer">
            <h4>Sizes</h4>
            {{ color_form.size_formset.management_form }}
            {% for size_form in color_form.size_formset  %}
                <div class="dynamic-item size-item">
                    {{ size_form.size_name.label_tag }}
                    {{ size_form.size_name }}

                    {{ size_form.stock.label_tag }}
                    {{ size_form.stock }}

                    {{ size_form.price_increment.label_tag }}
                    {{ size_form.price_increment }}
                </div>
            {% endfor %}
        </div>

        <button type="button" class="add-size-btn add-btn">Add Size</button>
        <button type="button" class="remove-btn" onclick="removeColor(this)">Remove Color</button>
    </div>
{% endfor %}
Вернуться на верх