Dynamiczne dodawanie formularza do zestawu formularzy Django za pomocą Ajax

260

Chcę automatycznie dodawać nowe formularze do zestawu formularzy Django za pomocą Ajax, aby gdy użytkownik kliknie przycisk „dodaj”, uruchomi JavaScript, który dodaje nowy formularz (który jest częścią zestawu formularzy) do strony.

Chip Tol
źródło
Zgaduję tutaj, co to jest przypadek użycia, czy jest to coś w rodzaju funkcji „Dołącz inny plik” w Gmailu, gdzie użytkownik otrzymuje pole przesyłania pliku i nowe pola są dodawane do DOM w locie, gdy użytkownik klika przycisk „Dołącz inny plik” plus?
prairiedogg
Niedługo nad tym zacznę pracować, więc będę również zainteresowany wszelkimi odpowiedziami.
Van Gale,
2
To pytanie jest nieco rozmyte, w tytule, opisie i tagach wspomina o „Ajax”. Jednak żadna z odpowiedzi nie korzysta z Ajax, nadal wymaga przesłania formularza.
Antoine Pinsard

Odpowiedzi:

219

Tak to robię za pomocą jQuery :

Mój szablon:

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

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

Co to robi:

cloneMoreprzyjmuje selectorjako pierwszy argument, a typeformset jako drugi. To, co selectornależy zrobić, to przekazać to, co powinno się powielić. W tym przypadku przekazuję go div.table:last, aby jQuery szukał ostatniej tabeli z klasą table. Ta :lastczęść jest ważna, ponieważ selectorsłuży również do określenia, po czym zostanie wstawiony nowy formularz. Bardziej niż prawdopodobne, że będziesz tego chciał na końcu pozostałych formularzy. typeArgument jest tak, że możemy zaktualizować management_formpole, zwłaszcza TOTAL_FORMS, jak również rzeczywistych pól formularza. Jeśli masz zestaw formularzy pełen, powiedzmy, Clientmodeli, pola zarządzania będą miały identyfikatory id_clients-TOTAL_FORMSi id_clients-INITIAL_FORMS, podczas gdy pola formularza będą miały format id_clients-N-fieldnamezNbędący numerem formularza, zaczynając od 0. Więc z typeargumentu cloneMorewygląd funkcyjne, jak wiele form Obecnie są i przechodzi przez każdego wejścia i etykiety wewnątrz nowej formie zastępujący wszystkie nazwy pól / numery z czegoś podobnego id_clients-(N)-namedo id_clients-(N+1)-namei tak dalej. Po zakończeniu aktualizuje TOTAL_FORMSpole, aby odzwierciedlić nowy formularz i dodaje go na końcu zestawu.

Ta funkcja jest dla mnie szczególnie pomocna, ponieważ sposób jej konfiguracji pozwala mi korzystać z niej w całej aplikacji, gdy chcę udostępnić więcej formularzy w zestawie formularzy, i nie wymaga posiadania ukrytego formularza „szablonu” do kopiowania tak długo, jak go przekażę, nazwa zestawu formularzy i format, w jakim formularze są ułożone. Mam nadzieję, że to pomoże.

Paolo Bergantino
źródło
W IE klon z klonowanego elementu jest reprezentowany jako <niezdefiniowany> podczas wybierania w JS, dlaczego?
panchicore
Odkryłem, że w Django 1.1 musisz przypisać wartość do prefixelementu obiektu Formset. Powinna mieć tę samą wartość co typeargument cloneMorefunkcji.
Derek Reynolds,
3
Zmodyfikowałem to, aby wziąć selektor bez: ostatniego i użyłem var total = $ (selektor) .length; aby uzyskać moją sumę, ponieważ odświeżenie strony spowoduje usunięcie moich zestawów formularzy, ale pozostawi wzrost TOTAL prowadzący do zapisania niewłaściwej liczby. Następnie dodałem: w razie potrzeby ostatni do selektora. Dziękuję za to.
Greg
2
Przekonałem się, że to za pomocą $ (this) .attr ({'name': name, 'id': id}). Val (''). RemoveAttr ('zaznaczone'); Aby wyczyścić dane wejściowe, zepsują się pola wyboru. Ustawienie wartości val ('') nadaje polom wyboru pusty atrybut wartości. A ponieważ pola wyboru nie używają atrybutu wartości, nigdy nie zostanie zaktualizowany - bez względu na to, ile razy go klikniesz. Wygląda jednak na to, że wartość ma wyższy priorytet niż „zaznaczone” przypisanie pól wyboru. Co oznacza, że ​​zawsze będziesz publikować niezaznaczone pola wyboru.
niklasdstrom
proszę paolo, czy możesz sprawdzić mój problem stackoverflow.com/questions/62252867/…
art_cs
109

