Secara dinamis menambahkan formulir ke format Django dengan Ajax

260

Saya ingin secara otomatis menambahkan formulir baru ke Django formset menggunakan Ajax, sehingga ketika pengguna mengklik tombol "add" itu menjalankan JavaScript yang menambahkan formulir baru (yang merupakan bagian dari formset) ke halaman.

Chip Tol
sumber
Saya hanya menebak kasus penggunaan Anda di sini, apakah ini seperti fitur "Lampirkan File Lain" di gmail, di mana pengguna disajikan dengan bidang unggah file dan bidang baru ditambahkan ke DOM dengan cepat saat pengguna mengklik untuk "Melampirkan File Lain" tombol plus?
prairiedogg
Ini adalah sesuatu yang akan segera saya kerjakan, jadi saya juga akan tertarik dengan jawaban apa pun.
Van Gale
2
Pertanyaan ini agak kabur, itu menyebutkan "Ajax" dalam judul, deskripsi dan tag. Namun, tidak ada jawaban yang memanfaatkan Ajax, itu masih membutuhkan formulir untuk diserahkan.
Antoine Pinsard

Jawaban:

219

Ini adalah bagaimana saya melakukannya, menggunakan jQuery :

Templat saya:

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

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

Apa fungsinya:

cloneMoremenerima selectorsebagai argumen pertama, dan typeformset sebagai argumen ke-2. Apa yang selectorharus dilakukan adalah menyampaikannya apa yang harus digandakan. Dalam hal ini, saya meneruskannya div.table:lastsehingga jQuery mencari tabel terakhir dengan kelas table. Bagian :lastitu penting karena selectorjuga digunakan untuk menentukan apa bentuk baru yang akan dimasukkan setelah. Kemungkinan besar Anda menginginkannya di akhir formulir lainnya. The typeargumen adalah agar kita dapat memperbarui management_formlapangan, terutama TOTAL_FORMS, serta kolom formulir yang sebenarnya. Jika Anda memiliki formset penuh dengan, misalnya, Clientmodel, bidang manajemen akan memiliki ID id_clients-TOTAL_FORMSdan id_clients-INITIAL_FORMS, sedangkan bidang formulir akan dalam format id_clients-N-fieldnamedenganNmenjadi nomor formulir, dimulai dengan 0. Jadi dengan typeargumen cloneMorefungsi melihat berapa banyak bentuk yang ada saat ini, dan melewati setiap input dan label di dalam formulir baru menggantikan semua nama bidang / id dari sesuatu seperti id_clients-(N)-nameke id_clients-(N+1)-namedan seterusnya. Setelah selesai, itu memperbarui TOTAL_FORMSbidang untuk mencerminkan formulir baru dan menambahkannya ke akhir set.

Fungsi ini sangat membantu saya karena cara pengaturannya memungkinkan saya untuk menggunakannya di seluruh aplikasi ketika saya ingin memberikan lebih banyak formulir dalam sebuah formset, dan tidak membuat saya perlu memiliki formulir "templat" tersembunyi untuk menduplikasi selama saya berikan nama format dan format di mana formulir diletakkan. Semoga ini bisa membantu.

Paolo Bergantino
sumber
Di IE, klon dari elemen kloning direpresentasikan sebagai <undefined> saat memilih di JS, mengapa?
panchicore
Saya menemukan bahwa di Django 1.1 Anda harus memberikan nilai kepada prefixanggota Objek Formset. Nilai ini harus sama dengan typeargumen untuk cloneMorefungsi.
Derek Reynolds
3
Saya memodifikasi ini untuk mengambil pemilih tanpa: last dan used var total = $ (selector) .length; untuk mendapatkan total saya karena penyegaran halaman akan menghapus formet saya tetapi biarkan peningkatan TOTAL mengarah ke nomor yang salah disimpan. Saya kemudian menambahkan: terakhir ke pemilih sesuai kebutuhan. Terima kasih untuk ini.
Greg
2
Saya telah menemukan bahwa ini menggunakan $ (this) .attr ({'name': name, 'id': id}). Val (''). RemoveAttr ('checked'); Untuk menghapus input akan mengacaukan kotak centang. Pengaturan val ('') memberi kotak centang atribut nilai kosong. Dan karena kotak centang tidak menggunakan atribut nilai, ini tidak akan pernah diperbarui - tidak peduli berapa kali Anda mengkliknya. Tetapi tampaknya nilai memiliki prioritas lebih tinggi daripada "dicentang" yang dikaitkan dengan kotak centang. Yang berarti Anda akan selalu mengirim kotak centang yang tidak dicentang.
niklasdstrom
tolong paolo, bisakah Anda memeriksa masalah saya stackoverflow.com/questions/62252867/…
art_cs
109

