首页 文章

使用Ajax动态地将表单添加到Django formset

提问于
浏览
237

我想使用Ajax自动将新表单添加到Django表单集中,这样当用户单击“添加”按钮时,它会运行JavaScript,向页面添加一个新表单(它是表单集的一部分) .

15 回答

  • 2

    @Paolo Bergantino

    克隆所有附加的处理程序只需修改该行

    var newElement = $(selector).clone();
    

    对于

    var newElement = $(selector).clone(true);
    

    防止this problem.

  • 1

    查看动态django表单的以下解决方案:

    http://code.google.com/p/django-dynamic-formset/

    https://github.com/javisantana/django-dinamyc-form/tree/master/frm

    他们都使用jQuery并且是特定于django的 . 第一个似乎更精致,并提供了一个非常好的演示下载 .

  • 11

    我认为这是一个更好的解决方案 .

    How would you make a dynamic formset in Django?

    克隆的东西不是:

    • 当没有初始表格时添加表格

    • 更好地处理表单中的javascript,例如django-ckeditor

    • 保留初始数据

  • 205

    因为上面的所有答案都使用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 和formset的容器 . (在上面的例子中 choices

  • 13

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

    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);
    }
    
  • 4

    模拟和模仿:

    • 创建一个与 before 单击"add"按钮的情况对应的formset .

    • 加载页面,查看源并记下所有 <input> 字段 .

    • 修改formset以对应 after 单击"add"按钮的情况(更改额外字段的数量) .

    • 加载页面,查看源并记下 <input> 字段的更改方式 .

    • 创建一些JavaScript,以适当的方式修改DOM,将其从 before 状态移动到 after 状态 .

    • 将该JavaScript附加到"add"按钮 .

    虽然我知道formsets使用特殊隐藏的 <input> 字段并且大致知道脚本必须做什么,但我不记得我头脑中的细节 . 我上面描述的是我在你的情况下会做的 .

  • 91

    是的,如果您的条目数量有限,我还建议您只在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();
            };
        };
    }
    
  • 4

    一种选择是创建一个包含所有可能形式的formset,但最初将不需要的表单设置为隐藏 - 即 display: none; . 当's necessary to display a form, set it' sss显示到 block 或任何合适的时候 .

    如果不了解你的“Ajax”正在做什么的更多细节,很难给出更详细的回复 .

  • 2

    Paolo的建议很好地解决了一个问题 - 浏览器的后退/前进按钮 .

    如果用户使用后退/前进按钮返回到formset,则不会呈现使用Paolo脚本创建的动态元素 . 对某些人来说可能是一个交易破坏者的问题 .

    例:

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

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

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

    4)Formset现在缩减为原始形式,所有动态添加的形式都不存在

    这根本不是Paolo脚本的缺陷;但dom操作和浏览器缓存是生活中的事实 .

    我想可以在会话中存储表单的值,并且当formset加载以再次创建元素并从会话重新加载值时,可以使用一些ajax魔法;但取决于你想要关于同一个用户和表单的多个实例的肛门,这可能会变得非常复杂 .

    任何人都有一个很好的建议来处理这个?

    谢谢!

  • 18

    有一个jquery plugin for this,我在Django 1.3中设置了inline_form,它运行得很好,包括预填充,客户端表单添加,删除和多个inline_formsets .

  • 0

    我是这样做的,使用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 字段,特别是 TOTAL_FORMS ,以及实际的表单字段 . 如果您的表单集中包含 Client 模型,则管理字段的ID为 id_clients-TOTAL_FORMSid_clients-INITIAL_FORMS ,而表单字段的格式为 id_clients-N-fieldname ,其中 N 为表单编号,以 0 开头 . 因此,使用 type 参数, cloneMore 函数会查看当前有多少个表单,并遍历新表单中的每个输入和标签,将所有字段名称/ ID替换为 id_clients-(N)-nameid_clients-(N+1)-name 等等 . 完成后,它会更新 TOTAL_FORMS 字段以反映新表单并将其添加到集合的末尾 .

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

  • 1

    我从一个我曾经工作的应用程序posted a snippet . 与Paolo相似,但也允许您删除表单 .

  • 24

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

    Django Dynamic Formsets

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

    Django Formset Documentation

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

  • 6

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

    <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>
    

相关问题