Je veux ajouter automatiquement de nouveaux formulaires à un Django formset en utilisant Ajax, de sorte que lorsque l'utilisateur clique sur un bouton "ajouter", il exécute JavaScript qui ajoute un nouveau formulaire (qui fait partie du formset) à la page.
Voici comment je le fais en utilisant jQuery :
Mon template:
<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>
Dans un fichier 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);
}
Ce qu'il fait:
cloneMore
accepte selector
comme premier argument et le type
de formset comme deuxième argument. Ce que selector
devrait faire, c'est lui transmettre ce qu'il devrait dupliquer. Dans ce cas, je le passe div.table:last
Pour que jQuery recherche la dernière table avec une classe de table
. La partie :last
De celle-ci est importante car le selector
est également utilisé pour déterminer après quoi le nouveau formulaire sera inséré. Plus que probablement, vous le voudriez à la fin du reste des formulaires. L'argument type
permet de mettre à jour le champ management_form
, Notamment TOTAL_FORMS
, Ainsi que les champs de formulaire réels. Si vous avez un ensemble de formulaires plein de, par exemple, Client
modèles, les champs de gestion auront des identifiants de id_clients-TOTAL_FORMS
Et id_clients-INITIAL_FORMS
, Tandis que les champs de formulaire seront au format id_clients-N-fieldname
Avec N
étant le numéro du formulaire, commençant par 0
. Ainsi, avec l'argument type
, la fonction cloneMore
examine le nombre de formulaires existants et examine toutes les entrées et tous les libellés du nouveau formulaire en remplaçant tous les noms/identificateurs de champs provenant de id_clients-(N)-name
à id_clients-(N+1)-name
et ainsi de suite. Une fois terminé, il met à jour le champ TOTAL_FORMS
Afin de refléter le nouveau formulaire et l'ajoute à la fin de l'ensemble.
Cette fonction m’est particulièrement utile car sa configuration me permet de l’utiliser dans l’application lorsque je souhaite fournir plus de formulaires dans un formset et ne me rend pas obligé d’avoir un formulaire "modèle" masqué à dupliquer. aussi longtemps que je transmets le nom du formset et le format dans lequel les formulaires sont présentés. J'espère que ça aide.
Version simplifiée de la réponse de Paolo en utilisant empty_form
en tant que modèle.
<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>
J'ai posté un extrait à partir d'une application sur laquelle j'ai travaillé il y a quelque temps. Identique à celle de Paolo, mais vous permet également de supprimer des formulaires.
La suggestion de Paolo fonctionne à merveille avec une mise en garde: les boutons Précédent/Suivant du navigateur.
Les éléments dynamiques créés avec le script de Paolo ne seront pas rendus si l'utilisateur revient au formset à l'aide du bouton Précédent/Suivant. Un problème qui pourrait être un facteur décisif pour certains.
Exemple:
1) L'utilisateur ajoute deux nouveaux formulaires au formset en utilisant le bouton "ajouter-plus"
2) L'utilisateur remplit les formulaires et soumet le formulaire
3) L'utilisateur clique sur le bouton Précédent dans le navigateur.
4) Le jeu de formulaires est maintenant réduit à la forme d'origine, toutes les formes ajoutées dynamiquement n'y sont pas
Ce n'est pas un défaut du script de Paolo; mais une réalité de la vie avec la manipulation de dom et le cache du navigateur.
Je suppose que l'on pourrait stocker les valeurs du formulaire dans la session et avoir un peu de magie ajax lors du chargement du formset pour recréer les éléments et recharger les valeurs de la session; mais selon que vous voulez être sur le même utilisateur et sur plusieurs instances du formulaire, cela peut devenir très compliqué.
Quelqu'un a une bonne suggestion pour régler ce problème?
Merci!
Découvrez les solutions suivantes aux formulaires dynamiques Django:
http://code.google.com/p/Django-dynamic-formset/
https://github.com/javisantana/Django-dinamyc-form/tree/master/frm
Ils utilisent tous deux jQuery et sont spécifiques à Django. Le premier semble un peu plus raffiné et offre un téléchargement qui vient avec des démos qui sont excellentes.
Simulez et imitez:
<input>
des champs.<input>
_ champs modifiés.Bien que je sache que les formulaires utilisent des masques <input>
_ champs et savoir approximativement ce que le script doit faire, je ne me souviens pas des détails par coeur. Ce que j'ai décrit ci-dessus est ce que je ferais dans votre situation.
Il y a un plugin jquery pour cela , je l'ai utilisé avec inline_form défini dans Django 1.3, et cela fonctionne parfaitement, y compris le pré-peuplement, l'ajout, la suppression et la suppression de formulaires côté client. plusieurs inline_formsets.
Une autre version de cloneMore, qui permet la désinfection sélective des champs. Utilisez-le lorsque vous souhaitez empêcher l'effacement de plusieurs champs.
$('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);
}
Une option serait de créer un formset avec tous les formulaires possibles, mais de définir initialement les formulaires non requis sur masqué - c'est-à-dire, display: none;
. Lorsqu'il est nécessaire d'afficher un formulaire, définissez son affichage css sur block
ou selon ce qui convient.
Sans connaître plus de détails sur ce que fait votre "Ajax", il est difficile de donner une réponse plus détaillée.
Il y a un petit problème avec la fonction cloneMore. Etant donné que cela nettoie également la valeur des champs cachés générés automatiquement par Django, il provoque Django se plaindre si vous essayez de sauvegarder un jeu de formulaires avec plus d’un forme.
Voici un correctif:
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);
}
Je pense que c'est une bien meilleure solution.
Comment feriez-vous un formset dynamique dans Django?
Les choses ne clonent pas:
Pour les codeurs recherchant des ressources à comprendre un peu mieux les solutions ci-dessus:
Après avoir lu le lien ci-dessus, la documentation et les solutions précédentes de Django) devraient être beaucoup plus logiques.
Voici un résumé rapide de ce qui me troublait: Le formulaire de gestion contient un aperçu des formulaires qu’il contient. Vous devez garder ces informations exactes pour que Django soit au courant des formulaires que vous ajoutez. (Communauté, donnez-moi des suggestions si certaines de mes paroles sont fausses ici. Je suis nouveau à Django.)
Oui, je vous recommande également de les rendre au format HTML si vous avez un nombre fini d'entrées. (Si vous ne le faites pas, vous devrez utiliser une autre méthode).
Vous pouvez les cacher comme ça:
{% for form in spokenLanguageFormset %}
<fieldset class="languages-{{forloop.counter0 }} {% if spokenLanguageFormset.initial_forms|length < forloop.counter and forloop.counter != 1 %}hidden-form{% endif %}">
Alors le js est vraiment simple:
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();
};
};
}
@Paolo Bergantino
pour cloner tous les gestionnaires attachés, il suffit de modifier la ligne
var newElement = $(selector).clone();
for
var newElement = $(selector).clone(true);
pour éviter ce problème.
Parce que toutes les réponses ci-dessus utilisent jQuery et rendent certaines choses un peu complexes, j'ai écrit le script suivant:
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
}
};
Tout d'abord, vous devez définir auto_id sur false et désactiver ainsi la duplication de l'id et du nom. Étant donné que les noms d'entrée doivent être uniques sous leur forme, toute identification est faite avec eux et non avec les identifiants. Vous devez également remplacer le form
, type
et le conteneur du formset. (Dans l'exemple ci-dessus, choices
)