知識の枝

"All is well"

Django 動的に変化するフォーム

約102日前 2021年7月18日9:47
デジタル
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
<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>
このスクリプトの役割は2つあります。

①入力フォームに入力された値を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.py
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)
GETリクエストが来たときの処理を抜粋しています。

先ほど作成したフローチャートに沿った処理が行われています。

views.pyからhtmlへ2種類の変数「new_form1」と「new_form2」が渡されています。



続いて②についてです。
(>views.pyの中で生成したフォームを受け取り、htmlに追加する。)

views.pyからデータを受け取ったあとのスクリプトの処理が下記です。
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>' }));
});
新しいフォームが作られる場所が「#new_form1」と「#new_form2」のタグの部分です。
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":
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)
htmlから受け取ったデータを元に「Material」モデルや「Stock」モデルをデータベースに登録しています。

登録する際は「Model.objects.create()」メソッドを使い、各フィールド引数に受け取ったフォームデータを割り当てています。

登録処理の流れについてもフローチャートに記載してある為、その通りにコードを書きます。



登録作業が終わった後、htmlに帰ってスクリプト内でページのリロードを行っています。
done(response => {
location.reload(true);
});
今回はこんな書き方になっていますが、views.pyの中でredirect処理を書いて元のページを読み直してもOKだと思います。
(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>
内容自体は「入力された食材名の処理」と同じなので解説は省きました。

以上です。お疲れ様でした。