Dynamisches Hinzufügen eines Formulars zu einem Django-Formularset mit Ajax


260

Ich möchte mit Ajax automatisch neue Formulare zu einem Django-Formularsatz hinzufügen. Wenn der Benutzer auf die Schaltfläche "Hinzufügen" klickt, wird JavaScript ausgeführt, das der Seite ein neues Formular (das Teil des Formularsatzes ist) hinzufügt.


Ich vermute nur Ihren Anwendungsfall hier, ist es so etwas wie die Funktion "Andere Datei anhängen" in Google Mail, bei der dem Benutzer ein Feld zum Hochladen von Dateien angezeigt wird und dem DOM im Handumdrehen neue Felder hinzugefügt werden, wenn der Benutzer klickt auf "Andere Datei anhängen" plus Schaltfläche?
Prairiedogg

Daran wollte ich bald arbeiten, daher bin ich auch an Antworten interessiert.
Van Gale

2
Diese Frage ist etwas verschwommen, sie erwähnt "Ajax" in Titel, Beschreibung und Tags. Bei keiner der Antworten wird jedoch Ajax verwendet. Das Formular muss dennoch eingereicht werden.
Antoine Pinsard

Antworten:


219

So mache ich es mit jQuery :

Meine Vorlage:

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

In einer Javascript-Datei:

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

Was es macht:

cloneMoreakzeptiert selectorals erstes Argument und das typevon formset als zweites. Was das selectortun sollte, ist es zu übergeben, was es duplizieren sollte. In diesem Fall übergebe ich es div.table:lastso, dass jQuery nach der letzten Tabelle mit einer Klasse von sucht table. Der :lastTeil davon ist wichtig, da der selectorauch verwendet wird, um zu bestimmen, wonach das neue Formular eingefügt wird. Höchstwahrscheinlich möchten Sie es am Ende der restlichen Formulare. Das typeArgument ist, dass wir insbesondere das management_formFeld TOTAL_FORMSsowie die tatsächlichen Formularfelder aktualisieren können . Wenn Sie ein Formularset mit beispielsweise ClientModellen haben, haben die Verwaltungsfelder IDs von id_clients-TOTAL_FORMSund id_clients-INITIAL_FORMS, während die Formularfelder das Format id_clients-N-fieldnamewith habenNist die Formularnummer, beginnend mit 0. Mit dem typeArgument prüft die cloneMoreFunktion also, wie viele Formulare derzeit vorhanden sind, und durchläuft jede Eingabe und Beschriftung im neuen Formular, wobei alle Feldnamen / IDs von so etwas wie id_clients-(N)-namebis id_clients-(N+1)-nameusw. ersetzt werden. Nachdem es fertig ist, aktualisiert es das TOTAL_FORMSFeld, um das neue Formular wiederzugeben, und fügt es am Ende des Satzes hinzu.

Diese Funktion ist für mich besonders hilfreich, da ich sie aufgrund ihrer Einrichtung in der gesamten App verwenden kann, wenn ich mehr Formulare in einem Formularsatz bereitstellen möchte, und kein verstecktes "Vorlagen" -Formular zum Duplizieren erforderlich bin Solange ich es übergebe, den Namen des Formularsatzes und das Format, in dem die Formulare angeordnet sind. Ich hoffe es hilft.


Im IE wird ein Klon aus einem geklonten Element bei der Auswahl in JS als <undefiniert> dargestellt. Warum?
Panchicore

Ich habe festgestellt, dass Sie in Django 1.1 dem prefixMitglied des Formset-Objekts einen Wert zuweisen müssen. Dies sollte der gleiche Wert sein wie das typeArgument für die cloneMoreFunktion.
Derek Reynolds

3
Ich habe dies geändert, um den Selektor ohne: last zu verwenden und var total = $ (Selektor) .length; um meine Gesamtsumme zu erhalten, da eine Aktualisierung der Seite meine Formularsätze entfernen würde, aber die GESAMT-Erhöhung belassen würde, was dazu führen würde, dass die falsche Nummer gespeichert wird. Ich fügte dann hinzu: zuletzt zum Selektor nach Bedarf. Danke dafür.
Greg

2
Ich habe festgestellt, dass dies mit $ (this) .attr ({'name': name, 'id': id}). Val (''). RemoveAttr ('checked'); Wenn Sie die Eingabe löschen, werden die Kontrollkästchen durcheinander gebracht. Durch Setzen von val ('') erhalten die Kontrollkästchen ein leeres Wertattribut. Und da Kontrollkästchen das Wertattribut nicht verwenden, wird dieses niemals aktualisiert - egal wie oft Sie darauf klicken. Es scheint jedoch, dass der Wert eine höhere Priorität hat als das von Kontrollkästchen zugewiesene "Häkchen". Dies bedeutet, dass Sie immer nicht aktivierte Kontrollkästchen aktivieren.
Niklasdstrom

