今回は、前回…
の続きになります。
- 今回の課題のゴールは?
- まず、そもそもJavascriptではエラーチェックしないの?
- エラーチェック(兼更新)APIとの連携について
- CSSについて
- 共通functionやJSONを追加
- エラーメッセージ領域の操作とメッセージ表示
- 項目の枠を目立たせるclassの制御
- (エラーではない場合、)ダイアログを表示し画面遷移
- 参考:API(バックエンド側)は何をしている?
- エラーチェック時に、計算結果を返して、表示させる
- 最後に
今回の課題のゴールは?
エラーチェックのAPIを実行し、エラーがあった場合は、メッセージを表示したり、問題箇所の枠や文字を赤色にします。
正常の場合は、(更新まで行われたとみなし)画面遷移を行います。
まず、そもそもJavascriptではエラーチェックしないの?
APIを作る側はとしては、「念のために」全てのエラーチェックを行うことが多いです。
Javascriptでも全てのチェックを行うから、APIではチェックは大雑把でいいよ!とは行かないと思います。
- その項目の有効値など、こちらの手の内がわかってしまう
- ニセページを作られてしまった場合、エラーチェック無しで更新されてしまう
なので、
- type属性にdateやtimeを設定して、ある程度の入力制限は行う
程度で良いかなと思います。
エラーチェック(兼更新)APIとの連携について
入力画面によくある、「追加」「登録」「変更」「更新」等のボタンを押下した際は、概ねこのような処理が展開されます。
- エラーチェックの前の準備処理を行う
- エラーメッセージ領域のクリア・非表示
- エラーを示す(項目の枠を目立たせる)classのクリア
- エラーチェックAPIを実行
- setInputValue()で計算値をセット
- 戻ってきた値がエラーだった場合
- エラーメッセージを表示エリアにセットし、領域を表示する
- エラーを示す(項目の枠を目立たせる)classを対象の項目にセット
- functionを終了(return)
- エラーではなかった場合(更新まで終了)
- 更新した旨のダイアログを表示(OKを入力するまで待つ)
- 画面遷移(メニューに戻るか?、一覧画面に戻るか?)
難しそうなので、要素ごとに分類します。
- エラーメッセージ領域の操作とメッセージ表示
- 項目の枠を目立たせるclassの制御
- (エラーではない場合、)ダイアログを表示し画面遷移
この3つの要素ごとにプログラム例を作成していきます。
CSSについて
本来ならBootstrap等のCSSフレームワークを使用した例を提示すべきかもしれませんが、余計なDIVタグが多数ついてしまうこともあり、それはやめ、最低限のCSSのみ実装しています。
まあ、デザイン性無視でやっていますので、ダサいかもしれませんが、CSSフレームワークが特有のタグのせいで、本質がわからなくなるよりはマシですね。
共通functionやJSONを追加
今回は共通のfunctionがいくつもありますので、先にJavascriptファイルやJSONファイルを追加しておきます。
Javascriptファイル
js
ディレクトリを作成し、以下のJavascriptソース(vanilla-front-end.js)を作成します。
●vanilla-front-end.js
// メッセージ領域クリア function clrMessageArea() { // メッセージエリアを初期化 let msg = document.getElementById("MESSAGE_AREA"); msg.classList.add("vj-nodisp"); msg.innerHTML = ''; } // function // メッセージ領域セット function setMessageArea(arrMsg) { // メッセージ用のHTMLテキストを整形 let str = '<ul>'; arrMsg.forEach(function (msg) { str += '<li>' + msg + '</li>'; }); // arrMsg.forEach(function (msg) { str += '</ul>'; // メッセージエリアを表示 let msg = document.getElementById("MESSAGE_AREA"); msg.innerHTML = str; msg.classList.remove("vj-nodisp"); } // function // POSTによるAPI実行 async function actErrorCheck(frm_name, api_name) { // エラーチェックAPI実行 let frm = document.querySelector('form[name="' + frm_name + '"]'); let fd = new FormData(frm); let result = await fetch(api_name, { method: "POST", body: fd }); // JSONを取り出す return await result.json(); } // エラー項目を目立たせるためのクラス const CLASS_ERR = 'vj-invalid'; // エラー項目用のクラスを除去 function clrInputError() { // エラー項目用のクラスを取得 let cls = document.querySelectorAll('.' + CLASS_ERR); //console.log(cls); // エラー項目用のクラスを除去 cls.forEach(function (cl) { cl.classList.remove(CLASS_ERR); }); // cls.forEach(function (cl) { } // function // エラー項目用のクラスをセット function setInputError(arrInp) { // エラー項目名でループ arrInp.forEach(function (ip) { // フォームデータを取り直す let elm = document.querySelectorAll('[name="' + ip + '"]'); elm.forEach(function (el) { el.classList.add(CLASS_ERR); }); // elm.forEach(function (el) { }); // arrInp.forEach(function (arr) { } // function
JSONファイル
例のごとくapi_mock
ディレクトリに作成します。
今回は3ファイル作成します。
●check00.json
{ "error" : "0", "message" : [], "error_name" : [], "result" : [], "location" : "https://www.yahoo.co.jp/" }
●check01.json
{ "error" : "1", "message" : [ "日付が正しくありません。", "外出が変です。" ], "error_name" : [ "foo_date1", "foo_date2", "gaishutsu", "memo" ], "result" : [ {"name" : "memo", "value" : "あああ\nいいい\nううう"} ], "location" : "" }
●check02.json
{ "error" : "1", "message" : [ "時間が正しくありません。" ], "error_name" : [ "foo_time", "kintai", "foo2" ], "result" : [], "location" : "" }
エラーメッセージ領域の操作とメッセージ表示
まずは、ボタンを押すたびに、メッセージを表示したり、しなかったりしましょう。
プログラム例
●test_check01.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <script src="js/vanilla-front-end.js"></script> <style> #MESSAGE_AREA { background-color: lightpink; } .vj-nodisp { display:none; } </style> <script> // エラーチェック処理 async function actCheck(val) { // メッセージ領域クリア clrMessageArea(); // エラーチェックAPI実行 let pgmID = 'api_mock/check' + val + '.json'; jsonData = await actErrorCheck('frm', pgmID) // エラーの場合 if (jsonData.error === '1') { // メッセージ領域に値をセット setMessageArea(jsonData.message); } } // function // DOMコンテンツのロード完了時に実行 window.addEventListener('DOMContentLoaded', (ex) => { // メッセージ領域クリア clrMessageArea(); }); </script> </head> <body> <div id="MESSAGE_AREA" class="vj-nodisp"> ここにエラーが表示されます。 </div> <br> <form name="frm" id="FORM_AREA"> 日付:<input name="foo_date" type="date" value=""> <br /> 時刻:<input name="foo_time" type="time" value=""> <br /> <br /> <button type="button" onclick="actCheck('00');">ノーエラー</button> <button type="button" onclick="actCheck('01');">エラー1</button> <button type="button" onclick="actCheck('02');">エラー2</button> </form> </body> </html>
実行
http://localhost/~(ユーザー名)/test_check01.html
「エラー1」を押下すると、check01.json
を読み、日付に関するエラーメッセージが表示されます。
「エラー2」では、check02.json
を読み、時刻に関するエラーメッセージが表示されます。
「エラーなし」の場合は、check00.json
を読み、エラー欄がクリアされます。
actCheck()について
- clrMessageArea()を実行し、メッセージ領域をクリアします
- actErrorCheck()を実行し、エラーチェック結果のJSONを受け取ります
(取得するJSONをボタンによって変更しています) - JSONのerrorが1の場合は、setMessageArea()でエラーメッセージを表示します
clrMessageArea()について
// メッセージ領域クリア function clrMessageArea() { // メッセージエリアを初期化 let msg = document.getElementById("MESSAGE_AREA"); msg.classList.add("vj-nodisp"); msg.innerHTML = ''; } // function
処理の流れは以下の通り。
- メッセージ欄(id: MESSAGE_AREA)のオブジェクトを取得します
- クラス
vj-nodisp
を追加します - メッセージ欄の中身をクリアします
actErrorCheck()について
// POSTによるAPI実行 async function actErrorCheck(frm_name, api_url) { // エラーチェックAPI実行 let frm = document.querySelector('form[name="' + frm_name + '"]'); let fd = new FormData(frm); let result = await fetch(api_url, { method: "POST", body: fd }); // JSONを取り出す return await result.json(); }
この一覧の記事はすべてJSONのモックデータを取得するだけなので、紙芝居レベルではあまり意味はないのですが、実用に耐えられるように、フォームデータをPOSTでAPIに渡すようにしています。
処理は以下のとおり。
- querySelector()にてFORMタグのオブジェクトを取得
- fetch()で使用できる形のオブジェクトに変換
- POST&フォームデータにてfetch()を実行
- レスポンスからJSONを取り出して戻す
setMessageArea()について
// メッセージ領域セット function setMessageArea(arrMsg) { // メッセージ用のHTMLテキストを整形 let str = '<ul>'; arrMsg.forEach(function (msg) { str += '<li>' + msg + '</li>'; }); // arrMsg.forEach(function (msg) { str += '</ul>'; // メッセージエリアを表示 let msg = document.getElementById("MESSAGE_AREA"); msg.innerHTML = str; msg.classList.remove("vj-nodisp"); } // function
処理の流れは以下の通り。
- メッセージ用のHTMLテキストを整形します
- <ul> + 配列の値を<li>と</li>で囲んだもの + </ul>
- メッセージ欄(id:MESSAGE_AREA)のオブジェクトを取得します
- メッセージ欄の中身にメッセージのHTMLをセットします
- クラスvj-nodispを除去します(=表示されます)
項目の枠を目立たせるclassの制御
プログラム例
●test_check02.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <script src="../js/vanilla-front-end.js"></script> <style> #MESSAGE_AREA { background-color: lightpink; } .vj-nodisp { display: none; } .vj-invalid { border-color:red; } .vj-invalid + label { color:red; } </style> <script> // エラーチェック処理 async function actCheck(val) { // メッセージ領域クリア clrMessageArea(); // エラー項目用のクラスを除去 clrInputError(); // エラーチェックAPI実行 let pgmID = 'api_mock/check' + val + '.json'; jsonData = await actErrorCheck('frm', pgmID) // エラーの場合 if (jsonData.error === '1') { // メッセージ領域に値をセット setMessageArea(jsonData.message); // エラー項目用のクラスをセット setInputError(jsonData.error_name); } } // function // DOMコンテンツのロード完了時に実行 window.addEventListener('DOMContentLoaded', (ex) => { // メッセージ領域クリア clrMessageArea(); }); </script> </head> <body> <div id="MESSAGE_AREA" class="vj-nodisp"> ここにエラーが表示されます。 </div> <br> <form name="frm" id="FORM_AREA"> 日付:<input name="foo_date1" type="date" value="">~ <input name="foo_date2" type="date" value=""> <br /> 時刻:<input name="foo_time" type="time" value=""> <br /> <br /> <button type="button" onclick="actCheck('00');">ノーエラー</button> <button type="button" onclick="actCheck('01');">エラー1</button> <button type="button" onclick="actCheck('02');">エラー2</button> <br /> <br /> <br /> <hr /> 参考:以下は「vj-invalid」の効果。 <br /> <br /> 元号2:<select name="foo2" class="sel_gengo"> <option value="X">ここには追加しません</option> <option value="M">明治</option> <option value="T">大正</option> </select> <br /> 外出:<input type="radio" name="gaishutsu" value="1" id="gaishutsu1"> <label for="gaishutsu1">あり</label> <input type="radio" name="gaishutsu" value="0" checked id="gaishutsu2"> <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> </form> </body> </html>
実行
http://localhost/~(ユーザー名)/test_check02.html
「エラー1」「エラー2」を押下すると、check01.json
またはcheck02.json
を読み、それぞれ別の項目の枠または文字が赤色に変化します。
「エラーなし」の場合は、ノーエラーであることを表すcheck00.json
を読み、赤色がもとに戻ります。
INPUT/TEXTAREA/SELECTおよび、チェックボックス/ラジオボタンの違いについて
エラーでは、枠を赤色で囲む、あるいは(枠のない入力項目は)文字を赤色にするようにします。
.vj-invalid { border-color:red; } .vj-invalid + label { color:red; }
INPUT/TEXTAREA/SELECTといった「枠」がある項目については、エラーの際に枠を赤くするようにします。
CSSの「+ label」は、「枠」のないチェックボックスとラジオボタンで、威力を発揮します。
外出:<input type="radio" name="gaishutsu" value="1" id="gaishutsu1"> <label for="gaishutsu1">あり</label>
ラベルのforとINPUTのidを合わせておくと、CSSの「+ label」で上手く反転してくれるようです。
今回の記事にはあまり関係ない項目も下にありますが、念のために「枠をエラーでは赤色で囲む、あるいは文字を赤色にする」が満足できるか?を確認できるようにしています。<
変更されたactCheck()について
黄色いアンダーラインの付いた箇所が変更箇所です。
- clrMessageArea()を実行し、メッセージ領域をクリアします
- clrInputError()を実行し、エラーになっていた項目のクラスを除去します
- actErrorCheck()を実行し、エラーチェック結果のJSONを受け取ります
(取得するJSONをボタンによって変更しています) - JSONのerrorが1の場合は、
- setMessageArea()で、エラーメッセージを表示します
- setInputError()で、エラーとなった項目に「赤色の枠・文字に変更する」クラスを追加します
clrInputError()について
// エラー項目を目立たせるためのクラス const CLASS_ERR = 'vj-invalid'; // エラー項目用のクラスを除去 function clrInputError() { // エラー項目用のクラスを取得 let cls = document.querySelectorAll('.' + CLASS_ERR); //console.log(cls); // エラー項目用のクラスを除去 cls.forEach(function (cl) { cl.classList.remove(CLASS_ERR); }); // cls.forEach(function (cl) { } // function
処理は以下のとおり。
- querySelectorAll()にて、クラス
vj-invalid
をもつ全てのオブジェクトを取得 - 取得したオブジェクトでループ
- クラス
vj-invalid
を除去
- クラス
setInputError()について
// エラー項目用のクラスをセット function setInputError(arrInp) { // エラー項目名でループ arrInp.forEach(function (ip) { // フォームデータを取り直す let elm = document.querySelectorAll('[name="' + ip + '"]'); elm.forEach(function (el) { el.classList.add(CLASS_ERR); }); // elm.forEach(function (el) { }); // arrInp.forEach(function (arr) { } // function
処理は以下のとおり。
- SONのerror_name(引数)の配列でループ
- querySelectorAll()にて、name属性が一致する全てのオブジェクトを取得
- 取得したオブジェクトでループ
- クラス
vj-invalid
を追加
- クラス
(エラーではない場合、)ダイアログを表示し画面遷移
プログラム例
●test_check03.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <script src="../js/vanilla-front-end.js"></script> <style> #MESSAGE_AREA { background-color: lightpink; } .vj-nodisp { display: none; } .vj-invalid { border-color:red; } .vj-invalid + label { color:red; } </style> <script> // エラーチェック処理 async function actCheck(val) { // メッセージ領域クリア clrMessageArea(); // エラー項目用のクラスを除去 clrInputError(); // エラーチェックAPI実行 let pgmID = 'api_mock/check' + val + '.json'; jsonData = await actErrorCheck('frm', pgmID) // 入力値をセットする setInputValue(jsonData.result, 'frm'); // エラーの場合 if (jsonData.error === '1') { // メッセージ領域に値をセット setMessageArea(jsonData.message); // エラー項目用のクラスをセット setInputError(jsonData.error_name); //終了 return; } // エラーではない場合 alert("更新しました。"); location.href = jsonData.location; } // function // DOMコンテンツのロード完了時に実行 window.addEventListener('DOMContentLoaded', (ex) => { // メッセージ領域クリア clrMessageArea(); }); </script> </head> <body> <div id="MESSAGE_AREA" class="vj-nodisp"> ここにエラーが表示されます。 </div> <br> <form name="frm" id="FORM_AREA"> 日付:<input name="foo_date1" type="date" value="">~ <input name="foo_date2" type="date" value="" > <br /> 時刻:<input name="foo_time" type="time" value=""> <br /> <br /> <button type="button" onclick="actCheck('00');">ノーエラー</button> <button type="button" onclick="actCheck('01');">エラー1</button> <button type="button" onclick="actCheck('02');">エラー2</button> <br /> <br /> <br /> <hr /> 参考:以下は「vj-invalid」の効果。 <br /> <br /> 元号2:<select name="foo2" class="sel_gengo"> <option value="X">ここには追加しません</option> <option value="M">明治</option> <option value="T">大正</option> </select> <br /> 外出:<input type="radio" name="gaishutsu" value="1" id="gaishutsu1"> <label for="gaishutsu1">あり</label> <input type="radio" name="gaishutsu" value="0" checked id="gaishutsu2"> <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> </form> </body> </html>
実行
http://localhost/~(ユーザー名)/test_check03.html
内容はtest_check02.htmlとほぼ同様ですが、「エラーなし」の挙動が追加されています。
- 「更新しました」のダイアログを表示
- 「OK」ボタン押下後に、JSONの”location”にある値のURLに遷移する
変更されたactCheck()について
黄色いアンダーラインの付いた箇所が変更箇所です。
- clrMessageArea()を実行し、メッセージ領域をクリアします
- clrInputError()を実行し、エラーになっていた項目のクラスを除去します
- actErrorCheck()を実行し、エラーチェック結果のJSONを受け取ります
(取得するJSONをボタンによって変更しています) - JSONのerrorが1の場合は、
- setMessageArea()で、エラーメッセージを表示します
- setInputError()で、エラーとなった項目に「赤色の枠・文字に変更する」クラスを追加します
- 処理を終了します
- (以降はerrorが1ではない場合、)
- 「更新しました。」のダイアログを表示します
- JSONの値をもとに画面遷移します
参考:API(バックエンド側)は何をしている?
ところで、API(バックエンド側)では何をしているか気になりませんか?
このような処理が多いと思います。
- 戻り値(JSON)の初期化
- 簡単なエラーチェック(単項目エラーチェック)
- エラーが有る場合はここで処理を打ち切って、JSONを返す
- 複雑なエラーチェック(複合エラーチェック)
- 複雑なエラーチェック(複合エラーチェック)
- (以降はエラーが無い前提の処理)
単項目エラーチェック
単項目エラーチェックとは、その項目自体の有効値チェックです。
- 入力必須
- 数値チェック
- 日付・時刻チェック
- 長さ、(小数点含む)桁数チェック
- 区分チェック、コードチェック
- 値の範囲や、値の制限
- テーブルにコードが存在するか?
といったところでしょうか。
これがクリアできないと、もう少し複雑なエラーチェックをしようとした段階で、システムエラーとなってしまうため、先に行い、エラーがあった場合は処理を打ち切り、JSONを返します。
全てOKになった段階で、次のもう少し複雑なチェックへ進みます。
複合エラーチェック
こちらも、エラーで先に進めなくなった場合は、エラーチェックを打ち切ることになります。
- 開始・終了の大小チェック
- 計算結果の矛盾
- 二重伝票や、明細内の重複チェック
- DB内の他の入力(値)などとの兼ね合いで弾かれるエラー
あたりが、よくあるチェックでしょうか。
エラーの場合は、JSONを返し、終了します。
(ネストの多重化を防ぐため。)
DB更新など
エラーがない場合、晴れて更新処理に移ります。
テーブルの更新(SQLやActiveRecordなど)や、他のシステムのテーブルを更新するため、他システムのAPIをキックします。
終了後はトランザクション処理を行います。
処理によってはSlackに通知したいと思います。
SlackのAPIを実行したり、あるいはIFTTTのWeb hook経由で送信したり…といったところでしょうか。
エラーチェック時に、計算結果を返して、表示させる
test_check03.htmlについては、過去の記事、
を参考に、エラーチェックで求めた計算結果を返してもらって、値をセットするfunctionであるsetInputValue()を実行し、値をセットするロジックが挿入されています。
// 入力値をセットする setInputValue(jsonData.result, 'frm');
check01.jsonでは、TEXTAREAに3行ほど挿入するように設定しています。
「エラー1」ボタンを押下してください。
これを応用すれば…
- 計算値を求めて表示
- 郵便番号を入力することにより、住所の一部を入力項目にセット
といったことも、出来るかな…と思います。
最後に
残された機能は、明細行に対応したり…でしょうか。
また、このチュートリアルの範囲からは外れるかもしれませんが、共通箇所のローディング手法なども、まだ、取り上げてはいませんね。
次回は、確認画面の実装を考えてみます。
それでは、また。