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

当ページのリンクには広告が含まれています。
Djangoだけでフォームの追加ボタンを実装する

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

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

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

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

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

ちなみに、Djangoに必要なHTML/CSS、JavaScriptなどのWeb開発系言語はという学習サイトで無料で学習できるのでおすすめですよ!

\無料プランを無期限で試す/

メールアドレスだけで10秒で登録!

目次

実現したいこと

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

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

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

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

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

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

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

全体の手順まとめ

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

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

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

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

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

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

まずはmodels.pyとforms.py、urls.py、そしてテンプレートHTMLを作成します。

models.pyの作成

今回は文字列のnameと、整数のageをフィールドに持つMemberモデルを作成します。

from django.db import models

class Member(models.Model):
    name = models.CharField(max_length=100)
    age = models.IntegerField()

forms.pyの作成

続いてフォームの作成です。

モデルからフォームを簡単に作成できるModelFormを利用します。

from django import forms
from .models import Member

class MemberForm(forms.ModelForm):
    class Meta:
        model = Member
        fields = '__all__'
        labels = {'name': '名前', 'age': '年齢'}

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 and 'btn_add' in 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()メソッドでコピーしないとフィールドを書き換えることができないのでご注意ください。

フォームの保存に対応する

以上で、フォームの追加ボタンの実装は完了です。

しかし、このままでは送信ボタンを押しても何も起きません。

実際には、送信ボタンを押したらフォームの内容をデータベースに保存することが多いかと思います。

そこで、送信ボタンを押したらモデルにデータを保存するように改良します。

urls.pyの改良

今回は、送信ボタンを押したら「登録データの一覧ページ」が表示されるようにします。

そのため、一覧ページへのルーティングをurls.pyに追加しておきましょう。

一覧ページはMemberListViewというビュークラスで定義し、URLはlistという名前にします。

from django.urls import path
from . import views

urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('list/', views.MemberListView.as_view(), name='list')  # 一覧ページへのルーティング追加
]

一覧ページのテンプレートの追加

一覧ページは以下のようにシンプルにnameとageを表示します。

データがからの時の一覧ページのサンプル
データがない時の一覧ページ
一覧ページのサンプル
データがある時の一覧ページ

今回はクラスベースビューのListViewを使うので、{モデル名}_list.htmlというファイル名にしておくと楽ですよ。

ListViewに関しては以下の記事を参考にしてください!

【Django入門】CreateView、ListViewの使い方と実装

<h1>Members</h1>
<ul>
{% for member in object_list %}
    <li>
        {{ member.name }}({{ member.age }})
    </li>
{% empty %}
    <li>データがありません</li>
{% endfor %}
<a href="{% url 'index' %}">登録ページ</a>

views.pyの改良

では、views.pyに以下の修正を加えます。

  • 一覧ページのクラスを作る
  • 送信ボタンでデータを登録するようにする

一覧ページ用ビューの作成

まずは一覧ページのクラスを作成します。

今回は簡単に一覧ページが作成できるListViewを継承したクラスを定義します。

ListViewの詳しい解説はこちら

from django.views.generic.list import ListView
from .models import Member

class MemberListView(ListView):
    model = Member

このように、ListViewを使えば数行で一覧ページが実現できます。

クラス変数modelにモデル名を加えるだけでOKです。

送信ボタンの処理の実装

続いて、肝心の送信ボタンの処理を実装していきます。

まずは、post関数に、送信ボタンが押されたときの処理を追加します。

追加ボタンの時と同様に、ボタン名btn_submitrequest.POSTに含まれる場合、送信ボタンが押されたと判定できます。

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   # フォーム数を上書き

        # 送信ボタンが押された時の挙動(ここを追加します!)
        elif 'btn_submit' in request.POST:
            self.success_url = reverse_lazy('list')
            FORM_VALUES = {}
        
        return super().post(request, args, kwargs)

条件文の中の2行について説明します。

self.success_url = reverse_lazy('list')は、送信ボタンが押された時は一覧ページへ遷移してほしいので、クラス変数のself.success_urlを変更しています。

