I’m working on a Django nested formset where users can:
size_name
, stock
, and price_increment
field.When submitting the form, Django is incorrectly grouping multiple size field values into lists instead of treating them as separate entries.
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"
sizes-0-0-size_name = ["Small", "Medium"]
sizes-0-0-stock = ["100", "150"]
sizes-0-0-price_increment = ["50", "75"]
sizes-0-TOTAL_FORMS
field is appearing twice in the POST request, which might indicate a JavaScript duplication issue.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']
}>
JavaScript Issue:
TOTAL_FORMS
for sizes might not be updated properly, leading to duplicate values.Django Formset Issue:
prefix
handling.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
)
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,
})
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);
});
✅ 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.
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 %}