web-dev-qa-db-fra.com

Ajouter dynamiquement un formulaire à un Django formset avec Ajax

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.

252
Chip Tol

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.

211
Paolo Bergantino

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

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.

25
elo80ka

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!

18
cethegeek

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.

13
Kreychek

Simulez et imitez:

  • Créez un formset qui correspond à la situation avant en cliquant sur le bouton "ajouter".
  • Chargez la page, affichez la source et notez tous les <input> des champs.
  • Modifiez le formulaire pour qu'il corresponde à la situation after en cliquant sur le bouton "ajouter" (modifiez le nombre de champs supplémentaires).
  • Chargez la page, affichez la source et notez comment le <input> _ champs modifiés.
  • Créez du code JavaScript qui modifie le DOM de manière appropriée pour le déplacer de l'état before à l'état after.
  • Attachez ce JavaScript au bouton "ajouter".

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.

11
akaihola

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.

6
e-satis

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

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.

4
Daniel Naab

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);
}
2
Cesar Canassa

Je pense que c'est une bien meilleure solution.

Comment feriez-vous un formset dynamique dans Django?

Les choses ne clonent pas:

  • Ajouter un formulaire lorsqu'il n'existe pas de formulaire initial
  • Gère mieux le javascript sous la forme, par exemple Django-ckeditor
  • Conserver les données initiales
2
Bufke

Pour les codeurs recherchant des ressources à comprendre un peu mieux les solutions ci-dessus:

Django Dynamic Formsets

Après avoir lu le lien ci-dessus, la documentation et les solutions précédentes de Django) devraient être beaucoup plus logiques.

Documentation Django Formset

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

1
Ryan Buchmeier

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

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

1
panchicore

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)

1
R3turnz