Ajout dynamique d'un formulaire à un jeu de formulaires Django avec Ajax

260

Je veux ajouter automatiquement de nouveaux formulaires à un jeu de formulaires Django à l'aide d'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 jeu de formulaires) à la page.

Chip Tol
la source
Je devine juste votre cas d'utilisation ici, est-ce quelque chose comme la fonctionnalité "Joindre un autre fichier" dans gmail, où l'utilisateur est présenté avec un champ de téléchargement de fichier et de nouveaux champs sont ajoutés au DOM à la volée lorsque l'utilisateur clique à "Joindre un autre fichier" plus le bouton?
prairiedogg
C'est quelque chose sur lequel j'allais travailler bientôt, donc je serai également intéressé par toutes les réponses.
Van Gale
2
Cette question est un peu floue, elle mentionne "Ajax" dans le titre, la description et les balises. Cependant, aucune des réponses ne fait appel à Ajax, il faut tout de même soumettre le formulaire.
Antoine Pinsard

Réponses:

219

Voici comment je le fais, en utilisant jQuery :

Mon modèle:

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

cloneMoreaccepte selectorcomme premier argument, et typede formset comme 2e argument . Ce qu'il selectorfaut faire, c'est lui transmettre ce qu'il doit reproduire. Dans ce cas, je le passe div.table:lastpour que jQuery recherche la dernière table avec une classe de table. La :lastpartie est importante car le selectorest également utilisé pour déterminer ce que le nouveau formulaire sera inséré après. Il est plus que probable que vous le souhaiteriez à la fin du reste des formulaires. L' typeargument est que nous pouvons mettre à jour le management_formchamp, notamment TOTAL_FORMS, ainsi que les champs de formulaire réels. Si vous avez un jeu de formulaires rempli, par exemple, de Clientmodèles, les champs de gestion auront des ID de id_clients-TOTAL_FORMSet id_clients-INITIAL_FORMS, tandis que les champs de formulaire seront au format id_clients-N-fieldnameavecNétant le numéro de formulaire, en commençant par 0. Donc , avec l' typeargument de la cloneMorefonction ressemble à combien de formes Actuellement , il n'y, et passe par chaque entrée et étiquette à l' intérieur du nouveau formulaire remplaçant tous les noms / ids de quelque chose comme champ id_clients-(N)-nameà id_clients-(N+1)-nameet ainsi de suite. Une fois terminé, il met à jour le TOTAL_FORMSchamp pour refléter le nouveau formulaire et l'ajoute à la fin de l'ensemble.

Cette fonction est particulièrement utile pour moi car la façon dont elle est configurée me permet de l'utiliser dans toute l'application lorsque je veux fournir plus de formulaires dans un jeu de formulaires, et ne me fait pas avoir besoin d'un formulaire "modèle" caché pour dupliquer tant que je lui transmets le nom du jeu de formulaires et le format dans lequel les formulaires sont disposés. J'espère que ça aide.

Paolo Bergantino
la source
Dans IE, un clone d'un élément cloné est représenté comme <non défini> lors de la sélection dans JS, pourquoi?
panchicore
J'ai trouvé que dans Django 1.1, vous devrez attribuer une valeur au prefixmembre de l'objet Formset. Cela devrait avoir la même valeur que l' typeargument de la cloneMorefonction.
Derek Reynolds
3
J'ai modifié ceci pour prendre le sélecteur sans: dernier et utilisé var total = $ (sélecteur) .length; pour obtenir mon total car un rafraîchissement de la page supprimerait mes ensembles de formulaires mais laisserait l'augmentation TOTALE conduisant à l'enregistrement du mauvais numéro. J'ai ensuite ajouté: dernier au sélecteur au besoin. Merci pour cela.
Greg
2
J'ai trouvé que cela en utilisant $ (this) .attr ({'name': nom, 'id': id}). Val (''). RemoveAttr ('checked'); Pour effacer l'entrée, les cases à cocher seront gâchées. La définition de val ('') donne aux cases à cocher un attribut de valeur vide. Et puisque les cases à cocher n'utilisent pas l'attribut value, cela ne sera jamais mis à jour - peu importe le nombre de fois que vous cliquez dessus. Mais il semble que la valeur ait une priorité plus élevée que l'attribution "cochée" des cases à cocher. Ce qui signifie que vous posterez toujours des cases à cocher non cochées.
niklasdstrom
s'il vous plaît, paolo, pouvez-vous vérifier mon problème stackoverflow.com/questions/62252867/…
art_cs
109

