Djangoだけでフォームの追加ボタンを簡単に実装する方法

Djangoだけでフォームの追加ボタンを実装する

DjangoのFormって便利ですよね?

FormSetを使うことで、複数のフォームを表示させることもできます。

ただ、フォームの数を動的に増やそうとすると、少し工夫が必要です。

今回は、JavaScriptなどは一切使用せず、Djangoだけでフォームの追加ボタンを実装する方法を紹介します

Djangoの実装を追いかけながら実装したので、効率的で最小限の実装で実現できていると思っています

また、以下の記事ではDjangoが学べるおすすめのプログラミングスクールをDjangoを学ぶ目的別に紹介しています

Djangoの習得には幅広い知識が必要で、独学では大変な部分も多いと思いますので、気になる方はよければ覗いてみてください。

目次

実現したいこと

冒頭でも少し触れましたが、今回実現したいことをまとめます。

まず、djangoのFormSet機能を使って複数のフォームを作成します。

そして、ユーザーが、フォームの数が足りない時に、「追加する」ボタンを押すことで、フォームの数を増やすことができるページを作成したいです

さらに、いくつかのフォームを入力した状態でも、その情報を残したままフォームの追加を行えるようにします。

また、追加ボタンとは別に「送信する」ボタンを押すと、フォームの内容を送信することができます。

今回は、送信機能の部分は割愛して、フォームの追加機能だけを解説します。

最終的なページ表示例を載せておきますので、自分のやりたいことと一致しているか確認して下さい。

全体の手順まとめ

最初に、フォームの追加ボタン実現のための手順をまとめておきます。

手順が多いですが、ひとつずつ説明するので安心してください。

  1. forms.py、urls.py、テンプレートの作成
  2. views.pyの作成
  3. get_form_kwargsメソッドのオーバーライド
  4. postメソッドのオーバーライド

また、実装方法の説明の後、実際のdjangoの実装を確認して、なぜ今回の実装でうまくいくのか?の理由を説明します。

余裕のある人はそちらもぜひ読んでください!

forms.py、urls.py、テンプレートの作成

まずは特に特別なことは必要でないforms.pyとurls.py、そしてテンプレートHTMLを作成します。

これらは普段通りに実装すればいいです。

forms.pyの作成

まずはFormの作成です。

いつも通りにforms.pyに、必要なフォームの情報を定義していください。

今回の例では以下のようなフォームを使います。

from django import forms


class MemberForm(forms.Form):
    name = forms.CharField(max_length=100, label = '名前')
    age = forms.IntegerField(label='年齢')

urls.pyの作成

続いてURLの設定です。

今回はindexという名前をつけたページをIndexViewとして定義するので、その設定をしておきます。

nameやviewのクラス名などはご自身の環境に変えて下さい。

from django.urls import path
from . import views

urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
]

テンプレートの作成

今回用いるテンプレートは以下です。

ボタンが複数あるときは、name属性に名前をつけることがポイントです。

後で出てきますが、ビューではこのnameでどのボタンが押されたのかを判定します

今回は、追加するボタンにbtn_addという名前をつけました。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Formsetのテスト</title>
</head>
<body>
    <form method='POST'>
        {% csrf_token %}
        {% for per_form in form %}
          {{ per_form }}
          <br>
        {% endfor %}
        {{ form.management_form }}
        <button name="btn_submit" type="submit">
          送信する
        </button>
        <button name="btn_add" type="submit">
          追加する
        </button>
    </form>
    <h3>{{ message }}</h3>
</body>
</html>

Viewの作成

続いてビューの作成です。

今回は、クラスベースビューの中でも、フォームの扱いに特化したFormViewを継承したビューを使用します

FormViewの使い方は以下の記事で紹介しているので、あわせてご覧下さい。

グローバル変数とクラス変数の定義

まずは、グローバル変数とクラス変数を定義します。

from django.urls import reverse_lazy
from django.views.generic.edit import FormView
from django import forms
from .forms import MemberForm

FORM_NUM = 1      # フォーム数
FORM_VALUES = {}  # 前回のPSOT値

class IndexView(FormView):
    template_name = 'app/index.html'
    success_url = reverse_lazy('index')
    MemberFormSet = forms.formset_factory(
        form=MemberForm,
        extra=1,
        max_num=10,
    )
    form_class = MemberFormSet

グローバル変数から説明します。

1つ目は、フォーム数を保存するFORM_NUMです。

追加ボタンが押させるたびに、この数値をインクリメントしていきます