bitte paolo kannst du mein problem überprüfen stackoverflow.com/questions/62252867/…
art_cs

109

Vereinfachte Version von Paolos Antwort empty_formals Vorlage.

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

Wie kann ich das in der Ansicht behandeln? Wenn ich benutze, CompetitorFormSet = modelformset_factory(ProjectCompetitor, formset=CompetitorFormSets) ctx['competitor_form_set'] = CompetitorFormSet(request.POST)bekomme ich nur ein Formular, in sauberer Methode. Können Sie bitte erklären, wie Sie in Ansichten damit umgehen sollen?
AJ

Genial - danke. Nutzt die verfügbaren Django-Helfer (wie empty_form) hervorragend , was ich sehr schätze.
BigglesZX

@BigglesZX - Ich habe die Lösung angepasst und die neuen Zeilen mit leeren Formularen werden generiert. Die Auswahlfelder generieren jedoch eine Liste von FK-Auswahlmöglichkeiten (verfügbar) anstelle von Dropdown-Listen, die ansonsten für den ursprünglichen Satz von Formularen generiert werden. Wurde ein Problem dieser Art gemeldet?
user12379095

@ Dave könnten Sie die Antwort für spätere Versionen aktualisieren, zB 3.x? Es ist einfach und klar, aber es funktioniert nicht für mich
Poula Adel

1
@PoulaAdel Was funktioniert nicht? Ich habe es gerade auf Django 3.0.5 versucht und es funktioniert immer noch für mich. Nach 8 Jahren überraschend, aber ich denke, Django und jQuery haben eine gute Abwärtskompatibilität mit älterem Code.
Dave


18

Paolos Vorschlag funktioniert wunderbar mit einer Einschränkung - den Zurück / Vorwärts-Schaltflächen des Browsers.

Die mit Paolos Skript erstellten dynamischen Elemente werden nicht gerendert, wenn der Benutzer über die Schaltfläche Zurück / Vorwärts zum Formularsatz zurückkehrt. Ein Problem, das für manche ein Deal Breaker sein kann.

Beispiel:

1) Der Benutzer fügt dem Formularsatz über die Schaltfläche "Mehr hinzufügen" zwei neue Formulare hinzu

2) Der Benutzer füllt die Formulare aus und sendet das Formularset

3) Der Benutzer klickt im Browser auf die Schaltfläche "Zurück"

4) Formset wird jetzt auf das ursprüngliche Formular reduziert, alle dynamisch hinzugefügten Formulare sind nicht vorhanden

Dies ist überhaupt kein Fehler in Paolos Skript; aber eine Tatsache des Lebens mit Dom-Manipulation und Browser-Cache.

Ich nehme an, man könnte die Werte des Formulars in der Sitzung speichern und etwas Ajax-Magie haben, wenn das Formularset geladen wird, um die Elemente erneut zu erstellen und die Werte aus der Sitzung neu zu laden. Aber je nachdem, wie anal Sie über denselben Benutzer und mehrere Instanzen des Formulars sein möchten, kann dies sehr kompliziert werden.

Hat jemand einen guten Vorschlag, um damit umzugehen?

Vielen Dank!


2
Wenn Sie nach erfolgreicher Übermittlung umleiten, ist die Schaltfläche "Zurück" kein Problem. Wenn Sie beim nächsten Besuch die Formulare aus der Datenbank ausfüllen, werden zunächst alle Formulare angezeigt. Wenn Sie die Formulare aufgrund einer ungültigen Eingabe nicht bestehen, sollten alle bei der erneuten Anzeige mit Fehlern vorhanden sein. Es sei denn, ich verstehe Ihre Aussagen nicht ... Diese Weiterleitung nach der Übermittlung ist wirklich wichtig in einer gut funktionierenden App, die viele Codierer einfach nicht aufgrund der Anzahl der Apps mit schlechtem Verhalten erhalten, auf die ich im Web stoße.
Bootscodierer

Kannst du mir helfen stackoverflow.com/questions/62285767/… , ich habe viel versucht, aber keine Antwort bekommen! Ich schätze dich sehr
art_cs


11

Simulieren und imitieren:

  • Erstellen Sie ein Formularset, das der Situation entspricht, bevor Sie auf die Schaltfläche "Hinzufügen" klicken.
  • Laden Sie die Seite, zeigen Sie die Quelle an und notieren Sie sich alle <input>Felder.
  • Ändern Sie das Formularset entsprechend der Situation, nachdem Sie auf die Schaltfläche "Hinzufügen" geklickt haben (ändern Sie die Anzahl der zusätzlichen Felder).
  • Laden Sie die Seite, zeigen Sie die Quelle an und notieren Sie sich, wie die <input> Felder geändert haben.
  • Erstellen Sie einige JavaScript , die das DOM in geeigneter Weise modifiziert sie von der zu bewegen , bevor in dem Zustand nach Zustand zu verschieben.
  • Hängen Sie dieses JavaScript an die Schaltfläche "Hinzufügen" an.

