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.
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.
Risposte:
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:
cloneMore
accetta selector
come primo argomento e quello type
di formset come secondo. Quello che selector
dovrebbe fare è passare ciò che dovrebbe duplicare. In questo caso, lo passo in div.table:last
modo che jQuery cerchi l'ultima tabella con una classe di table
. La :last
parte di esso è importante perché selector
viene anche utilizzata per determinare quale sarà il successivo modulo inserito. Molto probabilmente lo vorresti alla fine del resto dei moduli. L' type
argomento è 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, Client
modelli, i campi di gestione avranno ID di id_clients-TOTAL_FORMS
e id_clients-INITIAL_FORMS
, mentre i campi del modulo saranno in un formato id_clients-N-fieldname
conN
essendo il numero del modulo, a partire da 0
. Quindi, con l' type
argomento, la cloneMore
funzione 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)-name
a id_clients-(N+1)-name
così via. Al termine, aggiorna il TOTAL_FORMS
campo 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.
prefix
membro dell'oggetto Formset. Questo dovrebbe avere lo stesso valore type
dell'argomento per la cloneMore
funzione.
Versione semplificata della risposta di Paolo usando empty_form
come 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>
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?
empty_form
) disponibili di Django , che apprezzo.
Ho pubblicato uno snippet da un'app su cui ho lavorato qualche tempo fa. Simile a quello di Paolo, ma consente anche di eliminare i moduli.
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!
Scopri le seguenti soluzioni ai moduli dinamici di django:
http://code.google.com/p/django-dynamic-formset/
https://github.com/javisantana/django-dinamyc-form/tree/master/frm
Entrambi fanno uso di jQuery e sono specifici di django. Il primo sembra un po 'più raffinato e offre un download fornito con demo eccellenti.
Simula e imita:
<input>
campi.<input>
sono cambiati campi.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.
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.
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 block
o qualsiasi altra cosa sia appropriata.
Senza conoscere maggiori dettagli su ciò che sta facendo il tuo "Ajax", è difficile dare una risposta più dettagliata.
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);
}
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);
}
Penso che questa sia una soluzione molto migliore.
Come realizzeresti un formset dinamico in Django?
Le cose clonano no:
Per i programmatori che cercano risorse per capire un po 'meglio le soluzioni di cui sopra:
Dopo aver letto il link sopra, la documentazione di Django e le soluzioni precedenti dovrebbero avere molto più senso.
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.)
@Paolo Bergantino
per clonare tutti i gestori collegati basta modificare la linea
var newElement = $(selector).clone();
per
var newElement = $(selector).clone(true);
per prevenire questo problema.
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();
};
};
}
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
, type
e il contenitore del formset. (Nell'esempio sopra choices
)