Я хочу динамически добавлять новые формы в набор форм Django, чтобы, когда пользователь нажимает кнопку «добавить», запускался JavaScript, который добавляет новую форму (которая является частью набора форм) на страницу.

272
Brian Tol 2 Фев 2009 в 01:26
Я просто догадываюсь о вашем варианте использования здесь, это что-то вроде функции «Прикрепить другой файл» в Gmail, где пользователю предоставляется поле для загрузки файла, а новые поля добавляются в DOM на лету, когда пользователь нажимает к кнопке с плюсом «Прикрепить другой файл»?
 – 
prairiedogg
2 Фев 2009 в 03:56
Это то, над чем я собирался в ближайшее время поработать, поэтому мне также будут интересны любые ответы.
 – 
Van Gale
2 Фев 2009 в 07:09
Думаю, это гораздо лучшее решение. stackoverflow .com / questions / 2353710 /… Клонирует ли что-то, чего не делает: - Добавляет форму, когда исходных форм не существует - Лучше обрабатывает javascript в форме, например django-ckeditor - Сохраняет исходные данные
 – 
Bufke
25 Июл 2011 в 20:21

10 ответов

Лучший ответ

Вот как я это делаю, используя 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 в качестве первого аргумента и type набора форм в качестве второго. Что должен делать selector, так это передавать ему то, что он должен дублировать. В этом случае я передаю его div.table:last, чтобы jQuery искал последнюю таблицу с классом table. Часть :last важна, потому что selector также используется для определения того, после чего будет вставлена ​​новая форма. Скорее всего, вы захотите, чтобы это было в конце остальных форм. Аргумент type предназначен для того, чтобы мы могли обновить поле management_form, особенно TOTAL_FORMS, а также сами поля формы. Если у вас есть набор форм, полный, скажем, моделей Client, поля управления будут иметь идентификаторы id_clients-TOTAL_FORMS и id_clients-INITIAL_FORMS, а поля формы будут иметь формат id_clients-N-fieldname где N - это номер формы, начинающийся с 0. Таким образом, с аргументом type функция cloneMore проверяет, сколько форм существует в настоящее время, и просматривает каждый ввод и метку внутри новой формы, заменяя все имена / идентификаторы полей чем-то вроде id_clients-(N)-name на id_clients-(N+1)-name и так далее. По завершении он обновляет поле TOTAL_FORMS, чтобы отразить новую форму, и добавляет ее в конец набора.

Эта функция особенно полезна для меня, потому что способ ее настройки позволяет мне использовать ее во всем приложении, когда я хочу предоставить больше форм в наборе форм, и не заставляет меня иметь скрытую "шаблонную" форму для дублирования. пока я передаю ему имя набора форм и формат, в котором они расположены. Надеюсь, это поможет.

229
Paolo Bergantino 16 Сен 2009 в 10:42
В IE клон из клонированного элемента отображается как <не определено> при выборе в JS, почему?
 – 
panchicore
13 Окт 2009 в 18:46
Я обнаружил, что в Django 1.1 вам нужно будет присвоить значение члену prefix объекта Formset. Это должно быть то же значение, что и аргумент type для функции cloneMore.
 – 
Derek Reynolds
11 Фев 2010 в 21:46
3
Я изменил это, чтобы использовать селектор без: last и использовал var total = $ (selector) .length; чтобы получить мою общую сумму, потому что обновление страницы удалит мои наборы форм, но оставит ОБЩЕЕ увеличение, что приведет к сохранению неправильного числа. Затем я добавил: last в селектор по мере необходимости. Спасибо за это.
 – 
Greg
5 Ноя 2010 в 03:47
2
Я обнаружил, что это с помощью $ (this) .attr ({'name': name, 'id': id}). Val (''). RemoveAttr ('checked'); Чтобы очистить ввод, будут испорчены флажки. Установка val ('') дает флажкам атрибут пустого значения. А поскольку флажки не используют атрибут значения, он никогда не будет обновлен - независимо от того, сколько раз вы щелкаете по нему. Но похоже, что значение имеет более высокий приоритет, чем "отмеченный" атрибут флажков. Это будет означать, что вы всегда будете размещать неотмеченные флажки.
 – 
niklasdstrom
14 Июл 2011 в 23:28
Пожалуйста, Паоло, вы можете проверить мою проблему stackoverflow.com/questions/62252867/…
 – 
art_cs
9 Июн 2020 в 17:47

Упрощенная версия ответа Паоло с использованием 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>
120
Pierre de LESPINAY 28 Мар 2014 в 13:42
Как я могу справиться с этим в представлении? когда я использую CompetitorFormSet = modelformset_factory(ProjectCompetitor, formset=CompetitorFormSets) ctx['competitor_form_set'] = CompetitorFormSet(request.POST) я получаю только одну форму в чистом методе. не могли бы вы объяснить, как бороться с этим в просмотрах?
 – 
A.J.
6 Май 2014 в 11:28
Молодец - спасибо. Отлично использует доступные помощники Django (например, empty_form), что я ценю.
 – 
BigglesZX
14 Июн 2019 в 21:53
- Я адаптировал решение, и создаются новые строки пустых форм. Однако поля выбора создают список вариантов выбора FK (доступных) вместо раскрывающихся списков, которые в противном случае создаются для исходного набора форм. Сообщалось ли о каких-либо проблемах такого характера?
 – 
