inuinu blog(開発用)

BOT @wagagun の開発ノウハウや、IT向け?の雑記ブログです。

素のJavascript(Vanilla)だけでフロントエンド開発(07:チェック&更新API編)

このページでは、ReactやVueではなく、Vanillaと呼ばれる素のJavascriptのみで、フロントエンド開発が可能であるか?を解説します。今回はエラーチェックのAPIを実装します。

今回は、前回…

inuinu-tech.hatenablog.jp

の続きになります。

いきなりここにたどり着いたのでしたら、お手すきの際で構いませんので、下記の記事からも読んでみていただければ幸いです。

inuinu-tech.hatenablog.jp

今回の課題のゴールは?

エラーチェックのAPIを実行し、エラーがあった場合は、メッセージを表示したり、問題箇所の枠や文字を赤色にします。

正常の場合は、(更新まで行われたとみなし)画面遷移を行います。

まず、そもそもJavascriptではエラーチェックしないの?

APIを作る側はとしては、「念のために」全てのエラーチェックを行うことが多いです。
Javascriptでも全てのチェックを行うから、APIではチェックは大雑把でいいよ!とは行かないと思います。

  • その項目の有効値など、こちらの手の内がわかってしまう
  • ニセページを作られてしまった場合、エラーチェック無しで更新されてしまう

なので、

  • type属性にdateやtimeを設定して、ある程度の入力制限は行う

程度で良いかなと思います。

エラーチェック(兼更新)APIとの連携について

入力画面によくある、「追加」「登録」「変更」「更新」等のボタンを押下した際は、概ねこのような処理が展開されます。

  1. エラーチェックの前の準備処理を行う
    • エラーメッセージ領域のクリア・非表示
    • エラーを示す(項目の枠を目立たせる)classのクリア
  2. エラーチェックAPIを実行
  3. setInputValue()で計算値をセット
  4. 戻ってきた値がエラーだった場合
    • エラーメッセージを表示エリアにセットし、領域を表示する
    • エラーを示す(項目の枠を目立たせる)classを対象の項目にセット
    • functionを終了(return)
  5. エラーではなかった場合(更新まで終了)
    • 更新した旨のダイアログを表示(OKを入力するまで待つ)
    • 画面遷移(メニューに戻るか?、一覧画面に戻るか?)

難しそうなので、要素ごとに分類します。

  • エラーメッセージ領域の操作とメッセージ表示
  • 項目の枠を目立たせるclassの制御
  • (エラーではない場合、)ダイアログを表示し画面遷移

この3つの要素ごとにプログラム例を作成していきます。

setInputValue()については、この記事の最後の方に、ヒントが書いてあります。

CSSについて

本来ならBootstrap等のCSSフレームワークを使用した例を提示すべきかもしれませんが、余計なDIVタグが多数ついてしまうこともあり、それはやめ、最低限のCSSのみ実装しています。

まあ、デザイン性無視でやっていますので、ダサいかもしれませんが、CSSフレームワークが特有のタグのせいで、本質がわからなくなるよりはマシですね。

共通functionやJSONを追加

今回は共通のfunctionがいくつもありますので、先にJavascriptファイルやJSONファイルを追加しておきます。

Javascriptファイル

jsディレクトリを作成し、以下のJavascriptソース(vanilla-front-end.js)を作成します。

他の演習を先に行い、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()について

  1. clrMessageArea()を実行し、メッセージ領域をクリアします
  2. actErrorCheck()を実行し、エラーチェック結果のJSONを受け取ります
    (取得するJSONをボタンによって変更しています)
  3. JSONのerrorが1の場合は、setMessageArea()でエラーメッセージを表示します

clrMessageArea()について

// メッセージ領域クリア
function clrMessageArea() {
  // メッセージエリアを初期化
  let msg = document.getElementById("MESSAGE_AREA");
  msg.classList.add("vj-nodisp");
  msg.innerHTML = '';
} // function

処理の流れは以下の通り。

  1. メッセージ欄(id: MESSAGE_AREA)のオブジェクトを取得します
  2. クラスvj-nodispを追加します
  3. メッセージ欄の中身をクリアします

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に渡すようにしています

処理は以下のとおり。

  1. querySelector()にてFORMタグのオブジェクトを取得
  2. fetch()で使用できる形のオブジェクトに変換
  3. POST&フォームデータにてfetch()を実行
  4. レスポンスから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

処理の流れは以下の通り。

  1. メッセージ用のHTMLテキストを整形します
    • <ul> + 配列の値を<li>と</li>で囲んだもの + </ul>
  2. メッセージ欄(id:MESSAGE_AREA)のオブジェクトを取得します
  3. メッセージ欄の中身にメッセージのHTMLをセットします
  4. クラス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()について

