【Javascript】自動計算ツールの作り方(動的に出力される入力フォームに対応)

プログラミング

 単価×数量の計算や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()を使用できます。

まとめ

 この基本形ができれば、「バナナとリンゴは同時に買えない」「パイナップルを購入するとリンゴが半額になる」といった、行をまたがる制御がやり易くなります。

 このような複雑な制御となりがちなビジネスロジックと、基本的な集計ロジックを一緒にしてしまうと、プログラムが煩雑で見づらいものになります。
シンプルに考えられる単位に分割すると、プログラムもシンプルで見やすいものになると思います。

参考になればうれしいです。

コメント

タイトルとURLをコピーしました