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

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

https://developer.mozilla.org/ja/docs/Learn/Forms/Form_validation

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

[^validate]: クライアント側でバリデーションをしたからと言ってサーバー側でサボるのは良くありません。ちょっとの知識があればHTMLのバリデーション程度はごく簡単に回避できますし、もう少し知識があればリクエストを改竄（偽装）することもできます。
[^m1]: 今回のモデルにした本家様は「サーバーへ負荷をかけないために」とか言いつつ、HTMLで簡単に実装できる桁数チェックをわざわざサーバー側で行っている疑惑があり、これは七不思議の1つです。

<details>
<summary>**<font color="blue">この記事の更新記録</font>**</summary><dl>
<dt>2021-05-20</dt><dd>投稿</dd>
<dt>2021-05-21</dt><dd>コメントいただいた内容について追記しました。</dd><dd>誤っていた内容を修正しました。（取り消し線を引いています。）</dd><dd>リンクをテキストからカードへ変更しました。（カードの存在を知りませんでした。）</dd>
</dl></detail>

# デモ
<p data-height="512" data-theme-id="0" data-slug-hash="zYZKXXz" data-default-tab="result" data-user="ys-j" data-embed-version="2" data-pen-title="zYZKXXz" class="codepen"></p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

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

# HTML

```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=>`のターゲットになる。ラベルを分けなければ必要ない。<br>`document.getElementById()`でアクセス。 |
| name | 送信したときにサーバ側へ一緒に送られる。<br>`<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<br>submit | ボタン<br>送信ボタン | `<button>`要素のほうが使い勝手が良い |

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

### <font color="red">追記 (2021-05-21):</font> `type=tel`を使う方法
コメントで`type=tel`を使う方法を教えていただきました。（@brocadedCongerさんありがとうございます。）
<blockquote>

```html
<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)'>
```

</blockquote>

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

## 各属性について
### pattern属性
フォームバリデーションの要です。~~`type=text`でしか使えないようなので要注意です~~[^number]。
<font color="red">追記 (2021-05-21)：`type=text`の他に`type=tel`と`type=password`でも使えるようです。</font>

[^number]: ちなみに`type=number`でバリデーションを再現しようとすると`min`・`max`・`step`属性で最小値・最大値・刻みを指定するのが限界です。

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

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}`にするとさらに正確です。市区町村コードは[総務省](https://www.soumu.go.jp/denshijiti/code.html)にあります。大した数ではないので`|`を多用しても問題ないかと思います。

### autocomplete属性
テキストボックスに以前入力した値がプルダウンで出てくる機能です。`on`か`off`を指定します。

### required属性
必須の項目になります。値はありません（`required`だけでOK）。

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

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

### <font color="red">追記 (2021-05-21):</font> その他使えそうな属性
<dl>
<dt>maxlength</dt>
<dd>（UTF-16単位で）入力できる最大文字数</dd>
<dt>minlength</dt>
<dd>入力できる最小文字数</dd>
</dl>

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

# おまけ1: 入力桁数をリアルタイムで表示する
本家サイトに入力桁数のリアルタイム表示機能があったので再現しました[^m2]。

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

```js: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:css
output.digits:empty {
  display: none;
}
output.digits::before {
  content: "現在の入力桁数：";
}
```

# おまけ2: select要素にoption要素を動的に追加する
もちろんHTMLに直接書くのがベターですが、配列風オブジェクトから動的に`<option>`を生成しています（気持ちスッキリに見えなくもない）。

```js: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文でなんとかしたほうがベターです。（モダンブラウザでも対応していない場合があります。）

```js: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歳以上を対象にする場合は、現在の日付を取得して引き算をするなどして対応します[^m3]。上の例ではデフォルトの選択肢を対象の人数が最も多いものになるように、生年を`Array.reverse()`で降順に並べています[^m4]。

[^m3]: 対象外年齢の選択肢を作っても無駄ですし、ユーザーを混乱させる元になりますので気をつけましょう。なお、本家のフォームでは世界最高齢を超える長寿の方からまだ生まれていないベイビーまで選択肢がありますが、これも七不思議の1つです。
[^m4]: 本家では65歳以上が対象のようですが、デフォルトの選択肢は1970年1月1日生になっている点も七不思議の1つです。

# おまけ4: 年月に合わせて日の選択可否を動的に設定する
月によって末日が30日だったり31日だったり、もしくは2月末日も閏年かどうかにとって変わりますので、日の選択肢が年月の選択に追従して変更されるようにします[^m5]。

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

```js: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;
```

### <font color="red">追記 (2021-05-21):</font> `Date`を使って月の末日を得る方法
コメントで`Date`オブジェクトを使う方法を教えていただきました。（@okiameenaさんありがとうございます。めちゃくちゃスッキリ書けます。）

> ```js
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 - 1`（`month`はn月のn）なので、`Date(year, month, 0)`とすることでその月の末日（つまり`daysOfMonth`その月は何日あるか）が得られるようです。

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

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