今回は、前回…
の続きになります。
今回の課題のゴールは?
今回は、一覧画面を作表しますが、前回の明細の追加処理を応用したものを実装します。
入力画面ではないので、簡単そうに見えますが、さて…
一覧画面とは?
一覧画面とは、Ruby on Railsのscaffoldのリストのようなものです。
ただ今回は、一覧画面のみを作成し、scaffoldのような行削除=レコード削除や、登録・変更画面への遷移は実装しません。
ページングも実装しないことにします。
プログラム例
それでは早速、一覧画面のHTMLから作成していきましょう。
プログラム例
HTMLファイルは2つ作成します(一覧と明細)。
●test_ichiran.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_LINE = 20; async function setIchiran(val) { // 一覧テーブルをクリア rmvIchiran(); // TABLEタグの値をセット // API実行 result = await fetch("api_mock/get_ichiran" + val + ".json", { method: "GET" }); // JSONを取り出す jsonData = await result.json(); //console.log(jsonData.result); // テーブルの枠を作成 // MAX_LINEを超えない範囲で作成 let cnt = (jsonData.line_count < MAX_LINE) ? jsonData.line_count : MAX_LINE; addIchiranArea(cnt, jsonData.unique_key); // 入力値をセットする setInputValue(jsonData.result, FRM_NAME); } // function // 明細画面へ function goMeisai(objThis) { //console.log(objThis.dataset.uniqueKey); let pgm = "test_meisai.html?cs=" + objThis.dataset.uniqueKey; //console.log(pgm); location.href = pgm; } // DOMコンテンツのロード完了時に実行 window.addEventListener('DOMContentLoaded', (ex) => { // APIを使用した初期表示 setIchiran(1) }); </script> </head> <body> <template id="TEMPLATE"> <tr id="line_no_" data-unique-key="" data-line-no="" onclick="goMeisai(this)"> <td id="cust_code_"></td> <td id="cust_name_"></td> <td id="cust_tantousha_"></td> </tr> </template> <form name="frm" id="FORM_AREA"> <button type="button" onclick="setIchiran(1);">一覧1</button> <button type="button" onclick="setIchiran(2);">一覧2</button> <input type="text" name="detail_id" value="line_no_,cust_code_,cust_name_,cust_tantousha_"> </form> <br/ > <table> <thead> <tr> <th>コード</th> <th>取引先名</th> <th>担当者</th> </tr> </thead> <tbody id="ICHIRAN_AREA"> </tbody> </table> </body> </html>
●test_meisai.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_LINE = 20; async function setMeisai() { // GETパラメータを取得 let prm = new URL(window.location.href).searchParams; // 変換 let prm_uk = cnvDefault(prm.get('uk')); // API実行 result = await fetch("api_mock/get_meisai" + prm_uk + ".json", { method: "GET" }); // JSONを取り出す jsonData = await result.json(); //console.log(jsonData.result); // 入力値をセットする setInputValue(jsonData.result, FRM_NAME); } // function // 一覧画面へ戻る function goIchiran() { location.href = "test_ichiran.html"; } // DOMコンテンツのロード完了時に実行 window.addEventListener('DOMContentLoaded', (ex) => { // APIを使用した初期表示 setMeisai() }); </script> </head> <body> <form name="frm" id="FORM_AREA"> <button type="button" onclick="goIchiran();">一覧へ戻る</button> <br/ > <br/ > コード:<span id="cust_code"></span><br/ > 取引先名:<span id="cust_name"></span><br/ > 住所:<span id="cust_addr"></span><br/ > 担当者:<span id="cust_tantousha"></span><br/ > </form> <br/ > </body> </html>
js
ディレクトリを作成し、以下のJavascriptソース(vanilla-front-end.js)を作成します。
●vanilla-front-end.js
// 明細行追加 function addIchiranArea(cnt, uniqueKey) { //console.log(cnt); //console.log(uniqueKey); let dtl_cnt = 0; Array.from({ length: cnt }).forEach(function(strID) { //Array(cnt).fill().forEach(function(strID) { //[...Array(cnt)].forEach(function(strID) { dtl_cnt++; //console.log(dtl_cnt); // テンプレートを取得し、クローンを作成 let tmp = document.getElementById('TEMPLATE'); let cln = tmp.content.cloneNode(true); // クローンをコピペ document.getElementById('ICHIRAN_AREA').appendChild(cln); // datasetに行番号を挿入 document.getElementById('line_no_').dataset.lineNo = dtl_cnt; // datasetにユニークキーを挿入 document.getElementById('line_no_').dataset.uniqueKey = uniqueKey[dtl_cnt - 1]; // idを変更 chgCloneID(document.forms[FRM_NAME].detail_id.value, dtl_cnt); }); } // function // 明細行の一括削除 function rmvIchiran() { // 中身をすべて削除 document.getElementById('ICHIRAN_AREA').innerHTML = ''; /* // あるいはこちらの方法で行う let parent = document.getElementById('ICHIRAN_AREA'); // 子要素を全て削除 while (parent.firstChild) { parent.removeChild(parent.firstChild); } */ } // function
api_mockディレクトリにJSONファイルを3つを用意します。
●get_ichiran1.json
{ "line_count" : 3, "unique_key" : [ "C0001","C0002","C0003" ], "result" : [ {"name" : "cust_code_1", "value" : "C0001"}, {"name" : "cust_name_1", "value" : "株式会社ああああああ"}, {"name" : "cust_tantousha_1", "value" : "東京太郎"}, {"name" : "cust_code_2", "value" : "C0002"}, {"name" : "cust_name_2", "value" : "株式会社いいいいいい"}, {"name" : "cust_tantousha_2", "value" : "大阪次郎"}, {"name" : "cust_code_3", "value" : "C0003"}, {"name" : "cust_name_3", "value" : "株式会社うううううう"}, {"name" : "cust_tantousha_3", "value" : "名古屋三郎"} ] }
●get_ichiran2.json
{ "line_count" : 4, "unique_key" : [ "C0004","C0005","C0006","C0007" ], "result" : [ {"name" : "cust_code_1", "value" : "C0004"}, {"name" : "cust_name_1", "value" : "株式会社ええええええ"}, {"name" : "cust_tantousha_1", "value" : "アメリカ太郎"}, {"name" : "cust_code_2", "value" : "C0005"}, {"name" : "cust_name_2", "value" : "株式会社おおおおおお"}, {"name" : "cust_tantousha_2", "value" : "イギリス次郎"}, {"name" : "cust_code_3", "value" : "C0006"}, {"name" : "cust_name_3", "value" : "株式会社かかかかかか"}, {"name" : "cust_tantousha_3", "value" : "フランス三郎"}, {"name" : "cust_code_4", "value" : "C0007"}, {"name" : "cust_name_4", "value" : "株式会社きききききき"}, {"name" : "cust_tantousha_4", "value" : "ドイツ四郎"} ] }
●get_meisaiC0001.json
{ "result" : [ {"name" : "cust_code", "value" : "C0001"}, {"name" : "cust_name", "value" : "株式会社ああああああ"}, {"name" : "cust_addr", "value" : "東京都新宿区四谷番外地"}, {"name" : "cust_tantousha", "value" : "東京太郎"} ] }
実行方法
http://localhost/~(ユーザー名)/test_ichiran.html
初期表示はget_ichiran1.json
の値が一覧表示されているはずです。
「一覧1」を押下した場合は、get_ichiran1.json
の値が一覧表示されます。
「一覧2」を押下した場合は、get_ichiran2.json
の値が一覧表示されます。
C0001の行をクリックすると、明細画面(test_meisai.html
)へ遷移します。
プログラムの説明
一覧画面は、前回の明細行追加を応用して実装していますが、前回の明細行追加と異なる点としては、
- 枠を作ってデータを流し込むという手法自体は同じ
- 1行追加というニーズがないので、function内でループするようにする
- 入力項目がないので、nameはサポートせず、idのみのセットとする
(detail_idにtemplateにあるtdのidをセット) - TRタグ内にユニークキーを忍ばせておき、これを明細に遷移する際のGETパラメータとして活用する
最大行の、入力画面との扱いの違い
入力画面では、最大表示行は、予め表示する行という意味合いだったので、明細行より多くの空行を設定することが出来ました。
一覧画面はそうではなく、あくまでも表示行を超える空行は表示してはいけません。
また、明細行が最大行を超えた場合は、最大行を優先します。
明細の一括クリア
// 明細行の一括削除 function rmvIchiran() { // 中身をすべて削除 document.getElementById('ICHIRAN_AREA').innerHTML = ''; /* // あるいはこちらの方法で行う let parent = document.getElementById('ICHIRAN_AREA'); // 子要素を全て削除 while (parent.firstChild) { parent.removeChild(parent.firstChild); } */ } // function
ボタンを押下するたび、一度、明細行をすべてクリアし、再度明細行を描画しています。
rmvIchiran()で有効となっているロジックは、子要素すべてをinnerHTMLを消すといった、ざっくりとしたロジックになっています。
とてもシンプルなロジックですが、他のブログ等を見ると、多数のエレメントが入っている場合は、時間がかかるとのことです。
状況によっては、コメントとなっている行に入れ替えてください。
手間がかかっている処理のようですが、こちらのほうが多少は速いとのことです。
明細画面表示用にユニークキーを忍ばせておく
明細に画面遷移する際に使用するユニークキー(プログラム例では取引先コード)は、JSON内の、unique_keyにまとまっています。
"unique_key" : [ "C0001","C0002","C0003" ],
HTMLでは、
<tr id="line_no_" data-unique-key="" data-line-no="" onclick="goMeisai(this)">
にて、器(detaset)を用意し、
// datasetにユニークキーを挿入 document.getElementById('line_no_').dataset.uniqueKey = uniqueKey[dtl_cnt - 1];
で、JSON値(unique_key)からセットします。
TRタグ内をクリックした場合、
// 明細画面へ function goMeisai(objThis) { let pgm = "test_meisai.html?uk=" + objThis.dataset.uniqueKey; location.href = pgm; } (中略 HTML展開後) <tr id="line_no_1" data-unique-key="C0001" data-line-no="1" onclick="goMeisai(this)">
TRタグ内のdata-unique-keyの中身をパラメータcsにセットし、明細画面への遷移を行います。
この記事の一覧画面をアレンジする
この記事の一覧画面はなんだか物足りない気がします。
下記が足りませんね。
- 行削除がない(scaffoldみたいなヤツ?)
- ページ制御がない
- 明細件数も変更できない
- 条件の絞り込みがない
- ソートがない
- 一覧画面に戻った場合、各種条件がリセットされる
これらの処理を付加する際の処理手順や注意点を洗い出していきます。
行削除を実装するには?(画面遷移は不要)
今回の記事では未実装ですが、Ruby on Railsのscaffoldの一覧のように、一覧から、レコード削除したいこともあるかと思います。
もし、実装する場合は、以下のようにすると良いでしょう。
- (各行に設置した)「行削除」の旨のボタンを押下した場合、Confirmダイアログを表示する
- 「キャンセル」の場合は何もしない
- 行削除(上記例の場合は顧客レコードの削除)のAPIを実行する
- 削除完了後、再度、一覧画面APIを呼び出す
- JSONをもとに一覧を再作成する
行をJavascriptで削除しても良いかと思いますが、常に最新のデータを表示させたいのであれば、削除用のAPIで、削除後の一覧をJSON化して渡してもらうようにすれば良いと思います。
(一覧の表示ロジックは初期表示用のAPIと共通化しておいたほうがよいでしょう(バックエンド側)。)
ページ制御を考える場合は?(画面遷移は不要)
今回はページングも未実装ですが、行う場合は…
(ページごとにAPIを実行する場合。)
- フロントエンド側は、APIに以下のパラメータを渡す
- 1ページの表示可能行
- 今、何ページ目か
- API側は、ページ数に対応するデータと、実際の最大レコード数を返す(あるいは、最大(=最終)ページ数を渡す)
- フロントエンド側は、一覧をクリアし、再描画する
- フロントエンド側は、取得した最大レコード数or 最大(=最終)ページ数でページ欄を表示する
といった工夫が必要かと思います。
条件の絞り込みを行いたい場合は?(画面遷移は不要)
今回の記事では未実装ですが、絞り込みを行いたいというニーズはあると思います。
- フロントエンド側は、APIに以下のパラメータを渡す
- 絞り込み条件
- ページング情報(前項参照)
- API側は、対応するデータと、実際の最大レコード数を返す(あるいは、最大(=最終)ページ数を渡す)
- フロントエンド側は、一覧をクリアし、再描画する
- フロントエンド側は、取得した最大レコード数or 最大(=最終)ページ数でページ欄を表示する
ソート順を変更したい場合は?(画面遷移は不要)
処理詳細は省略しますが、同様にAPIを起動し、表を再描画することで解決できそうです。
その場合の注意点として…
- ソート順や検索条件、1ページの表示件数が変更された場合は、1ページ目からにリセットする
といった、仕様上のキメは必要ですね。
一覧画面に戻った場合、各種条件がリセットされる(画面遷移あり)
プログラム例では、test_ichiran.html ⇒ test_meisai.htmlのみ、画面遷移がありますよね。
画面遷移があった場合は、GETパラメータで受け渡ししないと、ページングもソートも検索条件もすべて消えてしまいます。
この場合は、
- 一覧⇒明細の際に、明細に一覧の条件をGETパラメータで渡す
- 一覧に戻る際もGETパラメータで一覧画面に渡す
とすることにより、少なくとも、一覧の条件はGETパラメータで取得できることになります。
ただし…そこまで頑張るかどうかですね。
というのも、検索条件だけではなく、昇順・降順や、ページの明細件数、現在のページ数といった項目も、受け渡しする必要があるからです。
さらに、それらを画面に反映する必要がありますし、パラメータに対するXSS攻撃対策を施す必要があります。
パラメータのXSS対策については、下記の記事をご参照ください。
明細画面をなんとか画面遷移しないで済ませられないか?
GETパラメータのXSS攻撃まで考えるなんて、ちょっと大変ですよね。
なんとか回避できないでしょうか?
それには、2つの手法があると思います。
- 一覧に各行にアコーディオン機能を付け、ボタン押下時に、補足行として明細を表示できるようにする
- モーダル画面を用意し、行押下時にモーダル表示をONにし、モーダルに明細を描画する
どちらも割と見かける方法だと思います。 PCで表示する分にはどちらでも良いかと思いますが、モバイルで表示する際は、どちらを採用してもレイアウトがシビアになります。
ただ、画面遷移でパラメータに四苦八苦するくらいならば、このふたつの手法で乗り切ったほうが、メンテナンスが楽かと思います。
一覧画面用のデータ取得はすべて一つのAPIで行う
一覧画面には、ソート順やページング、明細行数の変更、これらを別々のAPIを呼んで…なんて考えていると、とんでもない数のAPIを作らなければいけませんよね。
ではなく、以下の3つのAPIで処理しましょう。
そのうち、一覧画面表示用のAPIについて、アレンジ部分のパラメータや画面遷移を、再度、整理してみます。
タイミング | 画面遷移 | APIに渡すパラメータ※1の内、左欄に関わる箇所 |
---|---|---|
初期表示 | − | (いわゆるデフォルト値でAPI起動) |
絞り込み検索欄の変更 | − | 絞りこみ項目の数のパラメータが必要 ※2 |
1ページの表示可能件数を変更 | − | 1ページの可能表示件数 |
次ページへ、前ページへ、 あるいはページ番号を押下 |
− | 表示したいページ番号 |
ソート順番の変更 | − | ソート項目 昇順・降順の区分 |
明細画面から画面遷移ありで戻った場合 | ○※3 ※4 | (すべてを受け渡しする必要がある) |
明細を アコーディオンやモーダルで代用 |
− | − |
※1 原則的にすべてのパラメータを渡したほうが良い
※2 項目によってはSQLインジェクション対策が必要(API側でプレースホルダを利用するなど)
※3 明細画面に上記の状態をすべてGETで渡し、一覧画面に戻ってきた際にもそのGETパラメータを取得することが前提
※4 項目によってはエンコード・デコード(XSS攻撃対策)が必要
といったところでしょうか。
SQLインジェクションについては、API側の対策ですので、こちらでは割愛します。
最後に
細かい箇所については、ひととおり解説したと思います。
次回は、共通箇所をどうするか?です。
これが、Vanilla(素のJavascriptでの開発)の一番の苦手箇所かと思います。
それでは、また。