动态地向Django表单集添加表单

我想动态地向Django表单集添加新表单,这样当用户点击“;添加”;按钮,它运行JavaScript,向页面添加一个新表单(这是表单集的一部分)。

98614 次浏览

一种选择是创建一个包含所有可能表单的表单集,但最初将不需要的表单设置为隐藏——即display: none;。当需要显示一个表单时,将它的css显示设置为block或任何合适的值。

如果不了解更多关于“Ajax”正在做什么的细节,就很难给出更详细的回应。

模拟和模仿:

  • 创建一个对应于之前点击“添加”按钮的情况的表单集。
  • 加载页面,查看源代码并记录所有<input>字段。
  • 修改格式集以对应于单击“添加”按钮的情况(更改额外字段的数量)。
  • 加载页面,查看源代码,并注意<input>字段的变化。
  • 创建一些JavaScript,以适当的方式修改DOM,将其从之前状态移动到状态。
  • 将该JavaScript附加到“add”按钮。

虽然我知道表单集使用特殊的隐藏<input>字段,并且大约知道脚本必须做什么,但我不记得我的脑袋上的细节。我上面所描述的就是我在你的情况下会做的事情。

我是这样做的,使用jQuery:

我的模板:

<h3>My Services</h3>
\{\{ serviceFormset.management_form }}
{% for form in serviceFormset.forms %}
<div class='table'>
<table class='no_error'>
\{\{ form.as_table }}
</table>
</div>
{% endfor %}
<input type="button" value="Add More" id="add_more">
<script>
$('#add_more').click(function() {
cloneMore('div.table:last', 'service');
});
</script>

在javascript文件中:

function cloneMore(selector, type) {
var newElement = $(selector).clone(true);
var total = $('#id_' + type + '-TOTAL_FORMS').val();
newElement.find(':input').each(function() {
var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
var id = 'id_' + name;
$(this).attr({'name': name, 'id': id}).val('').removeAttr('checked');
});
newElement.find('label').each(function() {
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('for', newFor);
});
total++;
$('#id_' + type + '-TOTAL_FORMS').val(total);
$(selector).after(newElement);
}

它的作用:

cloneMore接受selector作为第一个参数,formset的type作为第二个参数。selector应该做的是将它应该复制的内容传递给它。在本例中,我向它传递div.table:last,以便jQuery查找带有table类的最后一个表。它的:last部分很重要,因为selector还用于确定新表单之后将插入什么。您更可能希望它出现在其余表单的末尾。type参数是为了更新management_form字段,特别是selector0,以及实际的表单字段。如果你有一个充满selector1模型的表单集,管理字段的id将是selector2和selector3,而表单字段的格式将是selector4,其中selector5是表单号,从selector6开始。因此,使用type参数,cloneMore函数查看当前有多少个表单,并遍历新表单中的每个输入和标签,替换从selector9到type0等所有字段名/id。完成后,它更新selector0字段以反映新表单,并将其添加到集合的末尾。

这个功能对我来说特别有帮助,因为它的设置方式允许我在整个应用程序中使用它,当我想在一个格式集中提供更多的表单时,并且不需要我有一个隐藏的“模板”表单来复制,只要我传递它的格式集名称和表单布局的格式。希望能有所帮助。

Paolo的建议很好用,但有一点需要注意——浏览器的后退/前进按钮。

如果用户使用后退/前进按钮返回到表单集,那么用Paolo的脚本创建的动态元素将不会被呈现。对一些人来说,这个问题可能是交易的破坏者。

例子:

1)用户使用“add-more”按钮向表单集中添加两个新表单

2)用户填充表单并提交表单集

3)用户在浏览器中点击后退按钮

4)形式集现在减少到原来的形式,所有动态添加的形式不存在

这根本不是保罗剧本的缺陷;但是dom操作和浏览器缓存是一个现实。

我认为可以在会话中存储表单的值,并在表单集加载时再次创建元素并从会话中重新加载值;但这取决于你想要的相同用户和表单的多个实例,这可能会变得非常复杂。

有人有好的建议吗?

谢谢!

克隆莫尔函数有一个小问题。因为它也会清除django自动生成的隐藏字段的值,如果你试图保存一个包含多个空表单的表单集,它会导致django报错。

这里有一个解决方案:

function cloneMore(selector, type) {
var newElement = $(selector).clone(true);
var total = $('#id_' + type + '-TOTAL_FORMS').val();
newElement.find(':input').each(function() {
var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
var id = 'id_' + name;


if ($(this).attr('type') != 'hidden') {
$(this).val('');
}
$(this).attr({'name': name, 'id': id}).removeAttr('checked');
});
newElement.find('label').each(function() {
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('for', newFor);
});
total++;
$('#id_' + type + '-TOTAL_FORMS').val(total);
$(selector).after(newElement);
}

另一个cloneMore版本,允许对字段进行选择性消毒。当您需要防止多个字段被擦除时使用它。