FORM_VALUES = {}では、送信ボタンが押された時は、次の登録時のフォームに値が残ってほしくないので、グローバル変数のFORM_VALUESをからの辞書に初期化しています。

これで、送信ボタンを押して一覧ページに遷移する処理が実装できました。

ただしこのままだと、送信ボタンを押してもデータが登録されないので、データの登録処理を実装します。

データの登録処理は、form_valid()メソッドに定義します。

このform_valid()メソッドは、フォームがPOSTされてから、フォーム内容の検証(バリデーション)が成功した時に、最終的に呼び出されるメソッドです。

もともとDjangoで定義されているform_valid()は、単純にself.success_urlに指定されたページへ遷移するだけ処理が実装されています。

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

【Django】FormViewでform_validをオーバーライドしてPOST時の動作を追加する

今回は、ページ遷移前にデータの登録を行うように修正しましょう。

class IndexView(FormView):
    (中略)

    # form_validをオーバーライドしてデータの登録処理を実装する
    def form_valid(self, form):
        # 送信ボタンが押されたとき
        if 'btn_submit' in self.request.POST:
            # バリデーション済みのデータを取得
            data = form.cleaned_data

            # フォームが複数あるので、一つずつループする
            for member_parameter in data:
                # フォームがからの可能性があるので、空ではないデータ飲み登録
                if member_parameter:
                    # フォームのデータでMemberモデルのオブジェクトを作成
                    member = Member(**member_parameter)
                    # データを登録
                    member.save()
        return super().form_valid(form)

ポイントとしては、

  • form.cleaned_dataで入力されたデータを取り出す
  • フォームを追加して、複数になっている可能性があるのでfor member_parameter in dataでひとつずつループ
  • 追加したいくつかのフォームが空の可能性があるので、if member_parameterのように空ではないときだけ登録処理を行うようにする
  • member = Member(**member_parameter)で、Memberモデルのオブジェクトを作る
  • member.save()で、作成したオブジェクトを登録する

ループして全てのデータを登録しているので、もちろん複数データを送信しても登録されますよ!

フォームページ 複数データを同時登録したとき
複数データを同時に送信
一覧ページ 複数データを同時登録したとき
すべてのデータがきちんと登録される

まとめ

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

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

from django.urls import reverse_lazy
from django.views.generic.edit import FormView
from django.views.generic.list import ListView
from django import forms
from .forms import MemberForm
from .models import Member

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

    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

    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   # フォーム数を上書き

        # 送信ボタンが押された時の挙動
        elif 'btn_submit' in request.POST:
            self.success_url = reverse_lazy('list')
            FORM_VALUES = {}
        
        return super().post(request, args, kwargs)
    
    # form_validをオーバーライドしてデータの登録処理を実装する
    def form_valid(self, form):
        # 送信ボタンが押されたとき
        if 'btn_submit' in self.request.POST:
            # バリデーション済みのデータを取得
            data = form.cleaned_data

            # フォームが複数あるので、一つずつループする
            for member_parameter in data:
                # フォームがからの可能性があるので、空ではないデータ飲み登録
                if member_parameter:
                    # フォームのデータでMemberモデルのオブジェクトを作成
                    member = Member(**member_parameter)
                    # データを登録
                    member.save()
        return super().form_valid(form)

# 一覧ページ用のビュー
class MemberListView(ListView):
    model = Member

あとは、一覧ページに編集ボタンや削除ボタンを追加すれば、データの修正・削除も実装できますよ。

データの編集・削除については以下の記事をご参照ください。

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

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

ちなみに、Djangoに必要なHTML/CSS、JavaScriptなどのWeb開発系言語はという学習サイトで無料で学習できるのでおすすめですよ!

\無料プランを無期限で試す/

メールアドレスだけで10秒で登録!

もっと本格的にDjangoを学びたいという方は、以下の書籍もおすすめですよ。

また、以下の記事ではDjangoが学べるスクールを4つ厳選したのでぜひ参考にしてください。

>>【2023年最新】Djangoが学べるスクール4選を見る

(参考)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をコピーしました!

コメント

コメントする

CAPTCHA


目次