今回は、前回…
の続きになります。
- 今回の課題のゴールは?
- 明細行が必要な入力ってどんな物があるの?
- 明細行の仕様を決める
- 明細行の実装(登録画面編)
- 行が増えていく仕組み
- 行が削除される仕組み
- 参考:API側の注意点
- 明細行の実装(変更画面編)
- 明細でも選択値の制御が生じるのならば
- 行を追加すると同時に値を複写をするには?
- フッター(ヘッダー?)部分に合計欄を設けたい
- 最後に
今回の課題のゴールは?
明細行付きの入力画面の、明細行を追加したり削除したりします。
また、変更画面の初期表示で、DBにある明細行を表示するようにします。
明細行が必要な入力ってどんな物があるの?
例えば、交通費精算でしょうか。
多くの法人では、日ごとに精算することはまれで、その週、あるいは月単位にまとめて精算しますので、日付、行き先、交通手段、金額などの、複数行入力が必須条件になるかと思います。<
また、販売管理でも、1商品・サービスのみを売買することはまれで、1回の取引で、複数の商品・サービスや個数が入力できなくてはいけませんよね。
明細行の仕様を決める
とりあえず、明細入力画面の仕様としては…
- 「行追加」ボタンを押下したら、明細の追加を行う
- 「行削除」ボタンを押下したら、明細の削除を行う
でしょうか。
削除を繰り返しても、行番号がバッティングしないように、最大値チェック用の行カウントと、明細行に付加する行番号は別に持ちます。
明細行の実装(登録画面編)
それでは早速、登録画面のHTMLから作成していきましょう。
プログラム例
●test_detail01.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <script src="js/vanilla-front-end.js"></script> <script> // form名 let FRM_NAME = 'frm'; // 最大明細行数 let MAX_DETAIL_LINE = 5; let INIT_DETAIL_LINE = 1; async function setInitData() { // 既存明細分の行を追加する let cnt = INIT_DETAIL_LINE; Array.from({ length: cnt }).forEach(function(strID) { //Array(cnt).fill().forEach(function(strID) { //[...Array(cnt)].forEach(function(strID) { addDetailLine(); }); } // function // DOMコンテンツのロード完了時に実行 window.addEventListener('DOMContentLoaded', (ex) => { // APIを使用した初期表示 setInitData(); }); </script> </head> <body> <template id="TEMPLATE"> <div id="line_no_" data-line-no=""> #<span id="line_no_disp_"></span> <input type="hidden" name="dummy_" value=""> <br /> 元号2:<select name="foo2_" class="sel_gengo"> <option value="X">選択してください</option> <option value="M">明治</option> <option value="T">大正</option> </select> 日付:<input name="foo_date_" type="date" value=""> <br /> 外出:<input type="radio" name="gaishutsu_" value="1" id="gaishutsu1_"> <label for="gaishutsu1_">あり</label> <input type="radio" name="gaishutsu_" value="0" checked id="gaishutsu2_" checked> <label for="gaishutsu2_">なし</label> <br /> 勤怠:<input type="checkbox" name="kintai_" value="1" id="kintai1_"> <label for="kintai1_">遅刻</label> <input type="checkbox" name="kintai_" value="2" id="kintai2_"> <label for="kintai2_">早退</label> <input type="checkbox" name="kintai_" value="3" id="kintai3_"> <label for="kintai3_">電車遅延</label> <br /> メモ:<textarea name="memo_" rows="5" cols="30"></textarea> <button type="button" id="del_line_no_" data-line-no="" onclick="dltDetailLine(this);">行削除</button> <hr> </div> </template> <form name="frm" id="FORM_AREA"> ここは見出しエリア <hr> <!-- ここから明細エリア --> <span id="DETAIL_AREA"></span> <button type="button" onclick="addDetailLine();">行追加</button> <input type="text" name="detail_count" value="0"> <input type="text" name="max_count" value="0"> <input type="text" name="detail_id" value="line_no_,del_line_no_,line_no_disp_,gaishutsu1_,gaishutsu2_,kintai1_,kintai2_,kintai3_"> <input type="text" name="detail_name" value="dummy_,foo2_,foo_date_,gaishutsu_,kintai_,memo_"> </form> </body> </html>
js
ディレクトリを作成し、以下のJavascriptソース(vanilla-front-end.js)を作成します。
●vanilla-front-end.js
// 明細行追加 function addDetailLine() { // 現在の明細番号を取得 let dtl_cnt = document.forms[FRM_NAME].detail_count.value; let max_cnt = document.forms[FRM_NAME].max_count.value; //console.log(dtl_cnt); // 最大明細数を超えた場合はダイアログを表示し、何もしない if (MAX_DETAIL_LINE != 0 && max_cnt >= MAX_DETAIL_LINE) { alert('明細行の上限' + MAX_DETAIL_LINE + '行を超えています。'); return; } // 行を加算 dtl_cnt++; max_cnt++; document.forms[FRM_NAME].detail_count.value = dtl_cnt; document.forms[FRM_NAME].max_count.value = max_cnt; // テンプレートを取得し、クローンを作成 let tmp = document.getElementById('TEMPLATE'); let cln = tmp.content.cloneNode(true); // クローンをコピペ document.getElementById('DETAIL_AREA').appendChild(cln); // datasetに行番号を挿入 document.getElementById('line_no_').dataset.lineNo = dtl_cnt; document.getElementById('del_line_no_').dataset.lineNo = dtl_cnt; document.getElementById('line_no_disp_').innerText = dtl_cnt; // idを変更 chgCloneID(document.forms[FRM_NAME].detail_id.value, dtl_cnt); // forを変更 chgCloneFor(document.forms[FRM_NAME].detail_id.value, dtl_cnt); // nameを変更 chgCloneName(document.forms[FRM_NAME].detail_name.value, dtl_cnt); } // function // idの書き換え function chgCloneID(lstID, lineNo) { let arrID = lstID.split(','); arrID.forEach(function(strID) { //console.log(strID); //console.log(lineNo); //console.log(document.getElementById(strID)); document.getElementById(strID).id = strID + lineNo; }); } // function // forの書き換え function chgCloneFor(lstID, lineNo) { let arrID = lstID.split(','); arrID.forEach(function(strID) { //console.log(strID); //console.log(lineNo); elm = document.querySelector('label[for="' + strID + '"]'); if (elm) { elm.htmlFor = strID + lineNo; //console.log(elm.htmlFor); } }); } // function // nameの書き換え function chgCloneName(lstName, lineNo) { let arrName = lstName.split(','); arrName.forEach(function(strName) { //console.log(strName); //console.log(lineNo); let elm = document.querySelectorAll('[name="' + strName + '"]'); elm.forEach(function(el) { //console.log(el); el.name = strName + lineNo; }); // elm.forEach(function(el) { }); // arrName.forEach(function(strName) { } // function // 明細行の削除 function dltDetailLine(objThis) { // objThisのdatasetからidを求める let strID = 'line_no_' + objThis.dataset.lineNo; document.getElementById(strID).remove(); // 最大行の減算 let max_cnt = document.forms[FRM_NAME].max_count.value; max_cnt--; document.forms[FRM_NAME].max_count.value = max_cnt; } // function
実行方法
http://localhost/~(ユーザー名)/test_detail01.html
まず、INIT_DETAIL_LINEで指定した行数分の明細が作成されます。
さらに、「行追加」ボタンで行が追加され、「行削除」ボタンでその行が削除されます。
また、行の合計が5件になったら、それ以降はダイアログが表示され、行は追加できなくなります。
(削除されれば、また追加できます。)
行が増えていく仕組み
TEMPLATEに追加したい行を忍ばせておく
まず、明細行の元ネタになる部分を、TEMPLATEタグで囲み、隠しておきます。
<template id="TEMPLATE"> <div id="line_no_" data-line-no=""> #<span id="line_no_disp_"></span> <input type="hidden" name="dummy_" value=""> <br /> 元号2:<select name="foo2_" class="sel_gengo"> <option value="X">選択してください</option> <option value="M">明治</option> <option value="T">大正</option> </select> 日付:<input name="foo_date_" type="date" value=""> <br /> (中略) </div> </template>
TEMPLATE内のidやnameにはアクセスできない
TEMPALTEタグは、ドキュメント対象外となるので、内部のエレメントにアクセスできません。
クローンをコピペする段階でnameやidが認識できるようになります。
ただしnameもidもユニークであることが求められます。
ですので、このようにします。
- TEMPLATEの中のidやnameもユニークな名前にする
- クローンをコピペした段階で、nameやidやforをリネームし、ユニークな値にする
(classはその限りではない) - そのユニークな値に対し、値をセットする
TEMPLATE上では「id="hoge_"」といった値にしておき、クローン後に新しい行番号を採番して(例えば採番した行番号が12ならば)、id="hoge_12"のような形にすればいいと思います。
値を取得するAPIも"hoge_12"となるように設計してください。
addDetailLine()
addDetailLine()
の内、テンプレートからコピペする手順は下記のロジックになります。
// テンプレートを取得し、クローンを作成 let tmp = document.getElementById('TEMPLATE'); let cln = tmp.content.cloneNode(true); (中略) // クローンをコピペ document.getElementById('DETAIL_AREA').appendChild(cln);
ブラウザのF12でElementを選択しておくと、増えていくさまがわかります。
ただ、これだけですと、2行目以降、nameやid、forが重複してしまうので、
// idを変更 chgCloneID(document.forms[FRM_NAME].detail_id.value, dtl_cnt); // forを変更 chgCloneFor(document.forms[FRM_NAME].detail_id.value, dtl_cnt); // nameを変更 chgCloneName(document.forms[FRM_NAME].detail_name.value, dtl_cnt);
で、id/for/nameに行番を付加していきます。
どのid/for/nameに対し行番を付加するのかは、別途INPUTタグ(name="detail_id"および"detail_name")内に定義しておくことで、処理の汎用化を図っています。
(forはdetail_idを流用します。存在しないものは無視するようにしています。)
また、addDetailLine()では、datasetにも値(行番)をセットしていますが、こちらは行削除に利用します。
行が削除される仕組み
<button type="button" id="del_line_no_" data-line-no="" onclick="dltDetailLine(this);">行削除</button>
一般的には、Javascriptの処理内容を変えることは(セキュリティの観点から)ご法度のようなので、dltDetailLine()の引数には、行番号を挿入することはしません(出来ません)。
代わりにthis=自分自身をセットします。
こういった場合では、addDetailLine()にてdata-line-noに行番号値をセットしておき、
function dltDetailLine(objThis) { // objThisのdatasetからidを求める let strID = 'line_no_' + objThis.dataset.lineNo; document.getElementById(strID).remove();
上記のように、パラメータにthis、つまり自分自身(のelements)を取得できるようにし、BUTTONタグ内にあるdata-line-noから行番号を取得して、id="line_no_(行番号)"のタグを、自分自身と子供を含め根こそぎremove()します。
つまり、dataset(data-hoge)はthisだけでは取得できない、引数(パラメータ)の代わりに利用します。
引数的な項目が複数必要な場合は、detasetを適宜、複数用意しましょう。
参考:API側の注意点
行の欠番に注意
ところで、今回の仕様では行削除も考えていますが、そうすると欠番が出ますよね?
行番号を詰めることはバグを誘発するだけのような気がするで、例題ではそのままサーバーサイドに送ることにしています。
JSONに変換してPOSTするといった仕様(キメ)であれば、配列化して行番を消すことは可能かもしれませんが、一般的には画面によっては添付ファイルなどもありますので、まあ、フォームデータで統一でしょうね。
その場合はname属性がAPI側(=サーバーサイドフレームワーク側)のパラメータになります。
ですので、API側(=サーバーサイドフレームワーク側)はname="hoge_n"に欠番があることを念頭において、プログラミングをすることになります。
さらに、理想的には行番号を詰めて(リナンバリングして)DBに落としたいので、API側ではそこも工夫しつつ。
API側で欠番チェックを汎用化するには、どんな明細でも必ずあるname名があれば便利ですが、なければ、ダミー用のhiddenを忍ばせておくと、そういったチェックには便利かもしれませんね。
name="dummy_n"が存在しない場合、false的な結果を返す言語ならやりやすいでしょうが、そうでない(例外が発生してしまう)言語は、nameの値を早めに配列+構造体(あるいは連想配列)化して、詰めてしまいます。
とはいえ、エラー対象のnameをJavascriptで装飾する(多くの場合はINPUTタグの枠を赤色にする)使用の場合は、詰めたとしても連想配列等で(value値の他に)name値も保持する必要はありそうですね。
明細行の実装(変更画面編)
登録はまっさらな空行を表示すれば良いのですが、変更画面では、DB上に既に明細(登録時のデータ)が存在していますよね?
なので、初期処理でデータをAPI経由で取得した上で、表示する必要があります。
そのためには、以前作成した、APIから取得したJSONをもとにnameやidに値をセットするsetInputValue()を今回も使用しますが、その直前に、APIから取得した明細行数をもとに、必要な行数分だけaddDetailLine()してあげれば、上手くいくと思います。
プログラム例
●test_detail02.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <script src="js/vanilla-front-end.js"></script> <script> // form名 let FRM_NAME = 'frm'; // 最大明細行数 let MAX_DETAIL_LINE = 5; let INIT_DETAIL_LINE = 1; async function setInitData() { // INPUTタグの値をセット // API実行 result = await fetch("api_mock/get_detail.json", { method: "GET" }); // JSONを取り出す jsonData = await result.json(); //console.log(jsonData.result); // 既存明細分の行を追加する let cnt = (jsonData.line_count > INIT_DETAIL_LINE) ? jsonData.line_count : INIT_DETAIL_LINE; Array.from({ length: cnt }).forEach(function(strID) { //Array(cnt).fill().forEach(function(strID) { //[...Array(cnt)].forEach(function(strID) { addDetailLine(); }); // 入力値をセットする setInputValue(jsonData.result, FRM_NAME); } // function // DOMコンテンツのロード完了時に実行 window.addEventListener('DOMContentLoaded', (ex) => { // APIを使用した初期表示 setInitData(); }); </script> </head> <body> <template id="TEMPLATE"> <div id="line_no_" data-line-no=""> #<span id="line_no_disp_"></span> <input type="hidden" name="dummy_" value=""> <br /> 元号2:<select name="foo2_" class="sel_gengo"> <option value="X">選択してください</option> <option value="M">明治</option> <option value="T">大正</option> </select> 日付:<input name="foo_date_" type="date" value=""> <br /> 外出:<input type="radio" name="gaishutsu_" value="1" id="gaishutsu1_"> <label for="gaishutsu1_">あり</label> <input type="radio" name="gaishutsu_" value="0" checked id="gaishutsu2_" checked> <label for="gaishutsu2_">なし</label> <br /> 勤怠:<input type="checkbox" name="kintai_" value="1" id="kintai1_"> <label for="kintai1_">遅刻</label> <input type="checkbox" name="kintai_" value="2" id="kintai2_"> <label for="kintai2_">早退</label> <input type="checkbox" name="kintai_" value="3" id="kintai3_"> <label for="kintai3_">電車遅延</label> <br /> メモ:<textarea name="memo_" rows="5" cols="30"></textarea> <button type="button" id="del_line_no_" data-line-no="" onclick="dltDetailLine(this);">行削除</button> <hr> </div> </template> <form name="frm" id="FORM_AREA"> ここは見出しエリア <hr> <!-- ここから明細エリア --> <span id="DETAIL_AREA"></span> <button type="button" onclick="addDetailLine();">行追加</button> <input type="text" name="detail_count" value="0"> <input type="text" name="max_count" value="0"> <input type="text" name="detail_id" value="line_no_,del_line_no_,line_no_disp_,gaishutsu1_,gaishutsu2_,kintai1_,kintai2_,kintai3_"> <input type="text" name="detail_name" value="dummy_,foo2_,foo_date_,gaishutsu_,kintai_,memo_"> </form> </body> </html>
api_mockディレクトリにget_detail.json
を用意します。
●get_detail.json
{ "line_count" : 2, "result" : [ {"name" : "foo2_1", "value" : "M"}, {"name" : "foo_date_1", "value" : "2023-07-01"}, {"name" : "gaishutsu_1", "value" : "1"}, {"name" : "kintai_1", "value" : "1,3"}, {"name" : "memo_1", "value" : "あああ\nいいい"}, {"name" : "foo2_2", "value" : "T"}, {"name" : "foo_date_2", "value" : "2023-07-10"}, {"name" : "gaishutsu_2", "value" : "0"}, {"name" : "kintai_2", "value" : "2"}, {"name" : "memo_2", "value" : "abcdefg"} ] }
実行方法
http://localhost/~(ユーザー名)/test_detail02.html
きちんと表示されているかを確認してみてください。
また、(JSONで取得した・しない関わらず)行の追加や削除ができることも確認してみてください。
ボタン付近にあるINPUTタグは、内容が確認できたら、hidden化しておきましょう。
変更箇所
APIを読み込む機能を実装しています。
さらに、
let cnt = (jsonData.line_count > INIT_DETAIL_LINE) ? jsonData.line_count : INIT_DETAIL_LINE;
にて、初期表示行(INIT_DETAIL_LINE)と、明細取得行(jsonData.line_count)のどちらかを採用するようにしています。
空の明細行追加の仕組み
前項で決定したのcnt
回数分、addDetailLine()を繰り返し、空の明細を作成します。
その後、setInputValue()を実行すると、明細行に値が入ります。
addDetailLine()を件数分実行するループについては…
Array.from({ length: cnt }).forEach(function(strID) { //Array(cnt).fill().forEach(function(strID) { //[...Array(cnt)].forEach(function(strID) {
といった具合に、いくつもの手法があるようですが、ご使用のブラウザで動作するものを、ご使用していただければと思います。
明細でも選択値の制御が生じるのならば
もし、明細項目の中で、選択値の制御が生じるのならば、こちらの記事、
を参考に、制御用functionの共通化を図ってください。
タイミング的にはこの3箇所になるかと思います。
- 初期値
- 行追加時
- 各明細のonclick/onchangeのタイミング
class="is-style-big_icon_caution">異なる明細に悪影響がないように作成するのがコツです。
明細の表示制御は難易度が高いですが、私の経験では、共通化はできると思います。
頑張ってみてください!
行を追加すると同時に値を複写をするには?
今回は例示として考えませんでしたが、
コピーと同時に、どこか指定した行から値を複写するといったボタンが欲しい!
とか、言われそうではありますよね。
行追加とは別に複写ボタンを追加
これを実現するには、addDetailLine()実行後に、行の複写のロジックを実装すればよいのですが…
それには2つの方法がありそうです。
- Javascriptでゴリゴリ複写
- (.valueが使用できない)radio/checkboxのコピペに難ありそう…
- POST(フォームデータ)とコピー元行番号、コピー先行番号をAPIに投げて、setInputValue()で使用できるデータに加工してもらう
おすすめは2.でしょうか。
汎用的に作れば、一つのAPIで事足りそうですね。
複写元の行をどう表現するか?
参考までに、複写元の行番号をどうやって求めるか?
次のいづれかの方法があると思います。
- 複写ボタンの横に、何行目を選択あるいは入力できるようにする
- SELECT/OPTIONだと、行が追加、削除されるために変更する煩わしさがありそう
- 行に欠番があると何行目と実際の行番号がわかりにくく、バグにもなりそう
- 各行の先頭にラジオボタン等を用意し、選択した行が複写されるようにする
- 行を追加しても、このラジオボタンのnameのリネームはしないようにする必要がある
フッター(ヘッダー?)部分に合計欄を設けたい
こちらもJavascriptでゴリゴリ作成派とAPI派に分かれると思います。
どちらにしても、実際はエラーチェックのAPIは実装すると思いますので、そちらで計算した後に、setInputValue()で値をセットしてあげれば良い思います。
最後に
残されたネタ(?)は、(今回のロジックを応用した)一覧画面の実装と、共通部分の組み込みでしょうか。
次回はその中の、一覧画面にチャレンジします。
それでは、また。