Obwohl ich weiß, dass Formsets spezielle versteckte <input>Felder verwenden und ungefähr wissen, was das Skript tun muss, erinnere ich mich nicht an die Details auf der Oberseite meines Kopfes. Was ich oben beschrieben habe, würde ich in Ihrer Situation tun.


Kannst du mir helfen stackoverflow.com/questions/62285767/… , ich habe viel stackoverflow.com/questions/62285767/… ausprobiert, aber keine Antwort bekommen! Ich schätze dich sehr
art_cs

6

Hierfür gibt es ein jquery-Plugin . Ich habe es mit inline_form in Django 1.3 verwendet und es funktioniert einwandfrei, einschließlich Vorbelegung, Hinzufügen, Entfernen von clientseitigen Formularen und mehreren inline_formsets.


Während der verlinkte Blog-Beitrag noch vorhanden ist, sind die Download-Links dort defekt. Anscheinend wurde das Plugin von @ elo80ka erstellt, dessen Antwort auf eine (vorläufige?) Version des Skripts verweist.
lfurini

Kannst du mir helfen stackoverflow.com/questions/62285767/… , ich habe viel versucht, aber keine Antwort bekommen! Ich schätze dich sehr
art_cs

4

Eine Möglichkeit wäre, mit jedem möglichen Formular ein Formularset zu erstellen, die nicht erforderlichen Formulare jedoch zunächst auf ausgeblendet zu setzen display: none;. Wenn ein Formular angezeigt werden muss, setzen Sie die CSS-Anzeige aufblock oder was auch immer angemessen ist.

Ohne mehr Details darüber zu wissen, was Ihr "Ajax" tut, ist es schwierig, eine detailliertere Antwort zu geben.


4

Eine weitere cloneMore-Version, die die selektive Bereinigung von Feldern ermöglicht. Verwenden Sie diese Option, wenn Sie verhindern möchten, dass mehrere Felder gelöscht werden.

$('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);
}

Kannst du mir helfen stackoverflow.com/questions/62285767/… , ich habe viel versucht, aber keine Antwort bekommen! Ich schätze dich sehr
art_cs

2

Es gibt ein kleines Problem mit der Funktion cloneMore. Da auch der Wert der automatisch von Django generierten ausgeblendeten Felder bereinigt wird, beschwert sich Django, wenn Sie versuchen, ein Formularset mit mehr als einem leeren Formular zu speichern.

Hier ist ein Fix:

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

Kannst du mir helfen stackoverflow.com/questions/62285767/… , ich habe viel versucht, aber keine Antwort bekommen! Ich schätze dich sehr
art_cs


2

Für die Programmierer da draußen, die nach Ressourcen suchen, um die oben genannten Lösungen ein wenig besser zu verstehen:

Django Dynamic Formsets

Nach dem Lesen des obigen Links sollten die Django-Dokumentation und frühere Lösungen viel sinnvoller sein.

Django Formset-Dokumentation

Als kurze Zusammenfassung dessen, was mich verwirrt hat: Das Verwaltungsformular enthält eine Übersicht über die darin enthaltenen Formulare. Sie müssen diese Informationen korrekt halten, damit Django die von Ihnen hinzugefügten Formulare kennt. (Community, bitte gib mir Vorschläge, wenn ein Teil meiner Formulierung hier nicht stimmt. Ich bin neu in Django.)



1

Ja, ich würde auch empfehlen, sie nur im HTML-Code zu rendern, wenn Sie eine begrenzte Anzahl von Einträgen haben. (Wenn Sie dies nicht tun, müssen Sie eine andere Methode verwenden).

Sie können sie folgendermaßen verstecken:

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

Dann ist das js ganz einfach:

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();
        };
    };
}

Kannst du mir helfen stackoverflow.com/questions/62285767/… , ich habe viel versucht, aber keine Antwort bekommen! Ich schätze dich sehr
art_cs

1

Da alle obigen Antworten jQuery verwenden und einige Dinge etwas komplexer machen, habe ich folgendes Skript geschrieben:

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

Zuerst sollten Sie auto_id auf false setzen und so die Duplizierung von ID und Name deaktivieren. Da die Eingabenamen in ihrer Form eindeutig sein müssen, erfolgt die gesamte Identifizierung mit ihnen und nicht mit IDs. Sie müssen auch die ersetzen form, typeund der Behälter der formset. (Im obigen Beispiel choices)


Kannst du mir helfen stackoverflow.com/questions/62285767/… , ich habe viel versucht, aber keine Antwort bekommen! Ich schätze dich sehr
art_cs
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.