知識の枝

"All is well"

Django&Ajax チェックボックスを扱う

約111日前 2021年7月9日23:22
デジタル
Django Python JavaScript

改訂履歴


2021/7/9 投稿

1. 背景


Djangoを使った開発中に覚えたことを備忘録として残します。

今回はAjaxを使ってチェックボックスのチェック有無で処理を分岐する方法を解説します。


2. ゴール


チェックした項目だけ一覧に追加する。


3. はじめに


DjangoでAjaxを使うと非同期通信を行うことができます。

非同期通信とはフロントとサーバーのやりとり時にページの読み込み直しをせず、ページの内容を更新可能な方法です。

今回扱うのはチェックボックスです。

チェックボックスのチェック有無を判断し、チェックされた内容を取得。
その後、htmlとviews.pyの間で非同期通信を行います。

こんなイメージです。




上記は料理を作るアプリの一部分です。

追加食材を選択し、追加量を入力する処理が行われています。


それではやり方を解説していきます。



4. Ajaxとチェックボックス


4.1 - 概要


上記例ではモーダル画面の中に食材の一覧表が表示されています。

一覧表の一番左の列には追加の有無を選択するチェックボックスが配置されています。

html上ではこのように書かれています。
html
<form id="ajax-select-materials" action="{% url 'app:add_material2recipe' storage_id menu_id %}" method="POST">
<table class="table table-striped table-hover">
<tr>
<th class="width5">+</th>
<th class="width35">品名</th>
<th class="width30">在庫</th>
<th class="width30">追加量</th>
</tr>
{% for stock_material, form in stocks|zip:formset %}
<tr>
<td><input type="checkbox" name="chk" value="{{stock_material}}" /></td>
<td>{{stock_material.name}}</td>
<td>{{stock_material.amount4ingredient}} {{stock_material.name.unit}}</td>
<td>{% render_field form.amount4ingredient class="form-control" id=stock_material.name.name %}</td>
</tr>
{% endfor %}
</table>
<div class="modal-footer">
<button type="button" data-bs-dismiss="modal" class="btn btn-secondary">
キャンセル
</button>
<button type="button" data-bs-dismiss="modal" class="btn btn-success" onclick="select_materials()">
OK
</button>
</div>
{% csrf_token %}
</form>
大雑把に構成を説明すると、<form>タグの中に<table>が配置されており、テーブルの中に「チェックボックス」や「名前」、「在庫量」、「入力フォーム」が表示されるようになっています。

CSSなどの細かい部分の解説は飛ばします。
入力フォームの書き方については こちらで解説しています。
入力フォームの見栄え向上


重要となるポイントは下記2行です。

1つ目はテーブルのチェックボックスの部分
<td><input type="checkbox" name="chk" value="{{stock_material}}" /></td>
2つ目は送信ボタン
<button type="button" data-bs-dismiss="modal" class="btn btn-success" onclick="select_materials()">



送信ボタンから見ていきましょう。

送信ボタンには「onclick="select_materials()"」というオプションがついています。

このオプションは「クリック動作が入った時に "select_materials()" というスクリプトを実行する」という指示になっています。


スクリプトの中身は下記の通りです。
<script>~</script>を<body>タグの任意の場所に配置します。
html
<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 select_materials() {
var selected = [];
$('input:checked').each(function () {
selected.push($(this).val());
selected.push(document.getElementById($(this).val()).value);
selected.push(document.getElementById("id_" + $(this).val()).value);
});
$.ajax({
'url': '{% url "cookme:add_material2recipe" storage_id menu_id %}',
'type': 'POST',
'data': {
'selected_materials': JSON.stringify(selected)
},
'dataType': 'json'
}).done(response => {
$('#id_added_name').empty();

for (const added_name of response.checked_material) {
const p = $('<p>', { html: '<span style="color:red;">' + added_name + '</span>' });
$('#id_added_name').append(p);
}
});
};
</script>
大きく分けて3つのブロックが存在します。




前半の約半分は所謂おまじないです。
POSTリクエストの場合は必ず書く必要があります。
csrfに関する記述です。

真ん中のブロックは先ほどの「onclick="select_materials()"」で実行される部分です。

送信ボタンを押すと実行されます。


