Django 動的に変化するフォーム
約343日前
2021年7月18日9:47
デジタル
Django JavaScript
2021/7/18 投稿
Djangoを使った開発中に覚えたことを備忘録として残します。
今回は動的に変化するフォームの実装方法を解説します。
入力した内容によって、それ以降のフォームの種類が変化する機能を実装する。
まず始めにイメージ共有からです。
動的に変化するフォームです。
![]()
入力した内容によって、それ以降の内容が変化しています。
上記は食材の在庫を管理するアプリの機能の一部で、在庫登録の作業をキャプチャしました。
登録したい食材の名前を入力すると、以下の流れで以降の内容が変化します。
■名前を入力
⇒ 材料のデータベースにアクセスし、過去に登録したことあるか確認。
■過去に登録したことがある場合
⇒ 「食品」か「調味料」かによって追加量の入力方法が変化する。
■過去に登録したことが無い場合
⇒ 「カテゴリ」を選択し、その内容によって登録する内容が変化する。
「食材」の場合、「追加量(数字)」と「単位」を入力。
「調味料」の場合、「状態(ドロップダウン)」を選択。
最後に「登録」ボタンを押すとデータベースに在庫が登録されます。
このように動的に変化するフォームは Ajax によって実装されています。
Ajaxについてはこちらで解説しています。
DjangoでAjaxを使い非同期通信を行う
それでは解説に移ります。
今回のような「入力内容によって動的に変化する機能」を作る際、処理の分岐を事前に考えておくとコードを書くときに楽になります。
実際私も最初は思いついたままにコードを書いていました。
が、エラー修正をしている中で頭の中がこんがらがってしまい、結局全て書き直しました。
書き直す際にまず始めにやったのが分岐処理の整理です。
手書きでもなんでも良いのですが、流れを視覚的に確認可能なフローチャートを作ってみましょう。
![]()
あとはフローチャートに沿ってコードを書くだけで、すんなり出来上がります。
※専門家じゃないのでフローチャートの書き方は解説できません。涙
目的は「処理の整理」ですので、書き方は自由で良いと思います。
フローチャートは書けました。
じゃあ次は実際にフォームの値を受け取って、次に表示するフォームを作成し、htmlテンプレートに返してみましょう。
まずフォームの入力内容をviews.pyに渡す処理です。
<input>欄に文字が入力されたら、「onchange」で関数「input_name()」を実行します。
input_name関数の引数には「this.id」を、つまり「id="add_stock_name"」を渡します。
同じhtml内に下記スクリプトを書きます。
①入力フォームに入力された値をviews.pyに渡す。
②views.pyの中で生成したフォームを受け取り、htmlに追加する。
まず①についてです。
input_name関数の引数には「"add_stock_name"」が入っていますので、このスクリプトの中では「form_id = "add_stock_name"」となります。
空の配列「input_name」を定義した後、この空の配列の中にフォームに入力した値を追加します。
続いてjson形式に加工した上記の配列をGETリクエストでviews.pyに投げます。
(正確にはURLディスパッチャurls.pyを経由してviews.pyの該当するビューに渡されます。)
先ほど作成したフローチャートに沿った処理が行われています。
views.pyからhtmlへ2種類の変数「new_form1」と「new_form2」が渡されています。
続いて②についてです。
(>views.pyの中で生成したフォームを受け取り、htmlに追加する。)
views.pyからデータを受け取ったあとのスクリプトの処理が下記です。
<div id="new_form_1">の中には「new_form1」のデータを。
<div id="new_form_2">の中には「new_form2」のデータを入れ、最後に「登録ボタン」を設置しています。
登録ボタンは「onclick」でPOST処理が実行されるようになっています。
(次項で説明します。)
ここまでが「動的にフォームの内容が変化する」のメイン的な内容でした。
それでは続けて、フォームに入力されたデータのPOST処理に移りたいと思います。
「登録ボタン」が押された後の動作を解説します。
add_stock関数も先ほどと同様にhtmlの<body>タグ内に<script>として設置してあります。
重要なのは中段の「function add_stock() ~」です。
「stock_info」という空の配列を作り、その中にフォームのデータを次々入れていきます。
基本的には「getElementById()」で目的の<input>タグを選択し、その値を取得しています。
動的に変化するフォームですので「存在しないid」も場合によってはあります。
そのような不確定な<input>タグについては「もしそのidを持つタグがあれば」というif文で対応しています。
必要なデータを全て取得したら、views.pyにデータをPOSTリクエストで投げます。
ここからはviews.pyでの登録処理です。
登録する際は「Model.objects.create()」メソッドを使い、各フィールド引数に受け取ったフォームデータを割り当てています。
登録処理の流れについてもフローチャートに記載してある為、その通りにコードを書きます。
登録作業が終わった後、htmlに帰ってスクリプト内でページのリロードを行っています。
(JsonResponseでhtmlに戻らないということ)
ここまででフローチャートの工程が全て完了しました。
動的に変化するフォームについて少しでも理解が深まれば幸いです。
本文中には記載していませんが、実際にはもう1つ下記のような<script>を入れています。
カテゴリー選択に関するAjax処理です。
以上です。お疲れ様でした。
デジタル
Django JavaScript
改訂履歴
2021/7/18 投稿
1. 背景
Djangoを使った開発中に覚えたことを備忘録として残します。
今回は動的に変化するフォームの実装方法を解説します。
2. ゴール
入力した内容によって、それ以降のフォームの種類が変化する機能を実装する。
3. はじめに
まず始めにイメージ共有からです。
動的に変化するフォームです。
入力した内容によって、それ以降の内容が変化しています。
上記は食材の在庫を管理するアプリの機能の一部で、在庫登録の作業をキャプチャしました。
登録したい食材の名前を入力すると、以下の流れで以降の内容が変化します。
■名前を入力
⇒ 材料のデータベースにアクセスし、過去に登録したことあるか確認。
■過去に登録したことがある場合
⇒ 「食品」か「調味料」かによって追加量の入力方法が変化する。
■過去に登録したことが無い場合
⇒ 「カテゴリ」を選択し、その内容によって登録する内容が変化する。
「食材」の場合、「追加量(数字)」と「単位」を入力。
「調味料」の場合、「状態(ドロップダウン)」を選択。
最後に「登録」ボタンを押すとデータベースに在庫が登録されます。
このように動的に変化するフォームは Ajax によって実装されています。
Ajaxについてはこちらで解説しています。
DjangoでAjaxを使い非同期通信を行う
それでは解説に移ります。
4. 動的変化するフォーム
4.1 - 分岐処理の事前整理
今回のような「入力内容によって動的に変化する機能」を作る際、処理の分岐を事前に考えておくとコードを書くときに楽になります。
実際私も最初は思いついたままにコードを書いていました。
が、エラー修正をしている中で頭の中がこんがらがってしまい、結局全て書き直しました。
書き直す際にまず始めにやったのが分岐処理の整理です。
手書きでもなんでも良いのですが、流れを視覚的に確認可能なフローチャートを作ってみましょう。
あとはフローチャートに沿ってコードを書くだけで、すんなり出来上がります。
※専門家じゃないのでフローチャートの書き方は解説できません。涙
目的は「処理の整理」ですので、書き方は自由で良いと思います。
4.2 - 新しいフォームの生成
フローチャートは書けました。
じゃあ次は実際にフォームの値を受け取って、次に表示するフォームを作成し、htmlテンプレートに返してみましょう。
まずフォームの入力内容をviews.pyに渡す処理です。
html最初はシンプルな構成です。
<form id="ajax-add-stocks" action="{% url 'cookme:add_stock' storage_id%}" method="POST">
<div>
<label>名前:
<input id="add_stock_name" onchange="input_name(this.id)"></input>
</label>
</div>
<div id="new_form_1">
</div>
<div id="new_form_2">
</div>
{% csrf_token %}
</form>
<input>欄に文字が入力されたら、「onchange」で関数「input_name()」を実行します。
input_name関数の引数には「this.id」を、つまり「id="add_stock_name"」を渡します。
同じhtml内に下記スクリプトを書きます。
htmlこのスクリプトの役割は2つあります。
<script>
function input_name(form_id) {
var input_name = [];
input_name.push(document.getElementById(form_id).value);
$.ajax({
'url': '{% url "cookme:add_stock" storage_id %}',
'type': 'GET',
'data': {
'name': JSON.stringify(input_name),
},
'dataType': 'json'
}).done(response => {
$('#new_form_1').empty();
$('#new_form_2').empty();
for (const form of response.new_form1) {
const p = $('<p>', { html: form });
$('#new_form_1').append(p);
}
for (const form of response.new_form2) {
const p = $('<p>', { html: form });
$('#new_form_2').append(p);
}
$('#new_form_2').append($('<p>', { html: '<button type="button" class="btn btn-green" onclick="add_stock()">登録</button>' }));
});
};
</script>
①入力フォームに入力された値をviews.pyに渡す。
②views.pyの中で生成したフォームを受け取り、htmlに追加する。
まず①についてです。
input_name関数の引数には「"add_stock_name"」が入っていますので、このスクリプトの中では「form_id = "add_stock_name"」となります。
空の配列「input_name」を定義した後、この空の配列の中にフォームに入力した値を追加します。
input_name.push(document.getElementById(form_id).value);getElementById("add_stock_name").value となるので、先ほど入力した<input>タグのvalue(=入力した文字)を取得しています。
続いてjson形式に加工した上記の配列をGETリクエストでviews.pyに投げます。
(正確にはURLディスパッチャurls.pyを経由してviews.pyの該当するビューに渡されます。)
'url': '{% url "cookme:add_stock" storage_id %}',
urls.py
path('<uuid:storage_id>/stock/add/', views.AddStock, name='add_stock'),
views.pyGETリクエストが来たときの処理を抜粋しています。
def AddStock(request, storage_id):
"""
Ajax処理
在庫を追加する際の処理
"""
if request.method == "GET":
# 入力データ(食材名)の受け取り
try:
received_name = eval(request.GET.get('name'))
except:
received_name = "dummy"
# 入力データ(カテゴリー)の受け取り
try:
received_category = eval(request.GET.get('category'))
except:
received_category = "nothing"
# 共通フォームの作成
# "賞味期限"の入力欄
exp_date = '<label>賞味期限:<input id="add_stock_exp_date"></input></label>'
# 入力データ(食材名)と同じMaterialが登録されているか?
if received_name[0] in [
str(i) for i in Material.objects.filter(
Q(associated__url_uuid=storage_id) | Q(associated=None))
]:
# >> YES
# そのMaterialのカテゴリーは"調味料"か?
if Material.objects.get(
name=received_name[0]).category.name == "調味料":
# >> YES
# "調味料量"の入力欄を作る
unit_choice = [i for i in Stock.status_choices]
amount_form = '<label>状態:<select name="unit" id="add_stock_amount">'
for unit_name in unit_choice:
amount_form += "<option value=" + unit_name[
1] + ">" + unit_name[1] + "</option>"
amount_form += '</select></label>'
new_form1 = [
amount_form,
exp_date,
]
new_form2 = []
else: # >> NO
# "食材量"の入力欄を作る
amount_form = '<label>追加量:</label><div style="display:inline-flex"><input id="add_stock_amount"></input>'
unit = '<span id="add_stock_unit">' + Material.objects.get(
name=received_name[0]).unit.name + "</span>"
amount_form += unit
new_form1 = [
amount_form,
exp_date,
]
new_form2 = []
else: # >> NO Materialに登録されていない食材名の場合
# カテゴリー選択欄を作る
category_choise = [str(i) for i in Category.objects.all()]
category_form = '<label>カテゴリ:<select name="category" id="add_stock_category" onchange="choice_category(this.value)">'
for category_name in category_choise:
category_form += "<option value=" + category_name + ">" + category_name + "</option>"
category_form += '</select></label>'
# 見栄え上 "食材量", "単位選択肢"欄を作る
unit_choice = [str(i) for i in Unit.objects.all()]
amount_form = '<label>追加量:</label><div style="display:inline-flex"><input id="add_stock_amount"></input>'
unit_form = '<label><select name="unit" id="add_stock_unit_select">'
for unit_name in unit_choice:
unit_form += "<option value=" + unit_name + ">" + unit_name + "</option>"
unit_form += '</select></label>'
amount_form += unit_form
new_form1 = [
category_form,
]
new_form2 = [
amount_form,
exp_date,
]
# 入力データ(カテゴリー)は"調味料"か?
if received_category != "nothing":
if received_category[0] == "調味料":
# >> YES
# "調味料量"の入力欄を作る
unit_choice = [i for i in Stock.status_choices]
amount_form = '<label>状態:<select name="unit" id="add_stock_amount">'
for unit_name in unit_choice:
amount_form += "<option value=" + unit_name[
1] + ">" + unit_name[1] + "</option>"
amount_form += '</select></label>'
new_form2 = [
amount_form,
exp_date,
]
else: # >> NO
# "食材量", "単位選択肢"欄を作る
unit_choice = [str(i) for i in Unit.objects.all()]
amount_form = '<label>追加量:</label><div style="display:inline-flex"><input id="add_stock_amount"></input>'
unit_form = '<label><select name="unit" id="add_stock_unit_select">'
for unit_name in unit_choice:
unit_form += "<option value=" + unit_name + ">" + unit_name + "</option>"
unit_form += '</select></label>'
amount_form += unit_form
new_form2 = [
amount_form,
exp_date,
]
d = {
'new_form1': new_form1,
'new_form2': new_form2,
}
return JsonResponse(d)
先ほど作成したフローチャートに沿った処理が行われています。
views.pyからhtmlへ2種類の変数「new_form1」と「new_form2」が渡されています。
続いて②についてです。
(>views.pyの中で生成したフォームを受け取り、htmlに追加する。)
views.pyからデータを受け取ったあとのスクリプトの処理が下記です。
done(response => {新しいフォームが作られる場所が「#new_form1」と「#new_form2」のタグの部分です。
$('#new_form_1').empty();
$('#new_form_2').empty();
for (const form of response.new_form1) {
const p = $('<p>', { html: form });
$('#new_form_1').append(p);
}
for (const form of response.new_form2) {
const p = $('<p>', { html: form });
$('#new_form_2').append(p);
}
$('#new_form_2').append($('<p>', { html: '<button type="button" class="btn btn-green" onclick="add_stock()">登録</button>' }));
});
html最初はどちらのタグも空っぽです。
<div id="new_form_1">
</div>
<div id="new_form_2">
</div>
<div id="new_form_1">の中には「new_form1」のデータを。
<div id="new_form_2">の中には「new_form2」のデータを入れ、最後に「登録ボタン」を設置しています。
登録ボタンは「onclick」でPOST処理が実行されるようになっています。
(次項で説明します。)
ここまでが「動的にフォームの内容が変化する」のメイン的な内容でした。
それでは続けて、フォームに入力されたデータのPOST処理に移りたいと思います。
4.3 - 登録処理
「登録ボタン」が押された後の動作を解説します。
<button type="button" class="btn btn-green" onclick="add_stock()">登録</button>このボタンをクリックすると「add_stock()」という関数が実行されます。
add_stock関数も先ほどと同様にhtmlの<body>タグ内に<script>として設置してあります。
<script>ここ でも解説したように前半部分は「おまじない」です。
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
var csrftoken = getCookie('csrftoken');
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
function add_stock() {
var stock_info = [];
stock_info.push(document.getElementById("add_stock_name").value);
if (document.getElementById("add_stock_category")) {
stock_info.push(document.getElementById("add_stock_category").value);
}
stock_info.push(document.getElementById("add_stock_exp_date").value);
stock_info.push(document.getElementById("add_stock_amount").value);
if (document.getElementById("add_stock_unit")) {
stock_info.push(document.getElementById("add_stock_unit").textContent);}
if (document.getElementById("add_stock_unit_select")) {
stock_info.push(document.getElementById("add_stock_unit_select").value);}
$.ajax({
'url': '{% url "cookme:add_stock" storage_id %}',
'type': 'POST',
'data': {
'stock_info': JSON.stringify(stock_info),
'csrfmiddlewaretoken': '{{ csrf_token }}',
},
'dataType': 'json'
}).done(response => {
location.reload(true);
});
};
</script>
重要なのは中段の「function add_stock() ~」です。
「stock_info」という空の配列を作り、その中にフォームのデータを次々入れていきます。
基本的には「getElementById()」で目的の<input>タグを選択し、その値を取得しています。
動的に変化するフォームですので「存在しないid」も場合によってはあります。
そのような不確定な<input>タグについては「もしそのidを持つタグがあれば」というif文で対応しています。
if (document.getElementById("add_stock_category")) {}
必要なデータを全て取得したら、views.pyにデータをPOSTリクエストで投げます。
ここからはviews.pyでの登録処理です。
if request.method == "POST":htmlから受け取ったデータを元に「Material」モデルや「Stock」モデルをデータベースに登録しています。
stock_info = request.POST.getlist('stock_info')
stock = [i for i in eval(stock_info[0])]
if stock[0] in [
str(i) for i in Material.objects.filter(
Q(associated__url_uuid=storage_id)
| Q(associated=None))
]:
if Material.objects.get(name=stock[0]).category.name == "調味料":
date_format = "%Y-%m-%d"
if stock[2] == "未開封":
choice_num = 1
elif stock[2] == "多い":
choice_num = 2
elif stock[2] == "少ない":
choice_num = 3
else:
choice_num = 4
Stock.objects.create(
associated=get_object_or_404(Storage, url_uuid=storage_id),
name=get_object_or_404(Material, name=stock[0]),
amount4condiment=choice_num,
exp_date=timezone.datetime.strptime(stock[1], date_format),
stock_uuid="s" + str(uuid.uuid4()).replace("-", ""))
else: #食品 or 冷凍なら
date_format = "%Y-%m-%d"
Stock.objects.create(
associated=get_object_or_404(Storage, url_uuid=storage_id),
name=get_object_or_404(Material, name=stock[0]),
amount4ingredient=int(stock[2]),
exp_date=timezone.datetime.strptime(stock[1], date_format),
stock_uuid="s" + str(uuid.uuid4()).replace("-", ""))
else: # Material登録を行う
if stock[1] != "調味料":
unit_name = Unit.objects.get(name=stock[4])
else:
unit_name = None
material = Material.objects.create(
associated=get_object_or_404(Storage, url_uuid=storage_id),
name=stock[0],
category=get_object_or_404(Category, name=stock[1]),
unit=unit_name,
)
date_format = "%Y-%m-%d"
if stock[1] != "調味料":
Stock.objects.create(
associated=get_object_or_404(Storage, url_uuid=storage_id),
name=material,
amount4ingredient=int(stock[3]),
exp_date=timezone.datetime.strptime(stock[2], date_format),
stock_uuid="s" + str(uuid.uuid4()).replace("-", ""))
else:
if stock[3] == "未開封":
choice_num = 1
elif stock[3] == "多い":
choice_num = 2
elif stock[3] == "少ない":
choice_num = 3
else:
choice_num = 4
Stock.objects.create(
associated=get_object_or_404(Storage, url_uuid=storage_id),
name=material,
amount4condiment=choice_num,
exp_date=timezone.datetime.strptime(stock[2], date_format),
stock_uuid="s" + str(uuid.uuid4()).replace("-", ""))
d = {
'': '',
}
return JsonResponse(d)
登録する際は「Model.objects.create()」メソッドを使い、各フィールド引数に受け取ったフォームデータを割り当てています。
登録処理の流れについてもフローチャートに記載してある為、その通りにコードを書きます。
登録作業が終わった後、htmlに帰ってスクリプト内でページのリロードを行っています。
done(response => {今回はこんな書き方になっていますが、views.pyの中でredirect処理を書いて元のページを読み直してもOKだと思います。
location.reload(true);
});
(JsonResponseでhtmlに戻らないということ)
ここまででフローチャートの工程が全て完了しました。
5. さいごに
動的に変化するフォームについて少しでも理解が深まれば幸いです。
本文中には記載していませんが、実際にはもう1つ下記のような<script>を入れています。
カテゴリー選択に関するAjax処理です。
<script>内容自体は「入力された食材名の処理」と同じなので解説は省きました。
function choice_category(category) {
var select_cat = [];
select_cat.push(category);
$.ajax({
'url': '{% url "cookme:add_stock" storage_id %}',
'type': 'GET',
'data': {
'category': JSON.stringify(select_cat),
},
'dataType': 'json'
}).done(response => {
$('#new_form_2').empty();
for (const form of response.new_form2) {
const p = $('<p>', { html: form });
$('#new_form_2').append(p);
}
$('#new_form_2').append($('<p>', {html:'<button type="button" class="btn btn-green" onclick="add_stock()">登録</button>'}));
});
};
</script>
以上です。お疲れ様でした。