Aggiunta dinamica di un modulo a un formset Django con Ajax


260

Voglio aggiungere automaticamente nuovi moduli a un formset Django usando Ajax, in modo che quando l'utente fa clic su un pulsante "aggiungi" esegue JavaScript che aggiunge un nuovo modulo (che fa parte del formset) alla pagina.


Sto solo indovinando il tuo caso d'uso qui, è qualcosa come la funzione "Allega un altro file" in Gmail, in cui all'utente viene presentato un campo di caricamento del file e nuovi campi vengono aggiunti al DOM al volo mentre l'utente fa clic al pulsante "Allega un altro file"?
prairiedogg,

Questo è qualcosa su cui lavorerò presto, quindi mi interesseranno anche le risposte.
Van Gale,

2
Questa domanda è un po 'confusa, menziona "Ajax" nel titolo, nella descrizione e nei tag. Tuttavia, nessuna delle risposte utilizza Ajax, ma richiede comunque l'invio del modulo.
Antoine Pinsard,

Risposte:


219

Ecco come lo faccio, usando jQuery :

Il mio modello:

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

Cosa fa:

cloneMoreaccetta selectorcome primo argomento e quello typedi formset come secondo. Quello che selectordovrebbe fare è passare ciò che dovrebbe duplicare. In questo caso, lo passo in div.table:lastmodo che jQuery cerchi l'ultima tabella con una classe di table. La :lastparte di esso è importante perché selectorviene anche utilizzata per determinare quale sarà il successivo modulo inserito. Molto probabilmente lo vorresti alla fine del resto dei moduli. L' typeargomento è che possiamo aggiornare l'management_form campo, in particolare TOTAL_FORMS, così come i campi del modulo effettivo. Se hai un formset pieno di, diciamo, Clientmodelli, i campi di gestione avranno ID di id_clients-TOTAL_FORMSe id_clients-INITIAL_FORMS, mentre i campi del modulo saranno in un formato id_clients-N-fieldnameconNessendo il numero del modulo, a partire da 0. Quindi, con l' typeargomento, la cloneMorefunzione esamina quante forme ci sono attualmente e passa attraverso ogni input ed etichetta all'interno del nuovo modulo sostituendo tutti i nomi / ID dei campi da qualcosa di simile id_clients-(N)-namea id_clients-(N+1)-namecosì via. Al termine, aggiorna il TOTAL_FORMScampo per riflettere il nuovo modulo e lo aggiunge alla fine dell'insieme.

Questa funzione mi è particolarmente utile perché il modo in cui è impostata mi consente di utilizzarla in tutta l'app quando voglio fornire più moduli in un formset e non mi ha bisogno di avere un modulo "modello" nascosto da duplicare fintanto che gli passo il nome del formset e il formato in cui sono disposti i moduli. Spero che sia d'aiuto.


In IE, un clone di un elemento clonato viene rappresentato come <non definito> quando si seleziona in JS, perché?
Panchicore,

Ho scoperto che in Django 1.1 dovrai assegnare un valore al prefixmembro dell'oggetto Formset. Questo dovrebbe avere lo stesso valore typedell'argomento per la cloneMorefunzione.
Derek Reynolds,

3
Ho modificato questo per prendere il selettore senza: last e usato var total = $ (selettore) .length; per ottenere il mio totale perché un aggiornamento della pagina rimuoverà i miei moduli ma lascerebbe l'aumento TOTALE che porta al salvataggio del numero sbagliato. Ho quindi aggiunto: ultimo al selettore, se necessario. Grazie per questo
Greg,

2
Ho scoperto che questo usando $ (this) .attr ({'name': name, 'id': id}). Val (''). RemoveAttr ('checked'); Per cancellare l'input verranno visualizzate delle caselle di controllo. L'impostazione di val ('') fornisce alle caselle di controllo un attributo di valore vuoto. E poiché le caselle di controllo non utilizzano l'attributo value, questo non verrà mai aggiornato, indipendentemente da quante volte lo fai clic. Ma sembra che il valore abbia una priorità più alta rispetto all'attributo "controllato" delle caselle di controllo. Ciò significa che pubblicherai sempre caselle di controllo non selezionate.
niklasdstrom,

