Ajaxを使用してDjangoフォームセットにフォームを動的に追加する


260

Ajaxを使用してDjangoフォームセットに新しいフォームを自動的に追加したいので、ユーザーが「追加」ボタンをクリックすると、新しいフォーム(フォームセットの一部)をページに追加するJavaScriptが実行されます。


私はここであなたのユースケースを推測しているだけです。それは、Gmailの「別のファイルを添付する」機能のようなものです。ユーザーにファイルアップロードフィールドが表示され、ユーザーがクリックするたびに新しいフィールドがDOMに追加されます。 「別のファイルを添付」プラスボタンに?
prairiedogg 2009

これは私がすぐに取り組むつもりであるので、私はどんな答えにも興味があります。
ヴァンゲイル

2
この質問は少しあいまいです。タイトル、説明、タグに「Ajax」が含まれています。ただし、いずれの回答もAjaxを利用しておらず、フォームを送信する必要があります。
Antoine Pinsard 2017年

回答:


219

これは私がjQueryを使用して行う方法です:

私のテンプレート:

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

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

それがすること:

cloneMoreselector最初の引数として受け入れtype、formsetの2 番目の引数として受け入れます。がselectorすべきことは、複製すべきものを渡すことです。この場合は、div.table:lastjQueryがのクラスを持つ最後のテーブルを検索するように渡しますtable。新しいフォームの挿入先を決定する:lastためにselectorも使用されるため、この部分は重要です。おそらく、残りのフォームの最後でそれを使いたいでしょう。type我々は更新できるように、引数がありmanagement_form、特に、フィールド、TOTAL_FORMSだけでなく、実際のフォームフィールドを。たとえば、Clientモデルがいっぱいのフォームセットがある場合、管理フィールドのIDはとにid_clients-TOTAL_FORMSなりid_clients-INITIAL_FORMSますが、フォームフィールドの形式はid_clients-N-fieldnamewithになります。Nで始まるフォーム番号0です。したがって、type引数を指定すると、cloneMore関数は現在存在するフォームの数を調べ、新しいフォーム内のすべての入力とラベルを調べて、id_clients-(N)-nameto id_clients-(N+1)-nameなどのすべてのフィールド名/ IDを置き換えます。完了すると、TOTAL_FORMSフィールドが更新されて新しいフォームが反映され、セットの最後に追加されます。

この機能は私にとって特に役立ちます。設定方法により、フォームセットにさらにフォームを提供したいときにアプリ全体で使用でき、複製する非表示の「テンプレート」フォームを用意する必要がないためです。私がそれに渡す限り、フォームセット名とフォームが配置されるフォーマットです。それが役に立てば幸い。


IEでは、JSで選択すると、複製された要素からの複製が<undefined>として表されます。なぜですか?
panchicore

Django 1.1ではprefix、Formsetオブジェクトのメンバーに値を割り当てる必要があることがわかりました。これは、関数のtype引数と同じ値でなければなりませんcloneMore
デレクレイノルズ

3
これを変更して、:lastなしでセレクターを取得し、var total = $(selector).length;を使用しました。ページを更新するとフォームセットは削除されますが、TOTALが増加したままになり、間違った数が保存されます。次に、必要に応じて:lastをセレクターに追加しました。これをありがとう。
グレッグ

2
これは$(this).attr({'name':name、 'id':id})。val( '')。removeAttr( 'checked');を使用していることがわかりました。入力をクリアすると、チェックボックスが台無しになります。val( '')を設定すると、チェックボックスに空の値属性が与えられます。また、チェックボックスはvalue属性を使用しないため、何回クリックしても、これは更新されません。しかし、値はチェックボックスの「チェック」属性よりも優先度が高いようです。これは、常にチェックされていないチェックボックスを投稿することを意味します。
niklasdstrom、2011

パオロあなたは私の問題をチェックすることができますしてくださいstackoverflow.com/questions/62252867/...を
art_cs

109

empty_formテンプレートとして使用したPaoloの回答の簡略版。

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

これをビューでどのように処理できますか?私が使用するときCompetitorFormSet = modelformset_factory(ProjectCompetitor, formset=CompetitorFormSets) ctx['competitor_form_set'] = CompetitorFormSet(request.POST)、クリーンな方法で、1つのフォームのみを取得します。これをビューで処理する方法を説明できますか?
AJ

すばらしい–ありがとう。利用可能なDjangoヘルパー(などempty_form)をうまく利用しています。
BigglesZX

@BigglesZX-私はソリューションを適合させ、空のフォームの新しい行が生成されています。ただし、選択ボックスは、元のフォームのセットに対して生成されるドロップダウンの代わりに、FK(利用可能)選択肢のリストを生成します。このような問題は報告されていますか?
user12379095

@Daveは、新しいバージョン、つまり3.xの回答を更新できますか?シンプルで明快ですが、効果がありません
Poula Adel

1
@PoulaAdel何が機能していませんか?私はDjango 3.0.5でこれを試してみましたが、それでもまだ動作します。8年後には驚くべきことですが、DjangoとjQueryには古いコードとの下位互換性があると思います。
デイブ


18

Paoloの提案は、1つの警告、つまりブラウザの戻る/進むボタンで美しく機能します。

Paoloのスクリプトで作成された動的要素は、ユーザーが戻る/進むボタンを使用してフォームセットに戻った場合、レンダリングされません。一部の取引ブレーカーになる可能性がある問題。