グローバル変数にしているのは、クラス変数の場合、ページをリロードするたびに初期化されてしまうからです。(今回は、追加ボタンを押すたびに新しいページを表示する形でフォーム数を増やします)

2つ目は、前回のPOST時のフォーム情報を格納するFORM_VALUESです。

こちらは、追加ボタンを押してページがリロードされても、入力中のフォームが初期化されないようにするために使用します

最初は空の辞書で初期化しておきます。

続いてクラス変数ですが、template_nameにはテンプレート名を設定し、success_urlでは、このIndexViewが再表示されるように設定しておきましょう。

form_classには、フォームセットを設定します。

フォームセットはformset_factoryで、MemberFormを複数個並べます。

フォーム数を表すextraは1で固定ですが、後でform-TOTAL_FROMSというフィールドを設定することで、フォーム数を変化させることが可能です。

get_form_kwargsのオーバーライド

まずはget_form_kwargsメソッドをオーバーライドします。

このメソッドは、ページが表示されたときに、クラス変数self.form_classのコンストラクタの引数を返すメソッドです。

デフォルトの動作では、クラス変数initialを引数initial、クラス変数prefixを引数prefixに設定します。(さらに補足すれば、デフォルトではinitialは空の辞書、prefixはNoneで初期化されています)

また、POST動作時には、form_classのコンストラクタの引数dataにself.request.POSTを代入します。

今回は、追加ボタンが押された時に、入力中のフォームがあった場合でもフォームの内容を残しておきたいので、以下のようにdataキーの値に、入力されたフォーム情報のFORM_VALUESを設定するようにオーバーライドします

class IndexView(FormView):
    (中略)
    def get_form_kwargs(self):
        # デフォルトのget_form_kwargsメソッドを呼び出す
        kwargs = super().get_form_kwargs()
        # FORM_VALUESが空でない場合(入力中のフォームがある場合)、dataキーにFORM_VALUESを設定
        if FORM_VALUES:
            kwargs['data'] = FORM_VALUES
        return kwargs

postメソッドのオーバーライド

postメソッドでは、以下2点を実装します。

  • 追加ボタンが押された時に、FORM_NUMをインクリメントする
  • 追加ボタンが押された時に、FORM_VALUESにリクエストの内容を設定する
  • FORM_VALUESのform-TOTAL_FORMSフィールドをFORM_NUMに置き換える
class IndexView(FormView):
    (中略)
    def post(self, request, *args, **kwargs):
        global FORM_NUM
        global FORM_VALUES
        # 追加ボタンが押された時の挙動
        if 'btn_add' in request.POST:
            FORM_NUM += 1    # フォーム数をインクリメント
            FORM_VALUES = request.POST.copy()  # リクエストの内容をコピー
            FORM_VALUES['form-TOTAL_FORMS'] = FORM_NUM   # フォーム数を上書き
        
        return super().post(request, args, kwargs)

重要ポイントが2つあります。

1つ目の重要ポイントは、if ‘btn_add’ in request.POSTの部分です。

これは、テンプレートにボタンが複数ある時に、どのボタンが押されたのかを判定する方法です。

今回はテンプレートで追加ボタンの名前にbtn_addを設定したので、その名前で判定しています。

実際に、追加ボタンを押した時のrequest.POSTの内容は以下のようになります。(トークン文字列は省略しています)

<QueryDict: {
    'csrfmiddlewaretoken': [トークン文字列],
    'form-0-name': [''], 'form-0-age': [''],
    'form-TOTAL_FORMS': [1], 'form-INITIAL_FORMS': ['0'], 'form-MIN_NUM_FORMS': ['0'], 'form-MAX_NUM_FORMS': ['10'],
    'btn_add': ['']
}>

最後のキーを見ると、確かにbtn_addというキーが追加されています!

2つ目の重要ポイントは、FORM_VALUES[‘form-TOTAL_FORMS’]をFORM_NUMで上書きしている点です。

このform-TOTAL_FORMSの内容がフォーム数を表しているため、ここをインクリメントした数にすることで、フォーム数を増やすことができます。

ちなみに、request.POSTはイミュータブル(編集不可)なので、copy()メソッドでコピーしないとフィールドを書き換えることができないのでご注意ください。

まとめ

以上でフォームの追加ボタンの実装ができます。

最後に、小分けにしていたFormViewの実装をまとめておきます。

from django.urls import reverse_lazy
from django.views.generic.edit import FormView
from django import forms
from .forms import MemberForm

FORM_NUM = 1
FORM_VALUES = {}

