基于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
相关产品推荐
相关产品推荐

