web-dev-qa-db-ja.com

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

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

252
Chip Tol

これは 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);
}

それが何をする:

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

この機能は、フォームセットでより多くのフォームを提供したいときにアプリ全体で使用できるようにセットアップされ、複製するための非表示の「テンプレート」フォームを必要としないため、特に役立ちます。フォームセット名とフォームがレイア​​ウトされる形式を渡す限り。それが役に立てば幸い。

211

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

スニペットを投稿 しばらく前に作業したアプリから。 Paoloに似ていますが、フォームを削除することもできます。

25
elo80ka

Paoloの提案は、ブラウザの「戻る/進む」ボタンという1つの注意事項でうまく機能します。

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

例:

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

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

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

4)Formsetは元のフォームに縮小され、動的に追加されたすべてのフォームはそこにありません

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

フォームの値をセッションに保存し、formsetがロードされて要素を再度作成し、セッションから値を再ロードするときにajaxマジックを使用できると思います。しかし、同じユーザーとフォームの複数のインスタンスについてどの程度肛門にしたいかによっては、これは非常に複雑になる場合があります。

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

ありがとう!

18
cethegeek

動的Djangoフォームに対する次のソリューションを確認してください。

http://code.google.com/p/Django-dynamic-formset/

https://github.com/javisantana/Django-dinamyc-form/tree/master/frm

どちらもjQueryを使用し、Django固有です。最初のものはもう少し洗練されているようで、優れたデモ付きでダウンロードできます。

13
Kreychek

シミュレートおよび模倣:

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

フォームセットが特別な非表示<input>フィールドを使用し、スクリプトが何をする必要があるかを知っていますが、頭の上の部分の詳細を思い出しません。上記で説明したことは、あなたの状況で私がすることです。

11
akaihola

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

6
e-satis

フィールドの選択的なサニタイズを可能にする別の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);
}
4
xaralis

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

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

4
Daniel Naab

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

これははるかに優れたソリューションだと思います。

Djangoで動的なフォームセットをどのように作成しますか?

クローンはしません:

  • 初期フォームが存在しない場合にフォームを追加
  • Django-ckeditorなど、JavaScriptをより適切に処理します
  • 初期データを保持
2
Bufke

上記のソリューションを少し良く理解するためにリソースを探しているコーダー向け:

Django Dynamic Formsets

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

Django Formset Documentation

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

1
Ryan Buchmeier

また、エントリの数が限られている場合は、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();
        };
    };
}
1
Bob Spryn

@パオロ・ベルガンティーノ

接続されているすべてのハンドラーを複製するには、行を変更するだけです

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

for

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

防止するために この問題

1
panchicore

上記の答えはすべて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

1
R3turnz