Si prega di paolo è possibile controllare il mio problema stackoverflow.com/questions/62252867/...
art_cs

109

Versione semplificata della risposta di Paolo usando empty_formcome modello.

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

come posso gestirlo nella vista? quando uso CompetitorFormSet = modelformset_factory(ProjectCompetitor, formset=CompetitorFormSets) ctx['competitor_form_set'] = CompetitorFormSet(request.POST)ottengo solo un modulo, in modo pulito. puoi per favore spiegare come gestire questo nelle viste?
AJ,

Fantastico - grazie. Fa un uso eccellente degli helper (come empty_form) disponibili di Django , che apprezzo.
BigglesZX

@BigglesZX - Ho adattato la soluzione e vengono generate le nuove righe di moduli vuoti. Tuttavia, le caselle di selezione generano un elenco di scelte FK (disponibili), invece di menu a discesa che vengono altrimenti generati per il set di moduli originale. È stato segnalato qualche problema di questa natura?
user12379095

@Dave potresti aggiornare la risposta per le versioni successive, ad es. 3.x? è semplice e chiaro ma non funziona per me
Poula Adel il

1
@PoulaAdel Cosa non funziona? Ho appena provato questo su Django 3.0.5 e funziona ancora per me. Sorprendente dopo 8 anni, ma immagino che Django e jQuery abbiano una buona compatibilità con il vecchio codice.
Dave,


18

Il suggerimento di Paolo funziona magnificamente con un avvertimento: i pulsanti avanti / indietro del browser.

Gli elementi dinamici creati con lo script di Paolo non verranno resi se l'utente torna al formset utilizzando il pulsante Indietro / Avanti. Un problema che potrebbe compromettere alcuni affari.

Esempio:

1) L'utente aggiunge due nuovi moduli al formset utilizzando il pulsante "aggiungi altro"

2) L'utente popola i moduli e invia il formset

3) L'utente fa clic sul pulsante Indietro nel browser

4) Formset è ora ridotto alla forma originale, non tutti i moduli aggiunti dinamicamente

Questo non è affatto un difetto con la sceneggiatura di Paolo; ma un dato di fatto con manipolazione dom e cache del browser.

Suppongo che uno potrebbe archiviare i valori del modulo nella sessione e avere un po 'di magia ajax quando il formset si carica per creare nuovamente gli elementi e ricaricare i valori dalla sessione; ma a seconda di quanto anale vuoi essere sullo stesso utente e più istanze del modulo questo può diventare molto complicato.

Qualcuno ha un buon suggerimento per affrontare questo?

Grazie!


2
Se reindirizzi dopo l'invio riuscito, il pulsante Indietro non è un problema. Se compili i moduli dal DB alla visita successiva, tutti i moduli vengono visualizzati inizialmente. Se i moduli non vengono completati a causa di input non validi, tutti dovrebbero essere presenti sul display nuovamente con errori. A meno che non capisca le tue affermazioni .... Quel reindirizzamento post-presentazione è davvero importante in una buona app funzionante, quella che molti programmatori semplicemente non si basano sul numero di app che si comportano male sul web.
Boatcoder

mi potete aiutare stackoverflow.com/questions/62285767/… , ho provato molto ma non ho ricevuto risposta! ti apprezzo molto
art_cs,


11

Simula e imita:

  • Crea un formset che corrisponda alla situazione precedente fare clic sul pulsante "Aggiungi".
  • Carica la pagina, visualizza la fonte e prendi nota di tutto <input> campi.
  • Modificare il formset in modo che corrisponda alla situazione successiva clic sul pulsante "Aggiungi" (modifica il numero di campi aggiuntivi).
  • Carica la pagina, visualizza la fonte e prendi nota di come <input> sono cambiati campi.
  • Crea del codice JavaScript che modifica il DOM in modo adeguato per spostarlo dallo stato precedente allo stato successivo .
  • Allega JavaScript al pulsante "aggiungi".

