You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于JavaScript实现Django Formset动态增删表单的问题

Django Formset 动态增删表单的问题与修复方案

我需要实现点击按钮动态增删Django Formset中的表单,参考了旧的JavaScript示例,但在多Formset场景下,删除某个Formset的全部表单后,addForm函数因为没有可克隆的表单而失效。我的思路是初始化一个隐藏的模板表单,按需复制显示,同时默认不显示任何表单,以下是当前的实现代码:

当前HTML代码

<div class="selection">
  <div class="forms">
    <!-- {% for dict in formset.errors %}
      {% for error in dict.values %}
        {{ error }}
      {% endfor %}
    {% endfor %} -->

    <form id="general-formset" method="POST">
      {% csrf_token %}

      {{ generalFormset.management_form }}
      {% for form in generalFormset %}
      <div class="formset">
        {{ form.as_table }}
        <button id="remove-form" type="button" onclick="removeForm(this)" display="None">Remove Constraint</button>
      </div>
      {% endfor %}
      <button id="add-form" type="button" onclick="addForm(this)">Add Constraint</button>
    </form>

    <form id="specific-formset" method="POST">
      {% csrf_token %}

      {{ specificFormset.management_form }}
      {% for form in specificFormset %}
      <div class="formset">
        {{ form.as_table }}
        <button id="remove-form" type="button" onclick="removeForm(this)">Remove Constraint</button>
      </div>
      {% endfor %}
      <button id="add-form" type="button" onclick="addForm(this)" display="None">Add Constraint</button>
    </form>
    <button id="submit" type="button">Submit</button>
  </div>
</div>

当前JavaScript代码

let submitButton = document.querySelector("#submit")

submitButton.addEventListener('click', submitForms)

function submitForms(e){
  document.getElementById("specific-formset").submit();
  document.getElementById("general-formset").submit();
}

function addForm(e){
  let container = e.parentElement
  let form = container.querySelectorAll(".formset")[0].cloneNode(true)
  let formNum = container.querySelectorAll(".formset").length-1
  let formRegex = RegExp(`form-(\\d){1}-`,'g')
  let totalForms = container.querySelector("#id_form-TOTAL_FORMS")

  formNum++
  form.innerHTML = form.innerHTML.replace(formRegex, `form-${formNum}-`)
  container.insertBefore(form, e)
      
  totalForms.setAttribute('value', `${formNum+1}`)
}

function removeForm(e){
  let parent = e.parentElement
  let container = parent.parentElement
  let formNum = container.querySelectorAll(".formset").length-1
  let totalForms = container.querySelector("#id_form-TOTAL_FORMS")
    
  parent.remove()
  formNum--
  totalForms.setAttribute('value', `${formNum-1}`)
}

修复方案

核心是给每个Formset添加隐藏的模板表单(利用Django Formset自带的empty_form),作为永久的克隆源,同时用独立容器管理动态表单,解决多Formset冲突和无表单可克隆的问题,还能实现默认不显示任何表单的需求。

修改后的HTML代码

<div class="selection">
  <div class="forms">
    <!-- {% for dict in formset.errors %}
      {% for error in dict.values %}
        {{ error }}
      {% endfor %}
    {% endfor %} -->

    <form id="general-formset" method="POST">
      {% csrf_token %}
      {{ generalFormset.management_form }}
      <!-- 隐藏的模板表单,仅用于克隆 -->
      <div class="formset-template" style="display: none;">
        {{ generalFormset.empty_form.as_table }}
        <button class="remove-form" type="button" onclick="removeForm(this)">Remove Constraint</button>
      </div>
      <!-- 动态表单容器,默认空 -->
      <div class="formset-container"></div>
      <button class="add-form" type="button" onclick="addForm(this)">Add Constraint</button>
    </form>

    <form id="specific-formset" method="POST">
      {% csrf_token %}
      {{ specificFormset.management_form }}
      <!-- 隐藏的模板表单 -->
      <div class="formset-template" style="display: none;">
        {{ specificFormset.empty_form.as_table }}
        <button class="remove-form" type="button" onclick="removeForm(this)">Remove Constraint</button>
      </div>
      <!-- 动态表单容器,默认空 -->
      <div class="formset-container"></div>
      <button class="add-form" type="button" onclick="addForm(this)">Add Constraint</button>
    </form>
    <button id="submit" type="button">Submit</button>
  </div>
</div>

修改后的JavaScript代码

let submitButton = document.querySelector("#submit")
submitButton.addEventListener('click', submitForms)

function submitForms(e){
  // 注意:连续提交两个表单会触发两次页面跳转,建议合并表单或改用AJAX提交
  document.getElementById("specific-formset").submit();
  document.getElementById("general-formset").submit();
}

function addForm(e){
  const form = e.closest('form');
  const template = form.querySelector('.formset-template');
  const container = form.querySelector('.formset-container');
  const totalFormsInput = form.querySelector('[id$="-TOTAL_FORMS"]');
  
  // 获取当前总表单数作为新表单的索引
  const formNum = parseInt(totalFormsInput.value);
  // 克隆模板并显示
  const newForm = template.cloneNode(true);
  newForm.classList.remove('formset-template');
  newForm.style.display = 'block';
  
  // 替换模板中的__prefix__占位符为实际索引
  const prefixRegex = new RegExp(`__prefix__`, 'g');
  newForm.innerHTML = newForm.innerHTML.replace(prefixRegex, formNum);
  
  // 添加到动态容器
  container.appendChild(newForm);
  // 更新总表单数
  totalFormsInput.value = formNum + 1;
}

function removeForm(e){
  const form = e.closest('form');
  const formElement = e.closest('div');
  const container = form.querySelector('.formset-container');
  const totalFormsInput = form.querySelector('[id$="-TOTAL_FORMS"]');
  
  // 删除当前表单
  formElement.remove();
  // 更新总表单数(Django允许总数大于实际数量,但保持一致更合理)
  totalFormsInput.value = container.children.length;
}

关键改进点

  • 使用Django Formset的empty_form生成模板,无需手动重复编写表单结构
  • 为每个Formset独立设置模板和容器,避免多Formset之间的DOM选择冲突
  • 用__prefix__占位符替换索引,比固定数字正则更适配Django的Formset命名规则
  • 修复原代码中removeForm函数的总表单数计算错误
  • 默认动态容器为空,实现默认不显示任何表单的需求

内容的提问来源于stack exchange,提问作者evan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 00:12:16