単価×数量の計算やExcelのSUM関数みたいな列合計を、HTMLの入力フォーム上でリアルタイム反映させる方法です。tableをアプリケーションが動的に出力していて、行数一定でない場合の集計にも対応できます。
検索してみると様々な方法が紹介されていたのですが、直感的にわかりやすいものを見つけることができなかったので、記事にしてみました。
- 集計列の自動計算、総合計の自動計算をする入力フォームを作成できる。
完成形
以下が完成形です。詳細はのちほど解説します。
<html>
<head>
<script src="total.js" defer></script>
</head>
<body onload="reCalc();">
<table>
<thead>
<tr>
<th>商品名</th>
<th>単価</th>
<th>数量</th>
<th>合計</th>
</tr>
</thead>
<tbody>
<tr>
<th>リンゴ</th>
<td>
<input type="number" id="tanka_001" name="tanka_001" readonly value="100">円
</td>
<td>
<input type="number" id="suryo_001" name="suryo_001" value="3" onchange="reCalc();">個
</td>
<td>
<input type="number" id="yokokei_001" name="yokokei_001" readonly value="0">円
</td>
</tr>
<tr>
<th>バナナ</th>
<td>
<input type="number" id="tanka_002" name="tanka_002" readonly value="200">円
</td>
<td>
<input type="number" id="suryo_002" name="suryo_002" value="2" onchange="reCalc();">個
</td>
<td>
<input type="number" id="yokokei_002" name="yokokei_002" readonly value="0">円
</td>
</tr>
<tr>
<th>パイナップル</th>
<td>
<input type="number" id="tanka_003" name="tanka_003" readonly value="300">円
</td>
<td>
<input type="number" id="suryo_003" name="suryo_003" value="1" onchange="reCalc();">個
</td>
<td>
<input type="number" id="yokokei_003" name="yokokei_003" readonly value="0">円
</td>
</tr>
</tbody>
<tfoot>
<tr>
<th colspan="3">総合計</th>
<td>
<input type="number" id="total" name="total" readonly value="0">円
</td>
</tr>
</tfoot>
</table>
</body>
</html>
// 指定したエレメント(input)が所属する行(tr)を取得
function gyo(obj)
{
return obj.parentElement.parentElement ;
}
// 指定したエレメント(input)と同じ行にある単価を取得
function tanka(obj)
{
return gyo(obj).querySelectorAll("input[id^=tanka]")[0].value ;
}
// 指定したエレメント(input)と同じ行にある数量を取得
function suryo(obj)
{
return gyo(obj).querySelectorAll("input[id^=suryo]")[0].value ;
}
// 指定したエレメント(input)の横計(単価×数量)を再計算してから取得
function yokokei(obj)
{
result = Number(tanka(obj)) * Number(suryo(obj));
gyo(obj).querySelectorAll("input[id^=yokokei]")[0].value = result ;
return result ;
}
// 総合計を再計算
function tatekei()
{
prices = Array.from(document.querySelectorAll("input[id^=yokokei]")).map(element => Number(yokokei(element))) ;
result = prices.reduce(function(sum,element){
return sum + element ;
});
return result;
}
// 再計算
function reCalc()
{
document.getElementById("total").value = tatekei();
return;
}
HTML
オンロード時と数量の変更時に全体を再計算させるよう、再計算用の関数(reCalc())を呼び出すようにしています。
HTMLファイルでのポイントは、以下のように「同じ列のid属性もしくはname属性の接頭辞は揃えておく」ということです。列名に名前を付けておくようなイメージです。
集計対象となる単価や計算結果を反映する横計の場所を、Javascriptで特定できるようにするためです。
なお、今回紹介する例ではid属性・name属性を「tanka_001」のように、接頭辞+連番の形にしていますが、連番にする必要はありません。
- 同じ列のid属性(またはname属性)は接頭辞をそろえておく
- 同じ行のid属性(またはname属性)は連番などで揃えておく必要はない
連番にしておくと、例えば「suryo_001」(数量)のエレメントから「yokokei_001」(横計)のエレメントを特定する場合、数量の連番部分を使って特定するといった使い方ができます。
しかし、連番を切り出す処理が必要になるため、とても煩雑です。
後述しますが、同じ行の別の列を見つけたいのであれば、「同じ行を見つけること」と、「別の列を見つけること」を分けた方がシンプルになります。
Javascript
gyo()
// 指定したエレメント(input)が所属する行(tr)を取得
function gyo(obj)
{
return obj.parentElement.parentElement ;
}
parentElementを使って集計対象となる行全体(trエレメント)を取得しています。引数となるオブジェクトは、inputエレメントが必ず指定されるように呼び出す側が制御します。
tanka()/suryo()/yokokei()
// 指定したエレメント(input)と同じ行にある単価を取得
function tanka(obj)
{
return gyo(obj).querySelectorAll("input[id^=tanka]")[0].value ;
}
// 指定したエレメント(input)と同じ行にある数量を取得
function suryo(obj)
{
return gyo(obj).querySelectorAll("input[id^=suryo]")[0].value ;
}
// 指定したエレメント(input)の横計(単価×数量)を再計算してから取得
function yokokei(obj)
{
result = Number(tanka(obj)) * Number(suryo(obj));
gyo(obj).querySelectorAll("input[id^=yokokei]")[0].value = result ;
return result ;
}
前述のgyo()とquerySelectorAllを使って、数量・単価・横計列(inputエレメント)を取得するようにしています。
yokokei()では、単価×数量の再計算を合わせて行っています。
tatekei()/reCalc()
再計算後の横計列を合計しています。
// 総合計を再計算
function tatekei()
{
prices = Array.from(document.querySelectorAll("input[id^=yokokei]")).map(element => Number(yokokei(element))) ;
result = prices.reduce(function(sum,element){
return sum + element ;
});
return result;
}
// 再計算
function reCalc()
{
document.getElementById("total").value = tatekei();
return;
}
「document.querySelectorAll(“input[id^=yokokei]”)」ですべての横計列を取得し、map()によって再計算結果をprices配列に格納します。その後、reduce()によって総合計を算出します。
querySelectorAll はNodelistを返却するため、そのままではmap()を使うことができません。Array.from()で配列化することで、map()を使用できます。
まとめ
この基本形ができれば、「バナナとリンゴは同時に買えない」「パイナップルを購入するとリンゴが半額になる」といった、行をまたがる制御がやり易くなります。
このような複雑な制御となりがちなビジネスロジックと、基本的な集計ロジックを一緒にしてしまうと、プログラムが煩雑で見づらいものになります。
シンプルに考えられる単位に分割すると、プログラムもシンプルで見やすいものになると思います。
参考になればうれしいです。
コメント