Uproszczona wersja odpowiedzi Paolo wykorzystująca empty_formjako szablon.

<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
źródło
jak mogę sobie z tym poradzić w widoku? kiedy używam CompetitorFormSet = modelformset_factory(ProjectCompetitor, formset=CompetitorFormSets) ctx['competitor_form_set'] = CompetitorFormSet(request.POST)i otrzymuję tylko jeden formularz, w czystej metodzie. czy możesz wyjaśnić, jak sobie z tym poradzić w widokach?
AJ
Genialne - dziękuję. Doskonale wykorzystuje dostępnych pomocników Django (takich jak empty_form), co doceniam.
BigglesZX
@BigglesZX - Dostosowałem rozwiązanie i generowane są nowe rzędy pustych formularzy. Jednak pola wyboru generują listę opcji FK (dostępnych), zamiast list rozwijanych, które w innym przypadku są generowane dla oryginalnego zestawu formularzy. Czy zgłoszono jakikolwiek problem tego rodzaju?
user12379095
@Dave czy mógłbyś zaktualizować odpowiedź dla późniejszych wersji tj. 3.x? to proste i jasne, ale dla mnie nie działa
Poula Adel
1
@PoulaAdel Co nie działa? Właśnie wypróbowałem to na Django 3.0.5 i nadal działa dla mnie. Zaskakujące po 8 latach, ale myślę, że Django i jQuery mają dobrą kompatybilność wsteczną ze starszym kodem.
Dave
18

Sugestia Paolo działa pięknie z jednym zastrzeżeniem - przyciskami Wstecz / Dalej przeglądarki.

Elementy dynamiczne utworzone za pomocą skryptu Paolo nie będą renderowane, jeśli użytkownik powróci do zestawu formularzy za pomocą przycisku Wstecz / Dalej. Problem, który może w niektórych przypadkach stanowić przeszkodę.

Przykład:

1) Użytkownik dodaje dwa nowe formularze do zestawu formularzy za pomocą przycisku „dodaj więcej”

2) Użytkownik wypełnia formularze i przesyła zestaw formularzy

3) Użytkownik klika przycisk Wstecz w przeglądarce

4) Zestaw formularzy został teraz zredukowany do pierwotnego formularza, nie ma tam wszystkich dynamicznie dodanych formularzy

Nie jest to wcale wada skryptu Paolo; ale faktem jest manipulacja domem i pamięć podręczna przeglądarki.

Przypuszczam, że można zapisać wartości formularza w sesji i mieć trochę magii ajax, gdy zestaw formularzy ładuje się, aby ponownie utworzyć elementy i ponownie załadować wartości z sesji; ale w zależności od tego, jak analny chcesz być na temat tego samego użytkownika i wielu wystąpień formularza, może to być bardzo skomplikowane.

Czy ktoś ma dobrą sugestię, jak sobie z tym poradzić?

Dzięki!

cethegeek
źródło
2
Jeśli przekierujesz po udanym przesłaniu, przycisk Wstecz nie stanowi problemu. Jeśli wypełnisz formularze z bazy danych przy następnej wizycie, wszystkie formularze pojawią się na początku. Jeśli nie uda się wypełnić formularzy z powodu nieprawidłowych danych wejściowych, wszystkie powinny znajdować się na ekranie z błędami. Chyba że nie rozumiem twoich oświadczeń ... Przekierowanie przesyłania postów jest naprawdę ważne w dobrze działającej aplikacji, której wielu programistów po prostu nie rozumie w oparciu o liczbę źle zachowujących się aplikacji, na które natrafiam w Internecie.
Boatcoder
czy możesz mi pomóc stackoverflow.com/questions/62285767/... , próbowałem już dużo, ale nie otrzymałem odpowiedzi! bardzo cię doceniam
art_cs
11

Symuluj i naśladuj:

  • Utwórz zestaw formularzy odpowiadający sytuacji przed kliknięciem przycisku „dodaj”.
  • Załaduj stronę, wyświetl źródło i zanotuj wszystkie <input>pola.
  • Zmodyfikuj zestaw formularzy, aby odpowiadał sytuacji po kliknięciu przycisku „dodaj” (zmień liczbę dodatkowych pól).
  • Załaduj stronę, wyświetl źródło i zanotuj <input>zmiany w polach.
  • Utwórz JavaScript, który modyfikuje DOM w odpowiedni sposób, aby przenieść go ze stanu przed do stanu po .
  • Załącz ten JavaScript do przycisku „dodaj”.

Chociaż wiem, że zestawy formularzy używają specjalnych ukrytych <input>pól i wiem mniej więcej, co skrypt musi zrobić, nie pamiętam szczegółów z góry głowy. Powyżej opisałem, co zrobiłbym w twojej sytuacji.