Version simplifiée de la réponse de Paolo en utilisant empty_formcomme 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>
Dave
la source
comment puis-je traiter cela dans la vue? quand j'utilise CompetitorFormSet = modelformset_factory(ProjectCompetitor, formset=CompetitorFormSets) ctx['competitor_form_set'] = CompetitorFormSet(request.POST)je reçois seulement un formulaire, en méthode propre. pouvez-vous expliquer comment traiter cela dans les vues?
AJ
Génial - merci. Fait une excellente utilisation des assistants Django disponibles (comme empty_form), ce que j'apprécie.
BigglesZX
@BigglesZX - J'ai adapté la solution et les nouvelles lignes de formulaires vides sont générées. Cependant, les cases de sélection génèrent une liste de choix FK (disponibles), au lieu de listes déroulantes qui sont autrement générées pour l'ensemble de formulaires d'origine. Un problème de cette nature a-t-il été signalé?
user12379095
@Dave pourriez-vous mettre à jour la réponse pour les versions ultérieures, par exemple 3.x? c'est simple et clair mais ça ne marche pas pour moi
Poula Adel
1
@PoulaAdel Qu'est-ce qui ne fonctionne pas? Je viens de l'essayer sur Django 3.0.5 et cela fonctionne toujours pour moi. Surprenant après 8 ans, mais je suppose que Django et jQuery ont une bonne compatibilité descendante avec le code plus ancien.
Dave
18

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 retourne au jeu de formulaires à l'aide du bouton Précédent / Suivant. Un problème qui peut être une rupture pour certains.

Exemple:

1) L'utilisateur ajoute deux nouveaux formulaires au jeu de formulaires à l'aide du bouton "ajouter plus"

2) L'utilisateur remplit les formulaires et soumet le jeu de formulaires

3) L'utilisateur clique sur le bouton de retour dans le navigateur

4) Formset est maintenant réduit au formulaire d'origine, tous les formulaires ajoutés dynamiquement ne sont pas là

Ce n'est pas du tout un défaut du script de Paolo; mais une réalité 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 lorsque le formset se charge pour recréer les éléments et recharger les valeurs de la session; mais selon la façon dont vous voulez être anal avec le même utilisateur et plusieurs instances du formulaire, cela peut devenir très compliqué.

Quelqu'un a une bonne suggestion pour faire face à cela?

Merci!

cethegeek
la source
2
Si vous redirigez après une soumission réussie, le bouton de retour n'est pas un problème. Si vous remplissez les formulaires de la BD lors de la prochaine visite, tous les formulaires apparaissent initialement. Si vous échouez les formulaires en raison d'une entrée non valide, tous doivent être présents sur le réaffichage avec des erreurs. À moins que je ne comprenne pas vos déclarations ... Cette redirection de post-soumission est vraiment importante dans une bonne application qui fonctionne, beaucoup de codeurs ne sont tout simplement pas basés sur le nombre d'applications qui se comportent mal sur le Web.
Boatcoder
pouvez-vous m'aider stackoverflow.com/questions/62285767/… , j'ai essayé beaucoup mais n'ai pas eu de réponse! je vous apprécie beaucoup
art_cs
11

Simulez et imitez:

  • Créez un jeu de formulaires qui correspond à la situation avant de cliquer sur le bouton "ajouter".
  • Chargez la page, affichez la source et notez tous les <input>champs.
  • Modifiez le jeu de formulaires pour qu'il corresponde à la situation après avoir cliqué sur le bouton "ajouter" (modifiez le nombre de champs supplémentaires).
  • Chargez la page, affichez la source et notez comment les <input>champs ont changé.
  • Créez du JavaScript qui modifie le DOM de manière appropriée pour le déplacer de l' état avant à l' état après .
  • Attachez ce JavaScript au bouton "ajouter".

