話題の「自衛隊東京ワクチン接種web予約」サイトをモデルにHTMLとJavaScriptでクライアント側のフォームの実装(バリデーションなど:詳しくは下のMDNを見てね)を練習しました。
HTMLのフォームバリデーションを利用することで、サーバーへ送信する前にクライアント側で値が適正か(少なくとも様式が正しいか)をチェックできます。無駄なリクエストを減らせます1 2。
**この記事の更新記録**
- 2021-05-20
- 投稿
- 2021-05-21
- コメントいただいた内容について追記しました。
- 誤っていた内容を修正しました。(取り消し線を引いています。)
- リンクをテキストからカードへ変更しました。(カードの存在を知りませんでした。)
デモ
コードの可読性は考慮してませんのでご了承ください。
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=number
は012345
というような先頭に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属性
フォームバリデーションの要です。3。type=text
でしか使えないようなので要注意です
追記 (2021-05-21):type=text
の他にtype=tel
とtype=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属性
テキストボックスに以前入力した値がプルダウンで出てくる機能です。on
かoff
を指定します。
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。
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;
output.digits:empty {
display: none;
}
output.digits::before {
content: "現在の入力桁数:";
}
おまけ2: select要素にoption要素を動的に追加する
もちろんHTMLに直接書くのがベターですが、配列風オブジェクトから動的に<option>
を生成しています(気持ちスッキリに見えなくもない)。
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文でなんとかしたほうがベターです。(モダンブラウザでも対応していない場合があります。)
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。
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 - 1
(month
はn月のn)なので、Date(year, month, 0)
とすることでその月の末日(つまりdaysOfMonth
その月は何日あるか)が得られるようです。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date
まとめ
まとまりがなく読みにくかったと思いますが、大変勉強になるモデルを元にフォームバリデーション+αを練習してみました。何かおかしい点、不明な点などありましたら、コメントお願いいたします。
-
クライアント側でバリデーションをしたからと言ってサーバー側でサボるのは良くありません。ちょっとの知識があればHTMLのバリデーション程度はごく簡単に回避できますし、もう少し知識があればリクエストを改竄(偽装)することもできます。 ↩
-
今回のモデルにした本家様は「サーバーへ負荷をかけないために」とか言いつつ、HTMLで簡単に実装できる桁数チェックをわざわざサーバー側で行っている疑惑があり、これは七不思議の1つです。 ↩
-
ちなみに
type=number
でバリデーションを再現しようとするとmin
・max
・step
属性で最小値・最大値・刻みを指定するのが限界です。 ↩ -
入力桁数のリアルタイム表示はするのにバリデーションはしないというのも七不思議の1つです。 ↩
-
対象外年齢の選択肢を作っても無駄ですし、ユーザーを混乱させる元になりますので気をつけましょう。なお、本家のフォームでは世界最高齢を超える長寿の方からまだ生まれていないベイビーまで選択肢がありますが、これも七不思議の1つです。 ↩
-
本家では65歳以上が対象のようですが、デフォルトの選択肢は1970年1月1日生になっている点も七不思議の1つです。 ↩
-
本家のフォームは1年=372日で設計されているようです。防衛省内で独自の暦を使用している可能性もありますが推測の域を出ておらず、これも七不思議の1つです。 ↩