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
, andprice_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:
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.
Django Formset Issue:
- Django might not be detecting individual size inputs properly due to incorrect
prefix
handling.
- Django might not be detecting individual size inputs properly due to incorrect
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 ColorSizeFormSet
s 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 %}