最後のブロックは views.py で処理した結果を受け取って、html上に反映させる処理です。


4.2 - データ収集と送信


まずブロック2を詳しく見ます。
script
function select_materials() {
var selected = [];
$('input:checked').each(function () {
selected.push($(this).val());
selected.push(document.getElementById($(this).val()).value);
});
$.ajax({
'url': '{% url "cookme:add_material2recipe" storage_id menu_id %}',
'type': 'POST',
'data': {
'selected_materials': JSON.stringify(selected)
},
'dataType': 'json'
})
チェックされている項目とその項目に入力されている値を取得したいので、値を取得して views.py に送る準備をします。

まず「var selected = [];」で取得した値を格納する空の配列を作ります。
pythonのリストみたいですね。

次にチェックボックスにチェックが入っている<input>タグを取得します。
$('input:checked')

チェックが入っている各<input>タグについてeach以下の処理がそれぞれの<input>タグごとに実行されます。
each(function () {
selected.push($(this).val());
selected.push(document.getElementById($(this).val()).value);
}
配列 selected に this の value を追加します。
selected.push($(this).val());
ここでの this とは <input> タグそのものを指します。

<input> タグの value、つまり {{stock_material}} が配列に追加されます。
<input type="checkbox" name="chk" value="{{stock_material}}" />

今回の場合、valueの中には食材の名前が入っています。
<input type="checkbox" name="chk" value="キャベツ">

結果、配列selectedの中身は['キャベツ']となります。


続いて2行目の下記一文
selected.push(document.getElementById($(this).val()).value);
ここでは getElementById(●●) によってIDが●●の要素を選択します。

●●の部分は $(this).val() ですので

getElementById("●●")

getElementById("キャベツ") となり、

IDが「キャベツ」の要素を選択します。


さて、IDが「キャベツ」 の要素はどれでしょうか?

それは、追加する量を入力するフォームです。
<td>{% render_field form.amount4ingredient class="form-control" id=stock_material.name.name %}</td>
id=stock_material.name.name の部分が「id="キャベツ"」 になっています。



このフォームの値を getElementById($(this).val()).value として取得し、配列selectedに追加します。

配列の中身は['キャベツ', '3.0']となります。

この処理がチェックされたボックスの数だけ繰り返され、上記の例では最終的に配列が['キャベツ', '3.0', 'にんじん', '2.0']となります。


それではデータの収集が終わりましたので、集めたデータ(=配列selected)をviews.pyに送ります。
$.ajax({
'url': '{% url "cookme:add_material2recipe" storage_id menu_id %}',
'type': 'POST',
'data': {
'selected_materials': JSON.stringify(selected)
},
'dataType': 'json'
})

「url」にはデータを処理するパス(ビュー)を指定します。

今回は「add_material2recipe」というパスを指定しています。
urls.py
path('<uuid:storage_id>/menu/recipe/<uuid:menu_id>/add_material2recipe',
views.Add_material2recipe,
name='add_material2recipe')
このパスはviews.pyの「Add_material2recipe」を参照していますので、そちらを見てみましょう。
views.py
def Add_material2recipe(request, storage_id, menu_id):
"""Ajax処理"""
"""材料を追加する処理"""
"""POSTデータの受け取り&リスト化"""
checked_material = request.POST.get('selected_materials')
materials = eval(checked_material)

"""イテレータ化"""
iter_materials = iter(materials)

"""htmlに返すデータ"""
return_materials = []
for name, amount in zip(iter_materials, iter_materials):
return_materials.append(str(name) + " " + str(float(amount)))
"""チェックを全て外した場合"""
if len(materials) == 0:
return_materials.append("追加の材料無し")

data = {
'checked_material': return_materials,
}

return JsonResponse(data)
中身については後述します。

「type」にはリクエストの種類を書きます。
今回はPOSTとしていますが、GETリクエストで行いたい場合はGETとして下さい。

「data」にはviews.pyに渡したいデータを書きます。
「selected_materials」という変数名で先ほど収集したデータ(selected)をJSON形式で渡します。
 'data': {
'selected_materials': JSON.stringify(selected)
},


「dataType」にはやり取りするデータの形式を指定します。
今回は「JSON」形式でやり取りを行います。


4.3 - views側の処理


さきほど出てきたviews.pyを上から順番に見ていきましょう。

まず最初にPOSTされたデータを受け取ります。
views.py
"""POSTデータの受け取り&リスト化"""
checked_material = request.POST.get('selected_materials')
materials = eval(checked_material)
「request.POST.get('selected_materials')」でデータを受け取ります。

受け取ったデータは ["キャベツ","3.0","にんじん","2.0"] となり、一見そのままリストとして使えそうです。

ですが実は上のデータはリストでは無く、str型なんです。
print(type(checked_material))
>> <class 'str'>
このままでは扱いにくいのでリスト型に直します。

ここで使用するのがeval関数です。

セキュリティ上、あまりよろしくないとされる関数ですが便利なので使います。

eval関数を使用すると、関数の引数として渡された文字列を「Pythonのコード」として扱ってくれます。

今回の場合、文字列データ ["キャベツ","3.0","にんじん","2.0"] はPythonのリストとして認識され、リスト型になります。
print(type(eval(checked_material)))
>> <class 'list'>


views.pyの続きを見ます。
views.py
"""イテレータ化"""
iter_materials = iter(materials)
ここでは先ほどのリストをイテレータ化しています。

イテレータ化する理由は次の処理でzip関数を使う為です。
views.py
"""htmlに返すデータ"""
return_materials = []
for name, amount in zip(iter_materials, iter_materials):
return_materials.append(str(name) + " " + str(float(amount)))
"""チェックを全て外した場合"""
if len(materials) == 0:
return_materials.append("追加の材料無し")
リストの中のデータを2つずつ取り出して扱いたい為、zip関数を利用しています。

zip関数の引数として "先ほど作ったイテレータ" を渡します。

2つずつ取り出したデータはそれぞれ、「食材の名前」「使う量」として1つの文字列に変換されます。



また、仮にチェックボックスのチェックを全て外した場合、上記の処理でエラーが発生してしまうのでif分岐で「チェックが無い場合の処理」を追加しています。
if len(materials) == 0:
return_materials.append("追加の材料無し")


最後に、加工したデータを辞書形式にしてから JsonResponse でhtmlにデータを渡します。
data = {
'checked_material': return_materials,
}

return JsonResponse(data)
普段使う context と同じ書き方です。

html上では「checked_material」という変数でリスト return_materials を使うことができます。



4.4 - html側の処理


<script>の続きを見てみます。

views.pyからレスポンスを受け取った後の処理です。
done(response => {
$('#id_added_name').empty();

for (const added_name of response.checked_material) {
const p = $('<p>', { html: '<span style="color:red;">' + added_name + '</span>' });
$('#id_added_name').append(p);
}
});
まずレスポンスを受け取った後、idが「id_added_name」の要素を取得し、中身を空にします。

対象のidが含まれるタグは下記の部分です。
<div id="id_added_name">
<p><span style="color:red;">キャベツ 3.0</span></p>
<p><span style="color:red;">にんじん 2.0</span></p>
</div>




毎回「empty()」を実行しないと、チェックボックスを編集する度に要素がどんどん増えてしまいます。



ここでようやく先ほどviews.pyで作成したデータを使います。
for (const added_name of response.checked_material) {
const p = $('<p>', { html: '<span style="color:red;">' + added_name + '</span>' });
$('#id_added_name').append(p);
}
Javascriptのfor文です。

「added_name」という変数に response の「checked_material」リストから順々に取り出したデータを当てはめてループします。

やっていることは、

①文字列データの前後にhtmlタグを付ける
const p = $('<p>', { html: '<span style="color:red;">' + added_name + '</span>' });
②先ほど空にしたタグに①の要素を追加
$('#id_added_name').append(p);
の2つです。

①の中で style="●●"class="■■" と書けば、しっかりCSSが適用されます。
上記の例では文字色を「赤」にしています。


非同期通信によって 「ページの再読み込みをせず」 ページの内容を更新することができました。

処理の裏側で、htmlとviews.pyの間でちゃんとやり取りされていたことが確認できたと思います。



5. さいごに


中々ボリュームが多くて疲れてしまいましたね。

最後まで頑張って読んで頂けて嬉しいです。

お疲れ様でした。