$('table tr.add-row a').click(function() {
toSanitize = new Array('id', 'product', 'price', 'type', 'valid_from', 'valid_until');
cloneMore('div.formtable table tr.form-row:last', 'form', toSanitize);
});


function cloneMore(selector, type, sanitize) {
var newElement = $(selector).clone(true);
var total = $('#id_' + type + '-TOTAL_FORMS').val();
newElement.find(':input').each(function() {
var namePure = $(this).attr('name').replace(type + '-' + (total-1) + '-', '');
var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
var id = 'id_' + name;
$(this).attr({'name': name, 'id': id}).removeAttr('checked');


if ($.inArray(namePure, sanitize) != -1) {
$(this).val('');
}


});
newElement.find('label').each(function() {
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('for', newFor);
});
total++;
$('#id_' + type + '-TOTAL_FORMS').val(total);
$(selector).after(newElement);
}

Paolo的答案的简化版本,使用empty_form作为模板。

<h3>My Services</h3>
\{\{ serviceFormset.management_form }}
<div id="form_set">
{% for form in serviceFormset.forms %}
<table class='no_error'>
\{\{ form.as_table }}
</table>
{% endfor %}
</div>
<input type="button" value="Add More" id="add_more">
<div id="empty_form" style="display:none">
<table class='no_error'>
\{\{ serviceFormset.empty_form.as_table }}
</table>
</div>
<script>
$('#add_more').click(function() {
var form_idx = $('#id_form-TOTAL_FORMS').val();
$('#form_set').append($('#empty_form').html().replace(/__prefix__/g, form_idx));
$('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1);
});
</script>

是的,如果你有有限数量的条目,我也建议在html中渲染它们。(如果你不这样做,你将不得不使用另一个方法)。

你可以像这样隐藏它们:

{% for form in spokenLanguageFormset %}
<fieldset class="languages-\{\{forloop.counter0 }} {% if spokenLanguageFormset.initial_forms|length < forloop.counter and forloop.counter != 1 %}hidden-form{% endif %}">

然后js真的很简单:

addItem: function(e){
e.preventDefault();
var maxForms = parseInt($(this).closest("fieldset").find("[name*='MAX_NUM_FORMS']").val(), 10);
var initialForms = parseInt($(this).closest("fieldset").find("[name*='INITIAL_FORMS']").val(), 10);
// check if we can add
if (initialForms < maxForms) {
$(this).closest("fieldset").find("fieldset:hidden").first().show();
if ($(this).closest("fieldset").find("fieldset:visible").length == maxForms ){
// here I'm just hiding my 'add' link
$(this).closest(".control-group").hide();
};
};
}

因为上面所有的答案都使用jQuery,使一些事情有点复杂,我写了下面的脚本:

function $(selector, element) {
if (!element) {
element = document
}
return element.querySelector(selector)
}


function $$(selector, element) {
if (!element) {
element = document
}
return element.querySelectorAll(selector)
}


function hasReachedMaxNum(type, form) {
var total = parseInt(form.elements[type + "-TOTAL_FORMS"].value);
var max = parseInt(form.elements[type + "-MAX_NUM_FORMS"].value);
return total >= max
}


function cloneMore(element, type, form) {
var totalElement = form.elements[type + "-TOTAL_FORMS"];
total = parseInt(totalElement.value);
newElement = element.cloneNode(true);
for (var input of $$("input", newElement)) {
input.name = input.name.replace("-" + (total - 1) + "-", "-" + total + "-");
input.value = null
}
total++;
element.parentNode.insertBefore(newElement, element.nextSibling);
totalElement.value = total;
return newElement
}
var addChoiceButton = $("#add-choice");
addChoiceButton.onclick = function() {
var choices = $("#choices");
var createForm = $("#create");
cloneMore(choices.lastElementChild, "choice_set", createForm);
if (hasReachedMaxNum("choice_set", createForm)) {
this.disabled = true
}
};
首先,你应该将auto_id设置为false,从而禁用id和name的重复。因为输入名称在表单中必须是唯一的,所以所有的标识都是用它们来完成的,而不是用id。 你还必须替换formtype和表单集的容器。(在上面的例子中choices)

对于那些正在寻找资源以更好地理解上述解决方案的程序员:

Django Dynamic Formsets

读完上面的链接后,Django文档和以前的解决方案应该更有意义了。

Django Formset Documentation

作为对我所困惑的内容的快速总结:管理表单包含其中表单的概述。你必须保证这些信息的准确性,这样Django才能知道你添加的表单。(社区,如果我的一些措辞不正确,请给我建议。我刚接触Django。)

我可以为每个正在寻找开箱即用的解决方案的人推荐django-dynamic-formsets

它取代了我从提出的解决方案中派生的所有代码,并提供了一些额外的功能,例如删除表单或对相关按钮进行风格化。