akaihola
źródło
czy możesz mi pomóc stackoverflow.com/questions/62285767/... , próbowałem już dużo stackoverflow.com/questions/62285767/... ale nie otrzymałem odpowiedzi! bardzo cię doceniam
art_cs
6

Jest do tego wtyczka jquery , użyłem jej z zestawem inline_form w Django 1.3 i działa idealnie, w tym prepopulation, dodawanie, usuwanie i wiele formularzy po stronie klienta.

e-satis
źródło
Chociaż link do blogu nadal istnieje, łącza do pobierania są zepsute. Najwyraźniej wtyczka została stworzona przez @ elo80ka, której odpowiedź wskazuje na (wstępną?) Wersję skryptu.
lfurini
czy możesz mi pomóc stackoverflow.com/questions/62285767/... , próbowałem już dużo, ale nie otrzymałem odpowiedzi! bardzo cię doceniam
art_cs
4

Jedną z opcji byłoby utworzenie zestawu formularzy z każdą możliwą formą, ale początkowo ustaw niepotrzebne formularze na ukryte - tj display: none;. Gdy konieczne jest wyświetlenie formularza, ustaw jego wyświetlanie css na blocklub cokolwiek jest odpowiednie.

Nie znając więcej szczegółów na temat tego, co robi Twój „Ajax”, trudno jest podać bardziej szczegółową odpowiedź.

Daniel Naab
źródło
4

Kolejna wersja cloneMore, która pozwala na selektywną dezynfekcję pól. Użyj go, gdy chcesz zapobiec skasowaniu kilku pól.

$('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
źródło
czy możesz mi pomóc stackoverflow.com/questions/62285767/... , próbowałem już dużo, ale nie otrzymałem odpowiedzi! bardzo cię doceniam
art_cs
2

Istnieje niewielki problem z funkcją cloneMore. Ponieważ usuwa również wartość ukrytych pól automatycznie generowanych przez django, powoduje, że django narzeka, jeśli spróbujesz zapisać zestaw formularzy z więcej niż jednym pustym formularzem.

Oto poprawka:

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
źródło
czy możesz mi pomóc stackoverflow.com/questions/62285767/... , próbowałem już dużo, ale nie otrzymałem odpowiedzi! bardzo cię doceniam
art_cs
2

Aby koderzy szukający zasobów mogli lepiej zrozumieć powyższe rozwiązania:

Dynamiczne zestawy formularzy Django

Po przeczytaniu powyższego linku dokumentacja Django i poprzednie rozwiązania powinny mieć o wiele większy sens.

Dokumentacja zestawu Django

Jako krótkie podsumowanie tego, co mnie pomyliło: Formularz zarządzania zawiera przegląd formularzy w nim zawartych. Musisz zachować dokładność tych informacji, aby Django wiedział o dodawanych formularzach. (Społeczność, proszę o sugestie, jeśli niektóre z moich sformułowań są tutaj. Jestem nowy w Django.)

Ryan Buchmeier
źródło
1

@Paolo Bergantino

aby sklonować wszystkie dołączone programy obsługi, wystarczy zmodyfikować linię

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

dla

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

aby zapobiec temu problemowi.

panchicore
źródło
czy możesz mi pomóc stackoverflow.com/questions/62285767/... , próbowałem już dużo, ale nie otrzymałem odpowiedzi! bardzo cię doceniam
art_cs
1

Tak, polecam również renderowanie ich w pliku HTML, jeśli masz skończoną liczbę wpisów. (Jeśli nie, będziesz musiał użyć innej metody).

Możesz je ukryć w następujący sposób:

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

Zatem js jest naprawdę prosty:

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
źródło
czy możesz mi pomóc stackoverflow.com/questions/62285767/... , próbowałem już dużo, ale nie otrzymałem odpowiedzi! bardzo cię doceniam
art_cs
1

Ponieważ wszystkie powyższe odpowiedzi używają jQuery i sprawiają, że niektóre rzeczy są nieco skomplikowane, napisałem następujący skrypt:

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

Najpierw powinieneś ustawić auto_id na false, a zatem wyłączyć powielanie identyfikatora i nazwy. Ponieważ nazwy wejściowe muszą być tam unikalne, cała identyfikacja odbywa się za ich pomocą, a nie za pomocą identyfikatorów. Trzeba też wymienić form, typea pojemnik z formset. (W powyższym przykładzie choices)

R3turnz
źródło
czy możesz mi pomóc stackoverflow.com/questions/62285767/... , próbowałem już dużo, ale nie otrzymałem odpowiedzi! bardzo cię doceniam
art_cs