Mentre conosco i formets usano speciali <input>campi nascosti e so approssimativamente cosa deve fare lo script, non ricordo i dettagli dalla parte superiore della mia testa. Quello che ho descritto sopra è quello che farei nella tua situazione.


mi potete aiutare stackoverflow.com/questions/62285767/… , ho provato molto stackoverflow.com/questions/62285767/… ma non ho ricevuto risposta! ti apprezzo molto
art_cs,

6

C'è un plugin jquery per questo , l'ho usato con inline_form impostato in Django 1.3, e funziona perfettamente, compresa la prepopolazione, l'aggiunta, la rimozione di moduli lato client e più inline_formsets.


Mentre il post sul blog collegato esiste ancora, i collegamenti per il download sono interrotti. Apparentemente, il plug-in è stato creato da @ elo80ka, la cui risposta punta a una versione (preliminare?) Dello script.
Lfurini,

mi potete aiutare stackoverflow.com/questions/62285767/… , ho provato molto ma non ho ricevuto risposta! ti apprezzo molto
art_cs,

4

Un'opzione sarebbe quella di creare un formset con ogni forma possibile, ma inizialmente impostare le forme non necessarie su nascosto - vale a dire display: none;. Quando è necessario visualizzare un modulo, impostare la visualizzazione css su blocko qualsiasi altra cosa sia appropriata.

Senza conoscere maggiori dettagli su ciò che sta facendo il tuo "Ajax", è difficile dare una risposta più dettagliata.


4

Un'altra versione di cloneMore, che consente la sanificazione selettiva dei campi. Usalo quando devi impedire la cancellazione di più campi.

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

mi potete aiutare stackoverflow.com/questions/62285767/… , ho provato molto ma non ho ricevuto risposta! ti apprezzo molto
art_cs,

2

C'è un piccolo problema con la funzione cloneMore. Dal momento che sta anche ripulendo il valore dei campi nascosti generati automaticamente da django, provoca lamentarsi django se si tenta di salvare un formset con più di un modulo vuoto.

Ecco una soluzione:

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

mi potete aiutare stackoverflow.com/questions/62285767/… , ho provato molto ma non ho ricevuto risposta! ti apprezzo molto
art_cs,


2

Per i programmatori che cercano risorse per capire un po 'meglio le soluzioni di cui sopra:

Django Dynamic Formsets

Dopo aver letto il link sopra, la documentazione di Django e le soluzioni precedenti dovrebbero avere molto più senso.

Documentazione Django Formset

Come una breve sintesi di ciò che mi stava confondendo: Il modulo di gestione contiene una panoramica dei moduli all'interno. Devi mantenere accurate queste informazioni affinché Django sia a conoscenza dei moduli che aggiungi. (Community, per favore, dammi dei suggerimenti se alcune delle mie parole non sono presenti qui. Sono nuovo di Django.)



1

Sì, consiglierei anche di renderli nel codice HTML se hai un numero finito di voci. (In caso contrario, dovrai utilizzare un altro metodo).

Puoi nasconderli in questo modo:

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

Quindi js è davvero semplice:

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

mi potete aiutare stackoverflow.com/questions/62285767/… , ho provato molto ma non ho ricevuto risposta! ti apprezzo molto
art_cs,

1

Perché tutte le risposte sopra usano jQuery e rendono alcune cose un po 'complesse ho scritto il seguente script:

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

Per prima cosa dovresti impostare auto_id su false e quindi disabilitare la duplicazione di id e nome. Poiché i nomi degli input devono essere univoci nella loro forma, tutta l'identificazione viene fatta con loro e non con gli ID. È inoltre necessario sostituire il form, typee il contenitore del formset. (Nell'esempio sopra choices)


mi potete aiutare stackoverflow.com/questions/62285767/… , ho provato molto ma non ho ricevuto risposta! ti apprezzo molto
art_cs,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.