黄色いアンダーラインの付いた箇所が変更箇所です。

  1. clrMessageArea()を実行し、メッセージ領域をクリアします
  2. clrInputError()を実行し、エラーになっていた項目のクラスを除去します
  3. actErrorCheck()を実行し、エラーチェック結果のJSONを受け取ります
    (取得するJSONをボタンによって変更しています)
  4. 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

処理は以下のとおり。

  1. querySelectorAll()にて、クラスvj-invalidをもつ全てのオブジェクトを取得
  2. 取得したオブジェクトでループ
    • クラス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

処理は以下のとおり。

  1. 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とほぼ同様ですが、「エラーなし」の挙動が追加されています。

  1. 「更新しました」のダイアログを表示
  2. 「OK」ボタン押下後に、JSONの”location”にある値のURLに遷移する

遷移先の変更は、check00.jsonを変更することで可能です。

変更されたactCheck()について

黄色いアンダーラインの付いた箇所が変更箇所です。

  1. clrMessageArea()を実行し、メッセージ領域をクリアします
  2. clrInputError()を実行し、エラーになっていた項目のクラスを除去します
  3. actErrorCheck()を実行し、エラーチェック結果のJSONを受け取ります
    (取得するJSONをボタンによって変更しています)
  4. JSONのerrorが1の場合は、
    • setMessageArea()で、エラーメッセージを表示します
    • setInputError()で、エラーとなった項目に「赤色の枠・文字に変更する」クラスを追加します
    • 処理を終了します
  5. (以降はerrorが1ではない場合、)
    • 「更新しました。」のダイアログを表示します
    • JSONの値をもとに画面遷移します

参考:API(バックエンド側)は何をしている?

ところで、API(バックエンド側)では何をしているか気になりませんか?

このような処理が多いと思います。

  1. 戻り値(JSON)の初期化
  2. 簡単なエラーチェック(単項目エラーチェック)
    • エラーが有る場合はここで処理を打ち切って、JSONを返す
  3. 複雑なエラーチェック(複合エラーチェック)
    • 複雑なエラーチェック(複合エラーチェック)
  4. (以降はエラーが無い前提の処理)
    • DB更新
    • 他のシステム、あるいは他社サービスAPI等の実行
    • 正常終了を表すJSONを返し終了

リファクタリング手法でよく使用される早期リターンを多用し、ネストを浅くするようにします。

単項目エラーチェック

単項目エラーチェックとは、その項目自体の有効値チェックです。

  • 入力必須
  • 数値チェック
  • 日付・時刻チェック
  • 長さ、(小数点含む)桁数チェック
  • 区分チェック、コードチェック
  • 値の範囲や、値の制限
  • テーブルにコードが存在するか?

といったところでしょうか。
これがクリアできないと、もう少し複雑なエラーチェックをしようとした段階で、システムエラーとなってしまうため、先に行い、エラーがあった場合は処理を打ち切り、JSONを返します。
全てOKになった段階で、次のもう少し複雑なチェックへ進みます。

複合エラーチェック

こちらも、エラーで先に進めなくなった場合は、エラーチェックを打ち切ることになります。

  • 開始・終了の大小チェック
  • 計算結果の矛盾
  • 二重伝票や、明細内の重複チェック
  • DB内の他の入力(値)などとの兼ね合いで弾かれるエラー

あたりが、よくあるチェックでしょうか。

エラーの場合は、JSONを返し、終了します。
(ネストの多重化を防ぐため。)

DB更新など

エラーがない場合、晴れて更新処理に移ります。

テーブルの更新(SQLActiveRecordなど)や、他のシステムのテーブルを更新するため、他システムのAPIをキックします。
終了後はトランザクション処理を行います。

処理によってはSlackに通知したいと思います。
SlackのAPIを実行したり、あるいはIFTTTのWeb hook経由で送信したり…といったところでしょうか。

エラーチェック時に、計算結果を返して、表示させる

test_check03.htmlについては、過去の記事、

inuinu-tech.hatenablog.jp

を参考に、エラーチェックで求めた計算結果を返してもらって、値をセットするfunctionであるsetInputValue()を実行し、値をセットするロジックが挿入されています。

  // 入力値をセットする
  setInputValue(jsonData.result, 'frm');

check01.jsonでは、TEXTAREAに3行ほど挿入するように設定しています。
「エラー1」ボタンを押下してください。

これを応用すれば…

  • 計算値を求めて表示
  • 郵便番号を入力することにより、住所の一部を入力項目にセット

といったことも、出来るかな…と思います。

最後に

残された機能は、明細行に対応したり…でしょうか。
また、このチュートリアルの範囲からは外れるかもしれませんが、共通箇所のローディング手法なども、まだ、取り上げてはいませんね。

次回は、確認画面の実装を考えてみます。

inuinu-tech.hatenablog.jp

それでは、また。