DjangoのFormって便利ですよね?
FormSetを使うことで、複数のフォームを表示させることもできます。
ただ、フォームの数を動的に増やそうとすると、少し工夫が必要です。
今回は、JavaScriptなどは一切使用せず、Djangoだけでフォームの追加ボタンを実装する方法を紹介します。
Djangoの実装を追いかけながら実装したので、効率的で最小限の実装で実現できていると思っています。
また、以下の記事ではDjangoが学べるおすすめのプログラミングスクールをDjangoを学ぶ目的別に紹介しています。
Djangoの習得には幅広い知識が必要で、独学では大変な部分も多いと思いますので、気になる方はよければ覗いてみてください。

実現したいこと
冒頭でも少し触れましたが、今回実現したいことをまとめます。
まず、djangoのFormSet機能を使って複数のフォームを作成します。
そして、ユーザーが、フォームの数が足りない時に、「追加する」ボタンを押すことで、フォームの数を増やすことができるページを作成したいです。
さらに、いくつかのフォームを入力した状態でも、その情報を残したままフォームの追加を行えるようにします。
また、追加ボタンとは別に「送信する」ボタンを押すと、フォームの内容を送信することができます。
今回は、送信機能の部分は割愛して、フォームの追加機能だけを解説します。
最終的なページ表示例を載せておきますので、自分のやりたいことと一致しているか確認して下さい。

全体の手順まとめ
最初に、フォームの追加ボタン実現のための手順をまとめておきます。
手順が多いですが、ひとつずつ説明するので安心してください。
- forms.py、urls.py、テンプレートの作成
- views.pyの作成
- get_form_kwargsメソッドのオーバーライド
- 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のクラス構成は以下のようになっています。

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つ増えたフォームが作成される、という仕組みでした。
コメント