Versi sederhana dari jawaban Paolo menggunakan empty_formsebagai templat.

<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
sumber
bagaimana saya bisa menangani ini dalam tampilan? ketika saya menggunakan CompetitorFormSet = modelformset_factory(ProjectCompetitor, formset=CompetitorFormSets) ctx['competitor_form_set'] = CompetitorFormSet(request.POST)i Hanya mendapatkan satu formulir, di dalam metode bersih. dapatkah Anda menjelaskan bagaimana menangani ini dalam pandangan?
AJ
Cemerlang - terima kasih. Membuat penggunaan yang sangat baik dari pembantu Django yang tersedia (seperti empty_form), yang saya hargai.
BigglesZX
@ BigglesZX - Saya telah mengadaptasi solusi dan baris baru formulir kosong dihasilkan Namun kotak pilih menghasilkan daftar pilihan FK (tersedia), bukan drop down yang dihasilkan untuk kumpulan formulir asli. Apakah ada masalah seperti ini yang dilaporkan?
user12379095
@ Pernahkah Anda memperbarui jawaban untuk versi yang lebih baru yaitu 3.x? itu sederhana dan jelas tetapi tidak bekerja untuk saya
Poula Adel
1
@PoulaAdel Apa yang tidak berfungsi? Saya baru saja mencoba ini pada Django 3.0.5 dan masih berfungsi untuk saya. Mengejutkan setelah 8 tahun, tapi saya kira Django dan jQuery memiliki kompatibilitas yang baik dengan kode lama.
Dave
18

Saran Paolo bekerja dengan indah dengan satu peringatan - tombol kembali / maju browser.

Elemen dinamis yang dibuat dengan skrip Paolo tidak akan diberikan jika pengguna kembali ke formset menggunakan tombol kembali / maju. Masalah yang mungkin menjadi pemecah kesepakatan bagi sebagian orang.

Contoh:

1) Pengguna menambahkan dua formulir baru ke formset menggunakan tombol "tambah lagi"

2) Pengguna mengisi formulir dan mengirimkan formset

3) Pengguna mengklik tombol kembali di browser

4) Formset sekarang dikurangi menjadi bentuk asli, semua bentuk yang ditambahkan secara dinamis tidak ada

Ini sama sekali bukan cacat pada naskah Paolo; tetapi fakta kehidupan dengan manipulasi dom dan cache browser.

Saya kira orang bisa menyimpan nilai-nilai formulir di sesi dan memiliki beberapa ajax magic ketika formset memuat untuk membuat elemen lagi dan memuat ulang nilai-nilai dari sesi; tetapi tergantung pada seberapa anal Anda ingin tentang pengguna yang sama dan beberapa contoh formulir ini mungkin menjadi sangat rumit.

Adakah yang punya saran bagus untuk menangani ini?

Terima kasih!

cethegeek
sumber
2
Jika Anda mengarahkan ulang setelah pengiriman berhasil, tombol kembali bukan masalah. Jika Anda mengisi formulir dari DB pada kunjungan berikutnya, semua formulir muncul pada awalnya. Jika Anda gagal formulir karena input yang tidak valid, semuanya harus ada di redisplay dengan kesalahan. Kecuali saya tidak memahami pernyataan Anda .... Pengalihan kiriman pos sangat penting dalam aplikasi yang berfungsi dengan baik, aplikasi yang banyak codernya tidak didasarkan pada jumlah aplikasi berperilaku buruk yang saya jalankan di web.
boatcoder
dapatkah Anda membantu saya stackoverflow.com/questions/62285767/… , saya sudah mencoba banyak tetapi tidak mendapatkan jawaban! saya sangat menghargai Anda
art_cs
11

Simulasikan dan tiru:

  • Buat formset yang sesuai dengan situasi sebelum mengklik tombol "add".
  • Muat halaman, lihat sumbernya dan catat semua <input>bidang.
  • Ubah formset agar sesuai dengan situasi setelah mengklik tombol "tambah" (ubah jumlah bidang tambahan).
  • Muat halaman, lihat sumbernya dan catat bagaimana <input>ladang berubah.
  • Buat beberapa JavaScript yang memodifikasi DOM dengan cara yang sesuai untuk memindahkannya dari keadaan sebelum ke keadaan sesudahnya .
  • Lampirkan JavaScript itu ke tombol "tambah".

Meskipun saya tahu format menggunakan <input>bidang tersembunyi khusus dan tahu kira-kira apa yang harus dilakukan skrip, saya tidak mengingat detail dari atas kepala saya. Apa yang saya jelaskan di atas adalah apa yang akan saya lakukan dalam situasi Anda.