user12379095
17 Фев 2020 в 08:10
Не могли бы вы обновить ответ для более поздних версий, например 3.x? это просто и понятно, но у меня это не работает
 – 
Poula Adel
9 Апр 2020 в 09:49
1
Что не работает? Я только что попробовал это на Django 3.0.5, и у меня он все еще работает. Удивительно после 8 лет, но я думаю, что Django и jQuery имеют хорошую обратную совместимость со старым кодом.
 – 
Dave
10 Апр 2020 в 19:51

Предложение Паоло прекрасно работает с одной оговоркой - кнопки браузера назад / вперед.

Динамические элементы, созданные с помощью сценария Паоло, не будут отображаться, если пользователь вернется к набору форм с помощью кнопки назад / вперед. Проблема, которая может стать для некоторых преградой.

Примере:

1) Пользователь добавляет две новые формы в набор форм с помощью кнопки «добавить еще».

2) Пользователь заполняет формы и отправляет набор форм.

3) Пользователь нажимает кнопку "Назад" в браузере.

4) Набор форм теперь приведен к исходной форме, всех динамически добавляемых форм нет

Это вовсе не дефект сценария Паоло; но факт из жизни с манипуляциями с домом и кешем браузера.

Я полагаю, что можно сохранить значения формы в сеансе и иметь некоторую магию ajax, когда набор форм загружается, чтобы снова создать элементы и перезагрузить значения из сеанса; но в зависимости от того, насколько анальным вы хотите быть одним и тем же пользователем и несколькими экземплярами формы, это может стать очень сложным.

У кого-нибудь есть хорошее предложение для решения этой проблемы?

Благодарность!

18
cethegeek 21 Апр 2009 в 16:52
4
Если вы перенаправляете после успешной отправки, кнопка "Назад" не проблема. Если вы заполните формы из БД при следующем посещении, все формы появятся изначально. Если вы не заполните формы из-за недопустимого ввода, все они должны появиться при повторном отображении с ошибками. Если я не понимаю ваших утверждений ... Это перенаправление отправки сообщений действительно важно в хорошем рабочем приложении, которое многие программисты просто не понимают из-за количества плохо работающих приложений, с которыми я сталкиваюсь в Интернете.
 – 
boatcoder
20 Мар 2013 в 18:08

Имитируйте и имитируйте:

  • Создайте набор форм, соответствующий ситуации до нажатия кнопки «добавить».
  • Загрузите страницу, просмотрите источник и обратите внимание на все поля <input>.
  • Измените набор форм, чтобы он соответствовал ситуации после нажатия кнопки «добавить» (измените количество дополнительных полей).
  • Загрузите страницу, просмотрите источник и обратите внимание на то, как изменились поля <input>.
  • Создайте код JavaScript, который изменяет модель DOM подходящим образом, чтобы переместить ее из состояния до в состояние после .
  • Прикрепите этот код JavaScript к кнопке «добавить».

Хотя я знаю, что в наборах форм используются специальные скрытые поля <input> и примерно знаю, что должен делать скрипт, я не припоминаю подробностей. То, что я описал выше, - это то, что я бы сделал в вашей ситуации.

11
akaihola 9 Фев 2009 в 23:49

Один из вариантов - создать набор форм со всеми возможными формами, но изначально установить скрытые ненужные формы, то есть display: none;. Когда необходимо отобразить форму, установите для нее отображение css на block или другое подходящее значение.

Не зная более подробной информации о том, что делает ваш "Ajax", трудно дать более подробный ответ.

4
Daniel Naab 2 Фев 2009 в 03:49

Еще одна версия 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);
}
4
xaralis 4 Ноя 2010 в 17:00
Можете ли вы помочь мне stackoverflow.com/questions/ 62285767 /…, я много пробовал, но не получил ответа! я очень ценю тебя
 – 
art_cs
9 Июн 2020 в 21:58

Для программистов, которые ищут ресурсы, чтобы немного лучше понять вышеперечисленные решения:

Наборы динамических форм Django

После прочтения приведенной выше ссылки документация Django и предыдущие решения должны иметь больше смысла.

Документация по Django Formset

Вкратце, что меня смутило: Форма управления содержит обзор форм внутри. Вы должны поддерживать точность этой информации, чтобы Django знал о добавляемых вами формах. (Сообщество, пожалуйста, дайте мне предложения, если некоторые из моих формулировок здесь отсутствуют. Я новичок в Django.)

4
Ryan Buchmeier 3 Мар 2018 в 17:53

Есть небольшая проблема с функцией 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);
}
2
Cesar Canassa 8 Июн 2010 в 01:22
1
Извините @art_cs, я не работал с Django несколько лет. Пожалуйста, внимательно изучите ответы на этот вопрос и используйте отладчик в инструментах разработчика вашего браузера, я уверен, что это довольно легко решить. Я бы также поискал готовое решение.
 – 
akaihola
27 Июн 2020 в 23:39

Да, я бы также рекомендовал просто отображать их в 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();
        };
    };
}
1
Bob Spryn 9 Авг 2012 в 05:05

Поскольку во всех приведенных выше ответах используется 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 и поэтому отключите дублирование идентификатора и имени. Поскольку имена входов должны быть уникальными в этой форме, вся идентификация выполняется с ними, а не с идентификаторами. Вы также должны заменить form, type и контейнер набора форм. (В приведенном выше примере choices)

1
user6216224user6216224 3 Апр 2017 в 19:40