例:

1)ユーザーが「追加」ボタンを使用してフォームセットに2つの新しいフォームを追加する

2)ユーザーがフォームにデータを入力し、フォームセットを送信します

3)ユーザーがブラウザの戻るボタンをクリックする

4)フォームセットは元のフォームに縮小され、動的に追加されたフォームはすべて存在しません

これは、Paoloのスクリプトの欠陥ではありません。しかし、dom操作とブラウザのキャッシュのある現実。

フォームの値をセッションに格納し、フォームセットが読み込まれて要素を再度作成し、セッションから値を再読み込みするときに、いくつかのajaxマジックを使用できると思います。しかし、同じユーザーにしたい肛門とフォームの複数のインスタンスに応じて、これは非常に複雑になる可能性があります。

これに対処するための良い提案がありますか?

ありがとう!


2
送信が成功した後でリダイレクトする場合、戻るボタンは問題ありません。次回の訪問時にDBからフォームに入力すると、すべてのフォームが最初に表示されます。無効な入力が原因でフォームが失敗した場合、すべてがエラーで再表示されます。私があなたの発言を理解していない場合を除いて....その投稿後のリダイレクトは、正常に動作しているアプリでは本当に重要です。多くのプログラマーは、私がWebで実行する動作の悪いアプリの数に基づいていないだけです。
ボートコーダー2013年

私を助けてくれませんか。stackoverflow.com / questions / 62285767 /… 感謝します
art_cs


11

模倣して模倣する:

  • 「追加」ボタンをクリックするに、状況対応するフォームセットを作成してください。
  • ページをロードし、ソースを表示して、すべての<input>フィールドをメモします。
  • 「追加」ボタンをクリックした、状況に対応するようにフォームセットを変更します(追加フィールドの数を変更します)。
  • ページをロードし、ソースを表示して、<input>フィールドがどのように変更されたかをメモします。
  • 前の状態から後の状態に移動するために適切な方法でDOMを変更するJavaScriptを作成します。
  • そのJavaScriptを「追加」ボタンに添付します。

フォームセットが特別な隠し<input>フィールドを使用していること、およびスクリプトが何をしなければならないかはほぼわかっていますが、頭の上の詳細を思い出しません。上記で説明したのは、あなたの状況で私が何をするかです。


stackoverflow.com/questions/62285767/…を手伝ってくれませんか、私は多くのstackoverflow.com/questions/62285767/…を試しましたが、答えが得られませんでした!
どうも

6

このためのjqueryプラグインがあり、Django 1.3のinline_formセットで使用しました。事前入力、クライアント側フォームの追加、削除、および複数のinline_formsetsを含め、完全に機能します。


リンクされたブログ投稿はまだ存在していますが、そこにあるダウンロードリンクは壊れています。どうやら、プラグインは@ elo80kaによって作成されたもので、その回答はスクリプトの(暫定版?)バージョンを指しています。
lfurini

stackoverflow.com/questions/62285767/…を手伝ってもらえますか?本当にありがとう
art_cs

4

1つのオプションは、可能なすべてのフォームでフォームセットを作成することですが、最初は不要なフォームを非表示に設定します(つまり、)display: none;。フォームを表示する必要がある場合は、css displayをblock適切な値に設定します。

「Ajax」が何をしているかの詳細を知らなければ、より詳細な応答を返すのは困難です。


4

フィールドの選択的なサニタイズを可能にする別のcloneMoreバージョン。複数のフィールドが消去されないようにする必要がある場合に使用します。

$('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);
}

stackoverflow.com/questions/62285767/…を手伝ってもらえますか?本当にありがとう
art_cs

2

cloneMore関数には小さな問題があります。また、djangoの自動生成された非表示フィールドの値も消去するため、複数の空のフォームを含むフォームセットを保存しようとすると、djangoがエラーを表示します。

ここに修正があります:

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

私を助けてくれませんか。stackoverflow.com / questions / 62285767 /… 感謝します
art_cs


2

上記のソリューションをもう少しよく理解するためにリソースを探しているプログラマーのために:

Django動的フォームセット

上記のリンクを読んだ後は、Djangoのドキュメントと以前の解決策がもっと理解できるはずです。

Django Formsetドキュメント

私が混乱していることの簡単な要約として:管理フォームには、フォームの概要が含まれています。追加したフォームをDjangoが認識できるようにするには、その情報を正確に保つ必要があります。(コミュニティ、私の言い回しの一部がここにない場合は、提案をお願いします。Djangoは初めてです。)



1

ええ、エントリの数が限られている場合は、htmlでレンダリングすることをお勧めします。(そうでない場合は、別の方法を使用する必要があります)。

あなたはこのようにそれらを隠すことができます:

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

次に、jsは本当に簡単です。

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

stackoverflow.com/questions/62285767/…を手伝ってもらえますか?本当にありがとう
art_cs

1

上記のすべての答えはjQueryを使用し、いくつかのことを少し複雑にするため、次のスクリプトを記述しました。

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

最初にauto_idをfalseに設定して、IDと名前の重複を無効にする必要があります。入力名はそこの形式で一意である必要があるため、すべての識別はIDではなくそれらで行われます。また、交換する必要がありformtypeそしてフォームセットのコンテナ。(上記の例ではchoices


stackoverflow.com/questions/62285767/…を手伝ってもらえますか?本当にありがとう
art_cs
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.