使用Ajax将表单动态添加到Django表单集中


260

我想使用Ajax自动将新表单添加到Django表单集中,以便当用户单击“添加”按钮时,它运行JavaScript,该JavaScript将新表单(属于表单集的一部分)添加到页面中。


我只是在这里猜测您的用例,是否类似于gmail中的“附加另一个文件”功能,即向用户显示文件上载字段,并在用户单击时将新字段即时添加到DOM中要“附加另一个文件”加号按钮?
prairiedogg

这是我即将进行的工作,因此我也会对所有答案感兴趣。
Van Gale '02

2
这个问题有点模糊,在标题,描述和标签中提到了“ Ajax”。但是,答案都没有使用Ajax,它仍然需要提交表单。
安托万·皮萨德

Answers:


219

这就是我使用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作为第一个参数,typeformset的形式作为第二个参数。什么是selector应该做的是把它传递它应该复制。在这种情况下,我将其传递给div.table:lastjQuery,以查找带有类的最后一个表table。它的:last一部分很重要,因为selector还可用于确定将在其后插入新表格的内容。您很可能希望在其余表格的末尾使用它。该type论点是,这样我们就可以更新management_form领域,特别是TOTAL_FORMS,还有实际的表单字段。如果您有一个充满Client模型的表单集,则管理字段的ID为id_clients-TOTAL_FORMSid_clients-INITIAL_FORMS,而表单字段的格式为id_clients-N-fieldnamewithN是表格编号,以开头0。因此,使用该type参数,该cloneMore函数将查看当前有多少个表单,并遍历新表单中的每个输入和标签,从而替换诸如id_clients-(N)-nameto id_clients-(N+1)-name等的所有字段名称/ id 。完成后,它将更新TOTAL_FORMS字段以反映新表单并将其添加到集合的末尾。

此功能对我特别有用,因为它的设置方式使我可以在整个应用程序中使用它,以便在表单集中提供更多表单,而不必让我需要隐藏的“模板”表单来进行复制只要我通过它,表单集名称和表单的布局格式。希望能帮助到你。


在IE中,在JS中进行选择时,来自克隆元素的克隆表示为<undefined>,为什么?
panchicore

我发现在Django 1.1中,您需要为prefixFormset Object 的成员分配一个值。该值应与typecloneMore函数的参数相同。
德里克·雷诺兹

3
我将其修改为不带:last的选择器,并使用var total = $(selector).length; 之所以要获得总计,是因为刷新页面会删除我的表单集,但会使TOTAL总数增加,从而导致保存错误的数字。然后,我根据需要将:last添加到选择器中。谢谢你
格雷格,2010年

2
我发现使用$(this).attr({'name':name,'id':id})。val('')。removeAttr('checked'); 清除输入将使复选框混乱。设置val('')将为复选框提供空值属性。而且由于复选框不使用value属性,因此无论您单击多少次,该属性都不会被更新。但似乎值的优先级高于复选框的“选中”属性。这意味着您将始终发布非选中的复选框。
niklasdstrom 2011年

请保罗,你可以检查我的问题stackoverflow.com/questions/62252867/...
art_cs

109

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>

我该如何处理呢?当我使用时,CompetitorFormSet = modelformset_factory(ProjectCompetitor, formset=CompetitorFormSets) ctx['competitor_form_set'] = CompetitorFormSet(request.POST)我只能以一种干净的方法获得一种形式。您能以意见解释如何处理吗?
2014年

辉煌-谢谢。很好地利用了可用的Django帮助程序(如empty_form),对此我表示赞赏。
BigglesZX

@BigglesZX-我已经适应了解决方案,并且生成了新的空表格行。但是,选择框将生成FK(可用)选项的列表,而不是为原始表单集生成的下拉列表。有没有报道过这种性质的问题?
user12379095

@Dave您可以为更高版本(即3.x)更新答案吗?简单明了,但对我
不起作用

1
@PoulaAdel什么不起作用?我只是在Django 3.0.5上尝试过,它仍然对我有用。8年后令人惊讶,但我想Django和jQuery与较旧的代码具有良好的向后兼容性。
戴夫


