Django Formset Dynamic Add/Remove Not Working for Colors, Sizes, and Images

Problem Statement

I am working on a Django formset where users can dynamically add/remove colors, sizes, and images when adding a product.

The structure of my formset is as follows:

  • A product can have multiple colors.
  • Each color can have multiple sizes.
  • Each color can have multiple images.
  • Users should be able to dynamically add/remove colors, sizes, and images using JavaScript.

Expected Behavior

  1. Clicking "Add Color" should create a new color form with its own size and image sections.
  2. Clicking "Add Size" within a color should add a new size only to that specific color.
  3. Clicking "Add Image" within a color should add a new image only to that specific color.
  4. Clicking "Remove Color" should remove the specific color section.

Current Issue

  • Only the "Remove Color" button works.
  • "Add Color", "Add Size", and "Add Image" buttons are not working.
  • No new elements are being appended to the DOM when clicking the buttons.

Debugging Attempts

✔ Checked Django Formset Management Forms (they exist).
✔ Verified event listeners are attached using console.log().
✔ Ensured cloneNode(true) properly copies elements.
✔ Checked for JavaScript errors in the console (no syntax errors).


Django Forms & Formsets

Forms (forms.py)

from django import forms
from django.forms import inlineformset_factory
from .models import VendorProduct, ProductColor, ProductSize, ProductImage, ColorImage

class ProductForm(forms.ModelForm):
    class Meta:
        model = VendorProduct
        fields = ['title', 'category', 'base_price']
        widgets = {
            'title': forms.TextInput(attrs={'class': 'form-control'}),
            'category': forms.Select(attrs={'class': 'form-control'}),
            'base_price': forms.NumberInput(attrs={'class': 'form-control'}),
        }

class ProductColorForm(forms.ModelForm):
    class Meta:
        model = ProductColor
        fields = ['color_name', 'color_code']
        widgets = {
            'color_name': forms.TextInput(attrs={'class': 'form-control'}),
            'color_code': forms.TextInput(attrs={'class': 'form-control'}),
        }

class ProductSizeForm(forms.ModelForm):
    class Meta:
        model = ProductSize
        fields = ['size_name', 'stock', 'price_increment']
        widgets = {
            'size_name': forms.TextInput(attrs={'class': 'form-control'}),
            'stock': forms.NumberInput(attrs={'class': 'form-control'}),
            'price_increment': forms.NumberInput(attrs={'class': 'form-control'}),
        }

class ProductImageForm(forms.ModelForm):
    class Meta:
        model = ProductImage
        fields = ['image']
        widgets = {
            'image': forms.FileInput(attrs={'class': 'form-control'}),
        }

class ColorImageForm(forms.ModelForm):
    class Meta:
        model = ColorImage
        fields = ['image']
        widgets = {
            'image': forms.FileInput(attrs={'class': 'form-control'}),
        }

# Formsets
ProductColorFormSet = inlineformset_factory(VendorProduct, ProductColor, form=ProductColorForm, extra=1, can_delete=True)
ProductImageFormSet = inlineformset_factory(VendorProduct, ProductImage, form=ProductImageForm, extra=1, can_delete=True)
ProductSizeFormSet = inlineformset_factory(ProductColor, ProductSize, form=ProductSizeForm, extra=1, can_delete=True)
ColorImageFormSet = inlineformset_factory(ProductColor, ColorImage, form=ColorImageForm, extra=1, can_delete=True)

HTML Template (add_product.html)

<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form.as_p }}

    <div id="colorContainer">
        {% for color_form in color_formset %}
            <div class="color-item">
                {{ color_form.color_name.label_tag }}
                {{ color_form.color_name }}

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

                <!-- Image Uploads -->
                <label>Color Images</label>
                {{ color_image_formset.management_form }}
                {% for image_form in color_image_formset %}
                    <div class="image-item">
                        {{ image_form.image }}
                    </div>
                {% endfor %}
                <button type="button" class="add-image-btn">Add Image</button>

                <!-- Size Section -->
                <div class="sizeContainer">
                    <h4>Sizes</h4>
                    {{ size_formset.management_form }}
                    {% for size_form in size_formset %}
                        <div class="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 Size</button>
                <button type="button" class="remove-btn">Remove Color</button>
            </div>
        {% endfor %}
    </div>

    <button type="button" id="addColorButton">Add Color</button>
    <button type="submit">Save</button>
</form>

JavaScript (dynamic.js)

    document.addEventListener("DOMContentLoaded", function () {
        // Initialize indexes
        let colorIndex = parseInt(document.querySelector('[name="colors-TOTAL_FORMS"]')?.value) || 0;
        let imageIndex = parseInt(document.querySelector('[name="images-TOTAL_FORMS"]')?.value) || 0;

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

            if (!totalForms) {
                console.error("Management form for colors-TOTAL_FORMS not found!");
                return;
            }

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

            // Attach event listeners to the new color block
            newColor.querySelector(".remove-btn").addEventListener("click", function () {
                removeColor(this);
            });

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

            colorContainer.appendChild(newColor);
            totalForms.value = colorIndex + 1; // Update Django formset tracker
            colorIndex++;
        }

        function addSize(button) {
            let sizeContainer = button.closest(".color-item").querySelector(".sizeContainer");
            let sizeIndex = sizeContainer.querySelectorAll(".size-item").length;

            let newSize = sizeContainer.querySelector(".size-item").cloneNode(true);
            newSize.querySelectorAll("input, select").forEach(input => {
                if (input.name) {
                    input.name = input.name.replace(/sizes-\d+/g, `sizes-${sizeIndex}`);
                    input.removeAttribute("id");
                    input.value = ""; // Clear input values
                }
            });

            sizeContainer.appendChild(newSize);
        }

        function removeColor(button) {
            let colorItem = button.closest(".color-item");
            colorItem.remove();

            // Update colors-TOTAL_FORMS count
            colorIndex--;
            document.querySelector('[name="colors-TOTAL_FORMS"]').value = colorIndex;
        }

        function addImage() {
            let imageContainer = document.getElementById("imageContainer");
            let totalForms = document.querySelector('[name="images-TOTAL_FORMS"]');

            if (!totalForms) {
                console.error("Management form for images-TOTAL_FORMS not found!");
                return;
            }

            let newImage = document.querySelector(".image-item").cloneNode(true);
            newImage.querySelectorAll("input").forEach(input => {
                if (input.name) {
                    input.name = input.name.replace(/images-\d+/g, `images-${imageIndex}`);
                    input.removeAttribute("id");
                    input.value = ""; // Clear input values
                }
            });

            imageContainer.appendChild(newImage);
            totalForms.value = imageIndex + 1; // Update Django formset tracker
            imageIndex++;
        }

        // Attach event listeners dynamically
        document.getElementById("addColorButton")?.addEventListener("click", addColor);
        document.getElementById("addImageButton")?.addEventListener("click", addImage);

        // Attach existing remove/add-size event listeners for initial elements
        document.querySelectorAll(".remove-btn").forEach(button => {
            button.addEventListener("click", function () {
                removeColor(this);
            });
        });

        document.querySelectorAll(".add-size-btn").forEach(button => {
            button.addEventListener("click", function () {
                addSize(this);
            });
        });
    });

Summary of Issues

  • Event listeners weren’t attached properly.
  • Formset names were not being updated dynamically.
  • JavaScript wasn’t using event delegation properly.
  • The script needed to handle both static and dynamically added elements.
Вернуться на верх