DjangoのFormって便利ですよね?
FormSetを使うことで、複数のフォームを表示させることもできます。
ただ、フォームの数を動的に増やそうとすると、少し工夫が必要です。
今回は、JavaScriptなどは一切使用せず、Djangoだけでフォームの追加ボタンを実装する方法を紹介します。
Djangoの実装を追いかけながら実装したので、効率的で最小限の実装で実現できていますよ。
ちなみに、Djangoに必要なHTML/CSS、JavaScriptなどのWeb開発系言語は侍テラコヤという学習サイトで無料で学習できるのでおすすめですよ!
\無料プランを無期限で試す/
メールアドレスだけで10秒で登録!
実現したいこと
冒頭でも少し触れましたが、今回実現したいことをまとめます。
まず、djangoのFormSet機能を使って複数のフォームを作成します。
そして、ユーザーが、フォームの数が足りない時に、「追加する」ボタンを押すことで、フォームの数を増やすことができるページを作成したいです。
さらに、いくつかのフォームを入力した状態でも、その情報を残したままフォームの追加を行えるようにします。
また、追加ボタンとは別に「送信する」ボタンを押すと、フォームの内容を送信することができます。
今回は、送信機能の部分は割愛して、フォームの追加機能だけを解説します。
最終的なページ表示例を載せておきますので、自分のやりたいことと一致しているか確認して下さい。
全体の手順まとめ
最初に、フォームの追加ボタン実現のための手順をまとめておきます。
手順が多いですが、ひとつずつ説明するので安心してください。
- models.py、forms.py、urls.py、テンプレートの作成
- views.pyの作成
- get_form_kwargsメソッドのオーバーライド
- 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を継承したクラスを定義します。
from django.views.generic.list import ListView
from .models import Member
class MemberListView(ListView):
model = Member
このように、ListViewを使えば数行で一覧ページが実現できます。
クラス変数model
にモデル名を加えるだけでOKです。
送信ボタンの処理の実装
続いて、肝心の送信ボタンの処理を実装していきます。
まずは、post
関数に、送信ボタンが押されたときの処理を追加します。
追加ボタンの時と同様に、ボタン名btn_submit
がrequest.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つ厳選したのでぜひ参考にしてください。
(参考)FormViewのGET時の挙動
最後に、もっと詳しく知りたい人のために、DjangoのFormViewの実装を覗いてget時の挙動を確認したいと思います。
今回、なぜget_form_kwargsメソッドをオーバーライドしたのか?の理由がみえてきます。
実際にDjangoの実装を確認することが、必要最低限の実装だけで済むようにするためのコツです。
クラス構成
Djangoのviewには様々なクラスがあり、それらを多重継承してクラスベースビューが構成されています。
今回使用するFormViewのクラス構成は以下のようになっています。
FormViewはTemplateResponceMixin、BaseFormViewの2つのクラスを継承しています。
さらにBaseFormViewはFormMixin、ProcessFormViewを継承したクラスです。
FormMixin、ProcessFormViewはそれぞれContextMixin、Viewを継承しています。
get時のシーケンス
続いて、GETメソッドが呼び出されたときのFormViewの挙動をシーケンス図を使って説明します。
少し複雑そうに見えるので、手順をまとめます。
- 最初にgetメソッドを呼び出す(ProcessFormViewで実装)
- get_context_dataの戻り値を引数として、render_to_reponceメソッドを呼び出す(render_to_responceはTemplateResponce、get_context_dataはFormMixinで実装)
- get_formメソッドを呼び出す(FormMixinで実装)
- get_form_kwargsメソッドの戻り値を引数として、form_classのインスタンスを生成する
- 生成した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つ増えたフォームが作成される、という仕組みでした。
コメント