10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ワクチン接種予約サイトに学ぶフォームバリデーション+α

Last updated at Posted at 2021-05-19

話題の「自衛隊東京ワクチン接種web予約」サイトをモデルにHTMLとJavaScriptでクライアント側のフォームの実装(バリデーションなど:詳しくは下のMDNを見てね)を練習しました。

HTMLのフォームバリデーションを利用することで、サーバーへ送信する前にクライアント側で値が適正か(少なくとも様式が正しいか)をチェックできます。無駄なリクエストを減らせます1 2

**この記事の更新記録**
2021-05-20
投稿
2021-05-21
コメントいただいた内容について追記しました。
誤っていた内容を修正しました。(取り消し線を引いています。)
リンクをテキストからカードへ変更しました。(カードの存在を知りませんでした。)

デモ

コードの可読性は考慮してませんのでご了承ください。

HTML

html
<form name=auth>
  <table>
    <tr>
      <th>市区町村<wbr>コード
      <td>
        <input name=cityCode inputmode=numeric placeholder=入力してください pattern=\d{6} autocomplete=off required>
        <small>半角6桁</small>
        <output name=cityCodeDigits class=digits></output>
    <tr>
      <th>接種券番号
      <td>
        <input name=ticketCode inputmode=numeric placeholder=入力してください pattern=\d{10} autocomplete=off required>
        <small>半角10桁</small>
        <output name=ticketCodeDigits class=digits></output>
    <tr>
      <th>生年月日
      <td>
        <label>
          <select name=birthYear required></select>
          <span></span>
        </label>
        <label>
          <select name=birthMonth required></select>
          <span></span>
        </label>
        <label>
          <select name=birthDay required></select>
          <span></span>
        </label>
  </table>
  <button name=submit>認証</button>
</form>

<form><input>にはname属性を振ります。id派もいるかもしれませんが基本的に不要だと思っています。

属性 特徴・JSでの参照方法
id <label for=>のターゲットになる。ラベルを分けなければ必要ない。
document.getElementById()でアクセス。
name 送信したときにサーバ側へ一緒に送られる。
<form>であればdocument.forms[name]<input>等であればHTMLFormElement[name]でアクセスできる。

input要素

「市区町村コード」「接種券番号」に使用しました。

https://developer.mozilla.org/ja/docs/Web/HTML/Element/input

type属性でコントロールのタイプを指定します。何も指定しなければtype=textです。下の表に今回使えそうなtypeを紹介します。

type 説明 備考
text テキスト デフォルト
number 数値
date 日付(年月日) ブラウザに依ってピッカーのインターフェースが違う
month 年月 Chrome系ブラウザ/Edgeのみ実装
button
submit
ボタン
送信ボタン
<button>要素のほうが使い勝手が良い

「市区町村コード」が数字6桁、「接種券番号」が数字10桁なので一瞬type=numberが正解そうに見えます。しかし、type=number012345というような先頭に0が付くようなフォーマットに対応していないので、type=textを使うことになります。

追記 (2021-05-21): type=telを使う方法

コメントでtype=telを使う方法を教えていただきました。(@brocadedCongerさんありがとうございます。)

<script>
  const numValidate = (e, n) => e.value = ("0".repeat(n) + e.value.replace(/\D/g, '')).slice(-n);
</script>

6桁 <input type='tel' value='000000' oninput='numValidate(this, 6)'><br>
10桁 <input type='tel' value='0000000000' oninput='numValidate(this, 10)'>

この方法を使えばinputmode属性(後述)を指定しなくてもスマートフォンなどでテンキーが表示されます。
※セマンティクス的には賛否が分れるかもしれません。

各属性について

pattern属性

フォームバリデーションの要です。type=textでしか使えないようなので要注意です3
追記 (2021-05-21):type=textの他にtype=teltype=passwordでも使えるようです。

正規表現を指定します。この正規表現パターンにマッチしないとフォームを送信できません(ブラウザがストップしてくれます)。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_Expressions

今回のモデルの例では「市区町村コード」は数字6桁なのでpattern=\d{6}、「接種券番号」は数字10桁なのでpattern=\d{10}になります。もちろん\d[0-9]に書き換え可能です。

ちなみに東京都の市区町村コードは「13」から始まるので13\d{4}にするとさらに正確です。市区町村コードは総務省にあります。大した数ではないので|を多用しても問題ないかと思います。

autocomplete属性

テキストボックスに以前入力した値がプルダウンで出てくる機能です。onoffを指定します。

required属性

必須の項目になります。値はありません(requiredだけでOK)。

inputmode属性

<input>に限らず使えるグローバル属性です。主にソフトウェアキーボードで入力する場面(スマートフォンなど)で、キーボードの種類を指定します。

https://developer.mozilla.org/ja/docs/Web/HTML/Global_attributes/inputmode

追記 (2021-05-21): その他使えそうな属性

maxlength
(UTF-16単位で)入力できる最大文字数
minlength
入力できる最小文字数

maxlength=minlengthにすれば桁数を制限できます。

おまけ1: 入力桁数をリアルタイムで表示する

本家サイトに入力桁数のリアルタイム表示機能があったので再現しました4

javascript
const auth = document.forms.auth;
const { cityCode, ticketCode } = auth;

const displayDigits = e => {
  const output = auth[e.target.name + "Digits"];
  output.value = `${e.target.value.length || ""}`;
};
cityCode.oninput = displayDigits;
ticketCode.oninput = displayDigits;
css
output.digits:empty {
  display: none;
}
output.digits::before {
  content: "現在の入力桁数:";
}