Bien que je sache que les jeux de formulaires utilisent des <input>champs cachés spéciaux et savent approximativement ce que le script doit faire, je ne me souviens pas des détails du haut de ma tête. Ce que j'ai décrit ci-dessus est ce que je ferais dans votre situation.

akaihola
la source
pouvez-vous m'aider stackoverflow.com/questions/62285767/… , j'ai essayé beaucoup stackoverflow.com/questions/62285767/… mais je n'ai pas eu de réponse! je vous apprécie beaucoup
art_cs
6

Il existe un plugin jquery pour cela , je l'ai utilisé avec inline_form défini dans Django 1.3, et cela fonctionne parfaitement, y compris la pré-remplissage, l'ajout, la suppression et la suppression de plusieurs formulaires inline côté client.

e-satis
la source
Bien que l'article de blog lié existe toujours, les liens de téléchargement sont rompus. Apparemment, le plugin a été créé par @ elo80ka, dont la réponse pointe vers une version (préliminaire?) Du script.
lfurini
pouvez-vous m'aider stackoverflow.com/questions/62285767/… , j'ai essayé beaucoup mais n'ai pas eu de réponse! je vous apprécie beaucoup
art_cs
4

Une option consisterait à créer un jeu de formulaires avec tous les formulaires possibles, mais définissez initialement les formulaires non requis sur caché, c'est-à-dire display: none;. Lorsqu'il est nécessaire d'afficher un formulaire, définissez son affichage css sur blockou tout ce qui est approprié.

Sans savoir plus de détails sur ce que fait votre "Ajax", il est difficile de donner une réponse plus détaillée.

Daniel Naab
la source
4

Une autre version cloneMore, qui permet un nettoyage sélectif des champs. Utilisez-le lorsque vous devez 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);
}
xaralis
la source
pouvez-vous m'aider stackoverflow.com/questions/62285767/… , j'ai essayé beaucoup mais n'ai pas eu de réponse! je vous apprécie beaucoup
art_cs
2

Il y a un petit problème avec la fonction cloneMore. Puisqu'il nettoie également la valeur des champs cachés générés automatiquement par django, django se plaint si vous essayez d'enregistrer un jeu de formulaires avec plusieurs formulaires vides.

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);
}
Cesar Canassa
la source
pouvez-vous m'aider stackoverflow.com/questions/62285767/… , j'ai essayé beaucoup mais n'ai pas eu de réponse! je vous apprécie beaucoup
art_cs
2

Pour que les codeurs qui recherchent des ressources comprennent un peu mieux les solutions ci-dessus:

Django Dynamic Formsets

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

Documentation de Django Formset

Pour résumer rapidement ce qui m'embarrassait: le formulaire de gestion contient un aperçu des formulaires qu'il contient. Vous devez conserver ces informations exactes afin que Django soit au courant des formulaires que vous ajoutez. (Communauté, donnez-moi s'il vous plaît des suggestions si certains de mes mots sont ici. Je suis nouveau à Django.)

Ryan Buchmeier
la source
1

@Paolo Bergantino

pour cloner tous les gestionnaires attachés, il suffit de modifier la ligne

var newElement = $(selector).clone();

pour

var newElement = $(selector).clone(true);

pour éviter ce problème.

panchicore
la source
pouvez-vous m'aider stackoverflow.com/questions/62285767/… , j'ai essayé beaucoup mais n'ai pas eu de réponse! je vous apprécie beaucoup
art_cs
1

Oui, je recommanderais également de les afficher en 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 masquer comme ceci:

{% 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();
        };
    };
}
Bob Spryn
la source
pouvez-vous m'aider stackoverflow.com/questions/62285767/… , j'ai essayé beaucoup mais n'ai pas eu de réponse! je vous apprécie beaucoup
art_cs
1

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

Vous devez d'abord définir auto_id sur false et ainsi désactiver la duplication de l'id et du nom. Étant donné que les noms d'entrée doivent être uniques sous cette forme, toute identification se fait avec eux et non avec des identifiants. Vous devez également remplacer le form, typeet le récipient du formset. (Dans l'exemple ci-dessus choices)

R3turnz
la source
pouvez-vous m'aider stackoverflow.com/questions/62285767/… , j'ai essayé beaucoup mais n'ai pas eu de réponse! je vous apprécie beaucoup
art_cs