在 django formset 中动态添加行
Posted
技术标签:
【中文标题】在 django formset 中动态添加行【英文标题】:Add row dynamically in django formset 【发布时间】:2020-07-22 22:05:09 【问题描述】:在我的 django 应用程序中,我有两个模型,即 Player
和 Team
,它们通过多对多关系连接。要在我的表格中动态添加数据,我想使用 javascript 在我的表单中添加 Add row
或 Remove Row
按钮,但无法这样做。
以下是详细信息:
模型.py
class Player(models.Model):
pname = models.CharField(max_length=50)
hscore = models.IntegerField()
age = models.IntegerField()
def __str__(self):
return self.pname
class Team(models.Model):
tname = models.CharField(max_length=100)
player= models.ManyToManyField(Player)
def __str__(self):
return self.tname
Forms.py
class PlayerForm(forms.Form):
pname = forms.CharField()
hscore= forms.IntegerField()
age = forms.IntegerField()
PlayerFormset= formset_factory(PlayerForm)
class TeamForm(forms.Form):
tname= forms.CharField()
player= PlayerFormset()
Views.py
def post(request):
if request.POST:
form = TeamForm(request.POST)
form.player_instances = PlayerFormset(request.POST)
if form.is_valid():
team= Team()
team.tname= form.cleaned_data['tname']
team.save()
if form.player_instances.cleaned_data is not None:
for item in form.player_instances.cleaned_data:
player = Player()
player.pname= item['pname']
player.hscore= item['hscore']
player.age= item['age']
player.save()
team.player.add(player)
team.save()
else:
form = TeamForm()
return render(request, 'new.html', 'form':form)
新的.html
<html>
<head>
<title>gffdfdf</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="/static/jquery.formset.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<form id="myForm" action="" method="post" class="">
% csrf_token %
<h2> Team</h2>
% for field in form %
field.errors
field.label_tag : field
% endfor %
form.player.management_form
<h3> Product Instance(s)</h3>
<table id="table-product" class="table">
<thead>
<tr>
<th>player name</th>
<th>highest score</th>
<th>age</th>
</tr>
</thead>
% for player in form.player %
<tbody class="player-instances">
<tr>
<td> player.pname </td>
<td> player.hscore </td>
<td> player.age </td>
</tr>
</tbody>
% endfor %
</table>
<button type="submit" class="btn btn-primary">save</button>
</form>
</div>
<script>
$(function ()
$('#myForm tbody tr').formset();
)
</script>
</body>
</html>
如何使用javascript添加或删除多对多关系连接的行?
上面的代码为我们提供了以下信息:
【问题讨论】:
【参考方案1】:为了保持简单和通用,我将 OP 的示例简化为单个模型和基本表单集,没有 Team
-Player
多对多关系。 JavaScript 部分的原理保持不变。如果你确实想实现多对多关系,你可以使用例如一个inline formset,正如here所解释的那样。
所以,假设我们有一个简单的模型:
class Player(models.Model):
name = models.CharField(max_length=50)
age = models.IntegerField()
我们的视图可能如下所示(基于docs 中的示例):
def my_formset_view(request):
response = None
formset_class = modelformset_factory(
model=Player, fields=('name', 'age'), extra=0, can_delete=True)
if request.method == 'POST':
formset = formset_class(data=request.POST)
if formset.is_valid():
formset.save()
response = redirect(to='my_success_view')
else:
formset = formset_class()
if response is None:
response = render(
request, 'myapp/my_formset_template.html', dict(formset=formset))
return response
下面的 my_formset_template.html
django 模板(跳过样板)使我们能够添加和删除 formset-forms:
...
<template id="id_formset_empty_form"> formset.empty_form </template>
<form method="post" id="id_html_form" autocomplete="off">
% csrf_token %
<table id="id_formset_container">
formset
</table>
<div id="id_formset_add_button" style="text-decoration: underline; cursor: pointer;">Add</div>
<input id="id_formset_submit_button" type="submit" value="Submit">
</form>
...
HTML <template> 元素可以轻松地从 formset.empty_form
复制内容。
旁注:如果我们不设置autocomplete="off"
,浏览器会在管理表单上缓存TOTAL_FORMS
的值,即使在重新加载页面后。
现在,下面的 JavaScript 为我完成了这项工作(没有尝试优化,我只是想让它易于阅读):
window.addEventListener('load', (event) =>
// get form template and total number of forms from management form
const templateForm = document.getElementById('id_formset_empty_form');
const inputTotalForms = document.querySelector('input[id$="-TOTAL_FORMS"]');
const inputInitialForms = document.querySelector('input[id$="-INITIAL_FORMS"]');
// get our container (e.g. <table>, <ul>, or <div>) and "Add" button
const containerFormSet = document.getElementById('id_formset_container');
const buttonAdd = document.getElementById('id_formset_add_button');
const buttonSubmit = document.getElementById('id_formset_submit_button');
// event handlers
buttonAdd.onclick = addForm;
buttonSubmit.onclick = updateNameAttributes;
// form counters (note: proper form index bookkeeping is necessary
// because django's formset will create empty forms for any missing
// indices, and will discard forms with indices >= TOTAL_FORMS, which can
// lead to funny behavior in some edge cases)
const initialForms = Number(inputInitialForms.value);
let extraFormIndices = [];
let nextFormIndex = initialForms;
function addForm ()
// create DocumentFragment from template
const formFragment = templateForm.content.cloneNode(true);
// a django form is rendered as_table (default), as_ul, or as_p, so
// the fragment will contain one or more <tr>, <li>, or <p> elements,
// respectively.
for (let element of formFragment.children)
// replace the __prefix__ placeholders from the empty form by the
// actual form index
element.innerHTML = element.innerHTML.replace(
/(?<=\w+-)(__prefix__|\d+)(?=-\w+)/g,
nextFormIndex.toString());
// add a custom attribute to simplify bookkeeping
element.dataset.formIndex = nextFormIndex.toString();
// add a delete click handler (if formset can_delete)
setDeleteHandler(element);
// move the fragment's children onto the DOM
// (the fragment is empty afterwards)
containerFormSet.appendChild(formFragment);
// keep track of form indices
extraFormIndices.push(nextFormIndex++);
function removeForm (event)
// remove all elements with form-index matching that of the delete-input
const formIndex = event.target.dataset.formIndex;
for (let element of getFormElements(formIndex))
element.remove();
// remove form index from array
let indexIndex = extraFormIndices.indexOf(Number(formIndex));
if (indexIndex > -1)
extraFormIndices.splice(indexIndex, 1);
function setDeleteHandler (containerElement)
// modify DELETE checkbox in containerElement, if the checkbox exists
// (these checboxes are added by formset if can_delete)
const inputDelete = containerElement.querySelector('input[id$="-DELETE"]');
if (inputDelete)
// duplicate the form index instead of relying on parentElement (more robust)
inputDelete.dataset.formIndex = containerElement.dataset.formIndex;
inputDelete.onclick = removeForm;
function getFormElements(index)
// the data-form-index attribute is available as dataset.formIndex
// https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes#javascript_access
return containerFormSet.querySelectorAll('[data-form-index="' + index + '"]');
function updateNameAttributes (event)
// make sure the name indices are consecutive and smaller than
// TOTAL_FORMS (the name attributes end up as dict keys on the server)
// note we do not need to update the indices in the id attributes etc.
for (let [consecutiveIndex, formIndex] of extraFormIndices.entries())
for (let formElement of getFormElements(formIndex))
for (let element of formElement.querySelectorAll('input, select'))
if ('name' in element)
element.name = element.name.replace(
/(?<=\w+-)(__prefix__|\d+)(?=-\w+)/g,
(initialForms + consecutiveIndex).toString());
updateTotalFormCount();
function updateTotalFormCount (event)
// note we could simply do initialForms + extraFormIndices.length
// to get the total form count, but that does not work if we have
// validation errors on forms that were added dynamically
const firstElement = templateForm.content.querySelector('input, select');
// select the first input or select element, then count how many ids
// with the same suffix occur in the formset container
if (firstElement)
let suffix = firstElement.id.split('__prefix__')[1];
let selector = firstElement.tagName.toLowerCase() + '[id$="' + suffix + '"]';
let allElementsForId = containerFormSet.querySelectorAll(selector);
// update total form count
inputTotalForms.value = allElementsForId.length;
, false);
请注意,简单地添加和删除表单集表单并没有那么复杂,直到出现问题:上面大约一半的行与处理边缘情况有关,例如动态添加的表单验证失败。
【讨论】:
以上是关于在 django formset 中动态添加行的主要内容,如果未能解决你的问题,请参考以下文章
如何在同一页面中同时使用 Django Dynamic Formset 和 Select2?