おまけ2: select要素にoption要素を動的に追加する

もちろんHTMLに直接書くのがベターですが、配列風オブジェクトから動的に<option>を生成しています(気持ちスッキリに見えなくもない)。

javascript
const { birthMonth, birthDay } = auth;
birthMonth.innerHTML = Array.from({ length: 12 }, (_, m) => `<option value=${1 + m}>${1 + m}`).join("");
birthDay.innerHTML = Array.from({ length: 31 }, (_, d) => `<option value=${1 + d}>${1 + d}`).join("");

年月日の「年」については後述します。
「月」「日」はlengthプロパティのみを持ったオブジェクトをそのままmapしてそれぞれHTMLで書いて、最後にjoin()してinnerHTMLにぶっ込んで終わりです(説明下手)。詳しくはMDNのArray.from()の説明を見てください。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/from

おまけ3: JavaScriptで西暦を和暦に変換する

これも直接HTMLに書くか、明治・大正・昭和ぐらいであればif文でなんとかしたほうがベターです。(モダンブラウザでも対応していない場合があります。)

javascript
const { birthYear } = auth;
const eraFormatter = new Intl.DateTimeFormat(["ja-JP-u-ca-japanese"], { era: "long", year: "numeric" });
birthYear.innerHTML = Array.from({ length: new Date().getFullYear() - 1965 }, (_, y) => `<option value=${1901 + y}>${1901 + y}年(${eraFormatter.format(new Date(1901 + y, 11, 31))})`).reverse().join("");

Intl.DateTimeFormatクラスで日付をロケールのフォーマットに整形することができます。コンストラクタの第1引数でロケールを、第2引数でオプションを指定します。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat

65歳以上を対象にする場合は、現在の日付を取得して引き算をするなどして対応します5。上の例ではデフォルトの選択肢を対象の人数が最も多いものになるように、生年をArray.reverse()で降順に並べています6

おまけ4: 年月に合わせて日の選択可否を動的に設定する

月によって末日が30日だったり31日だったり、もしくは2月末日も閏年かどうかにとって変わりますので、日の選択肢が年月の選択に追従して変更されるようにします7

javascript
const daysOfMonth = (y, m) => m !== 2 ? [31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m - 1] : y % 4 || !(y % 100) && y % 400 ? 28 : 29;
const disableInvalidDay = e => {
  const days = daysOfMonth(parseInt(birthYear.value), parseInt(birthMonth.value));
  const options = birthDay.options;
  options[28].disabled = days <= 28;
  options[29].disabled = days <= 29;
  options[30].disabled = days <= 30;
  birthDay.selectedIndex = Math.min(birthDay.selectedIndex, days - 1); // 選択中の日がdisabledのときは最も近い選択肢を選択し直す
};
birthYear.onchange = disableInvalidDay;
birthMonth.onchange = disableInvalidDay;

追記 (2021-05-21): Dateを使って月の末日を得る方法

コメントでDateオブジェクトを使う方法を教えていただきました。(@okiameenaさんありがとうございます。めちゃくちゃスッキリ書けます。)

const daysOfMonth = (y, m) => new Date(y, m, 0).getDate();


```js:解釈
/**
 * @param {number} year - 西暦 (1970=1970年, 2021=2021年)
 * @param {number} monthIndex - 月のインデックス (0=1月, 1=2月, ..., 11=12月)
 * @param {number} day - 日にち (1=1日, 2=2日, ..., 31=31日)
 */
date = new Date(year, monthIndex, day);

ここでday = 0とすればdateは繰り下がって前月の末日を示すことになります。monthIndex = month - 1monthはn月のn)なので、Date(year, month, 0)とすることでその月の末日(つまりdaysOfMonthその月は何日あるか)が得られるようです。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date

まとめ

まとまりがなく読みにくかったと思いますが、大変勉強になるモデルを元にフォームバリデーション+αを練習してみました。何かおかしい点、不明な点などありましたら、コメントお願いいたします。

  1. クライアント側でバリデーションをしたからと言ってサーバー側でサボるのは良くありません。ちょっとの知識があればHTMLのバリデーション程度はごく簡単に回避できますし、もう少し知識があればリクエストを改竄(偽装)することもできます。

  2. 今回のモデルにした本家様は「サーバーへ負荷をかけないために」とか言いつつ、HTMLで簡単に実装できる桁数チェックをわざわざサーバー側で行っている疑惑があり、これは七不思議の1つです。

  3. ちなみにtype=numberでバリデーションを再現しようとするとminmaxstep属性で最小値・最大値・刻みを指定するのが限界です。

  4. 入力桁数のリアルタイム表示はするのにバリデーションはしないというのも七不思議の1つです。

  5. 対象外年齢の選択肢を作っても無駄ですし、ユーザーを混乱させる元になりますので気をつけましょう。なお、本家のフォームでは世界最高齢を超える長寿の方からまだ生まれていないベイビーまで選択肢がありますが、これも七不思議の1つです。

  6. 本家では65歳以上が対象のようですが、デフォルトの選択肢は1970年1月1日生になっている点も七不思議の1つです。

  7. 本家のフォームは1年=372日で設計されているようです。防衛省内で独自の暦を使用している可能性もありますが推測の域を出ておらず、これも七不思議の1つです。

10
8
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?