18

Paolo的建议可以很好地解决一个警告-浏览器的后退/前进按钮。

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

例:

1)用户使用“添加更多”按钮将两个新表单添加到表单集中

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

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

4)现在将Formset缩减为原始表单,不存在所有动态添加的表单

这根本不是Paolo脚本的缺陷。但事实证明,使用dom操纵和浏览器的缓存是可行的。

我想可以在表单中存储表单的值,并且在加载表单集时再次使用ajax魔术来再次创建元素并从会话中重新加载值。但是根据您想要成为同一个用户的方式以及该表单的多个实例的情况,这可能会变得非常复杂。

有人对此有很好的建议吗?

谢谢!


2
如果您在成功提交后重定向,则后退按钮不是问题。如果您下次访问时从数据库填写表格,则所有表格最初都会出现。如果由于输入无效而使表单失败,则所有表单都应存在于重新显示中,并带有错误。除非我不理解您的陈述...。在一个运行良好的应用程序中,帖子提交重定向确实非常重要,很多程序员只是基于我在网络上遇到的性能不佳的应用程序而无法获得这种重定向。
boatcoder 2013年

您能帮我stackoverflow.com/questions/62285767 / ...,我尝试了很多,但没有得到答案!我非常感谢您
art_cs


11

模拟和模仿:

  • 单击“添加”按钮之前,创建与情况相对应的表单集。
  • 加载页面,查看源并记下所有<input>字段。
  • 单击“添加”按钮(更改其他字段的数量)后,修改表单集以适应情况。
  • 加载页面,查看源并记下<input>字段的变化。
  • 创建一些JavaScript,以适当的方式修改DOM,以将其从before状态移至after状态。
  • 将该JavaScript附加到“添加”按钮。

虽然我知道表单集使用特殊的隐藏<input>字段并且大致了解脚本必须执行的操作,但我不记得这些细节。我上面所描述的就是在您遇到的情况下我将要做的事情。


您能帮我stackoverflow.com/questions/62285767 / ...,我试过很多stackoverflow.com/questions/62285767 / ...但没有得到答案!我非常感谢您
art_cs

6

有一个jquery插件,我在Django 1.3中将它与inline_form一起使用,它可以完美地工作,包括预填充,客户端表单添加,删除以及多个inline_formsets。


虽然链接的博客文章仍然存在,但是那里的下载链接断开了。显然,该插件是由@ elo80ka创建的,其答案指向脚本的(初始版本)。
lfurini '19

您能帮我stackoverflow.com/questions/62285767 / ...,我尝试了很多,但没有得到答案!我非常感谢您
art_cs

4

一种选择是使用所有可能的表单创建一个表单集,但最初将不需要的表单设置为hidden,即display: none;。当需要显示表单时,将其设置为css display block或其他合适的方法。

在不了解“ Ajax”正在做什么的更多细节的情况下,很难给出更详细的响应。


4

另一个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);
}

您能帮我stackoverflow.com/questions/62285767 / ...,我尝试了很多,但没有得到答案!我非常感谢您
art_cs

2

cloneMore函数有一个小问题。由于它也清除了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);
}

您能帮我stackoverflow.com/questions/62285767 / ...,我尝试了很多,但没有得到答案!我非常感谢您
art_cs


2

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

Django动态表单集

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

Django Formset文档

作为让我感到困惑的快速摘要:管理表单包含其中的表单的概述。您必须保持准确的信息,以便Django知道添加的表单。(社区,如果我的某些措辞在这里不对,请给我建议。我是Django的新手。)



1

是的,如果条目数量有限,我还建议您仅将它们呈现在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();
        };
    };
}

您能帮我stackoverflow.com/questions/62285767 / ...,我尝试了很多,但没有得到答案!我非常感谢您
art_cs

1

由于以上所有答案均使用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


您能帮我stackoverflow.com/questions/62285767 / ...,我尝试了很多,但没有得到答案!我非常感谢您
art_cs
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.