akaihola
sumber
dapatkah Anda membantu saya stackoverflow.com/questions/62285767/… , saya sudah mencoba banyak stackoverflow.com/questions/62285767/… tetapi tidak mendapatkan jawaban! saya sangat menghargai Anda
art_cs
6

Ada plugin jquery untuk ini , saya menggunakannya dengan set inline_form di Django 1.3, dan berfungsi dengan baik, termasuk prepopulasi, penambahan sisi klien, penghapusan, dan beberapa inline_formsets.

e-satis
sumber
Sementara posting blog tertaut masih ada, tautan unduhan di sana rusak. Rupanya, plugin ini dibuat oleh @ elo80ka, yang jawabannya menunjuk ke versi skrip (preliminar?).
lfurini
dapatkah Anda membantu saya stackoverflow.com/questions/62285767/… , saya sudah mencoba banyak tetapi tidak mendapatkan jawaban! saya sangat menghargai Anda
art_cs
4

Salah satu opsi adalah membuat formset dengan setiap form yang mungkin, tetapi pada awalnya mengatur form yang tidak diminta menjadi tersembunyi - yaitu display: none;,. Bila perlu untuk menampilkan formulir, atur tampilan css ke blockatau apa pun yang sesuai.

Tanpa tahu lebih detail tentang apa yang "Ajax" Anda lakukan, sulit untuk memberikan respons yang lebih detail.

Daniel Naab
sumber
4

Versi cloneMore lainnya, yang memungkinkan sanitasi bidang selektif. Gunakan saat Anda perlu mencegah beberapa bidang terhapus.

$('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
sumber
dapatkah Anda membantu saya stackoverflow.com/questions/62285767/… , saya sudah mencoba banyak tetapi tidak mendapatkan jawaban! saya sangat menghargai Anda
art_cs
2

Ada masalah kecil dengan fungsi cloneMore. Karena itu juga membersihkan nilai dari bidang tersembunyi yang dihasilkan secara otomatis Django, hal itu menyebabkan Django mengeluh jika Anda mencoba menyimpan suatu formset dengan lebih dari satu formulir kosong.

Ini adalah perbaikan:

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
sumber
dapatkah Anda membantu saya stackoverflow.com/questions/62285767/… , saya sudah mencoba banyak tetapi tidak mendapatkan jawaban! saya sangat menghargai Anda
art_cs
2

Untuk coders di luar sana yang mencari sumber daya untuk memahami solusi di atas sedikit lebih baik:

Django Dynamic Formsets

Setelah membaca tautan di atas, dokumentasi Django dan solusi sebelumnya seharusnya lebih masuk akal.

Dokumentasi Django Formset

Sebagai ringkasan singkat tentang apa yang membuat saya bingung: Formulir Manajemen berisi tinjauan umum tentang formulir-formulir di dalamnya. Anda harus menjaga agar informasi itu akurat agar Django mengetahui formulir yang Anda tambahkan. (Komunitas, tolong beri saya saran jika beberapa kata-kata saya tidak ada di sini. Saya baru mengenal Django.)

Ryan Buchmeier
sumber
1

@ Paolo Bergantino

untuk mengkloning semua handler yang terpasang, cukup modifikasi jalurnya

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

untuk

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

untuk mencegah masalah ini.

panchicore
sumber
dapatkah Anda membantu saya stackoverflow.com/questions/62285767/… , saya sudah mencoba banyak tetapi tidak mendapatkan jawaban! saya sangat menghargai Anda
art_cs
1

Ya, saya juga merekomendasikan untuk menyerahkannya di html jika Anda memiliki jumlah entri yang terbatas. (Jika tidak, Anda harus menggunakan metode lain).

Anda dapat menyembunyikannya seperti ini:

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

Maka js sangat sederhana:

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
sumber
dapatkah Anda membantu saya stackoverflow.com/questions/62285767/… , saya sudah mencoba banyak tetapi tidak mendapatkan jawaban! saya sangat menghargai Anda
art_cs
1

Karena semua jawaban di atas menggunakan jQuery dan membuat beberapa hal agak rumit saya menulis skrip berikut:

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

Pertama, Anda harus mengatur auto_id ke false sehingga menonaktifkan duplikasi id dan nama. Karena nama input harus unik dalam bentuk di sana, semua identifikasi dilakukan dengan mereka dan bukan dengan id. Anda juga harus mengganti form, typedan wadah formset. (Dalam contoh di atas choices)

R3turnz
sumber
dapatkah Anda membantu saya stackoverflow.com/questions/62285767/… , saya sudah mencoba banyak tetapi tidak mendapatkan jawaban! saya sangat menghargai Anda
art_cs