class IndexView(FormView):
    template_name = 'app/index.html'
    success_url = reverse_lazy('index')
    MemberFormSet = forms.formset_factory(
        form=MemberForm,
        extra=1,
        max_num=10,
    )
    form_class = MemberFormSet

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        log.debug(f"get_form_kwargs {FORM_VALUES}")
        if FORM_VALUES:
            kwargs['data'] = FORM_VALUES
        return kwargs
    
    def post(self, request, *args, **kwargs):
        global FORM_NUM
        global FORM_VALUES
        if 'btn_add' in request.POST:
            FORM_NUM += 1
            FORM_VALUES = request.POST.copy()
            FORM_VALUES['form-TOTAL_FORMS'] = FORM_NUM
        
        return super().post(request, args, kwargs)

あとは、送信ボタンの挙動をform_validメソッドとして定義すれば、送信機能も作れます。

form_validについては以下の記事も参考ください。

Djangoはまだまだたくさんの機能が盛り沢山です。

このサイトでは、他にもさまざまな解説記事を載せているので、ぜひ参考にしてください!

また、独学だけでなく、人から教えてもらうというのも大切です。

以下の記事ではDjangoが学べるプログラミングスクールを、目的別におすすめを紹介していますので、こちらもぜひご検討ください。

FormViewのGET時の挙動

最後に、もっと詳しく知りたい人のために、DjangoのFormViewの実装を覗いてget時の挙動を確認したいと思います。

今回、なぜget_form_kwargsメソッドをオーバーライドしたのか?の理由がみえてきます。

実際にDjangoの実装を確認することが、必要最低限の実装だけで済むようにするためのコツです。

クラス構成

Djangoのviewには様々なクラスがあり、それらを多重継承してクラスベースビューが構成されています。

今回使用するFormViewのクラス構成は以下のようになっています。

FormViewnのクラス図

FormViewはTemplateResponceMixin、BaseFormViewの2つのクラスを継承しています。

さらにBaseFormViewはFormMixin、ProcessFormViewを継承したクラスです。

FormMixin、ProcessFormViewはそれぞれContextMixin、Viewを継承しています。

get時のシーケンス

続いて、GETメソッドが呼び出されたときのFormViewの挙動をシーケンス図を使って説明します。

FormViewのget時のシーケンス図

少し複雑そうに見えるので、手順をまとめます。

  1. 最初にgetメソッドを呼び出す(ProcessFormViewで実装)
  2. get_context_dataの戻り値を引数として、render_to_reponceメソッドを呼び出す(render_to_responceはTemplateResponce、get_context_dataはFormMixinで実装)
  3. get_formメソッドを呼び出す(FormMixinで実装)
  4. get_form_kwargsメソッドの戻り値を引数として、form_classのインスタンスを生成する
  5. 生成したform_classのインスタンスを”form”というコンテキスト情報に追加して返す

get_form_kwargsをオーバーライドした理由

今回は、4.のget_form_kwargsをオーバーライドすることで、form_class(今回の場合はMemberFormSet)の引数を上書きしました

ちなみに、get_form_kwargsメソッドのオリジナルの実装は以下のようになっています。

class FormMixin(ContextMixin):
    def get_form_kwargs(self):
        """Return the keyword arguments for instantiating the form."""
        kwargs = {
            "initial": self.get_initial(),
            "prefix": self.get_prefix(),
        }

        if self.request.method in ("POST", "PUT"):
            kwargs.update(
                {
                    "data": self.request.POST,
                    "files": self.request.FILES,
                }
            )
        return kwargs

今回は、GET時の挙動なので、”data”キーの値が格納されません。(9行目のif文に入らないので)

そのため、前回のPOST時の値(self.request.POST)を、form-TOTAL_NUMキーの値をインクリメントして、代入してあげました

そのため、追加ボタンを押した時のフォームの情報が保存されつつ、フォーム数が1つ増えたフォームが作成される、という仕組みでした。


よかったらシェアしてね!
  • URLをコピーしました!

コスパ最強のサブスク型プログラミング学習サイト!


侍テラコヤのロゴ

は、50種類以上の教材が利用し放題のプログラミング学習サイト

  • 単月プランは月額4,980円から始められる
  • チャットサポートとオンラインレッスンで挫折しにくい
  • HTML/CSS/JavaScript、Python、AWS、機械学習などの幅広い教材が利用し放題

教材の豊富さと料金の低さから、「現状コスパ最強のプログラミング学習サイト」です。

無料プランでも教材の一部を利用できるので、まずは無料プランに登録してどんな教材があるかチェックしてみてください!

コメント

コメントする

CAPTCHA


目次