HTMLフォーム検証の活用不足
(expressionstatement.com)- HTMLフォームには
required、type="email"、pattern、maxlength、setCustomValidityなどのネイティブ検証機能がすでに備わっているが、実際の活用はその機能の幅に比べて低い - 最も強力な
setCustomValidityは任意の検証ロジックや複雑なケースを処理できるが、DOMメソッドとしてしか提供されておらず、宣言的コンポーネントとは相性がよくない - Reactのような環境では、初期値の検証まで合わせようとすると
useRef、useLayoutEffect、onChangeが絡み合い、同じ検証ロジックが初期レンダリングと変更ハンドラに重複しやすい custom-validityのような属性ベースの抽象化があれば、非同期のユーザー名重複チェックやパスワード確認のように、状態と入力の間に依存関係がある検証を1か所で表現できる- ネイティブフォーム検証の採用率が低いのは、機能不足というより APIの使い勝手 の問題に近く、宣言的APIがHTML仕様に入れば活用の余地は大きくなる
HTMLフォーム検証がすでに提供しているもの
- 空入力を防ぐ最も簡単な方法は
required属性を追加すること - 入力制約は大きく3つの方法でかけられる
type="email"、type="number"、type="url"のような 入力タイプ を使うpattern、maxlengthのような 制約属性 を使う- 入力の
setCustomValidityDOMメソッドを使う
setCustomValidityは任意の検証ロジックや複雑なケースを扱えるため、最も強力な手段である- 前の2つの方法はHTML属性として宣言できるが、
setCustomValidityは メソッド呼び出し が必要だという点が異なる - DOM属性とプロパティの違いを扱った参考資料として Attributes vs Properties がリンクされている
setCustomValidity が不便になるポイント
setCustomValidityは対応するHTML属性がなく、メソッドとしてしか公開されていない- 文字列を渡すと入力は invalid 状態になり、ブラウザはその文字列を検証失敗の理由として表示する
- 空文字列を渡すと、他の制約がない限り入力は valid 状態になる
requiredのような動作を自前で実装するには、値が変わるたびにsetCustomValidityを呼び出さなければならない- ただし入力は最初は valid 状態なので、コンポーネントをリセットした直後に submit ボタンを押すとフォーム送信が通ってしまうことがある
- 値が空でも、変更イベントが発生する前は検証コードが実行されないため
- 初期値まで合わせるには、コンポーネントのマウント時点でも同じ検証を実行する必要がある
宣言的コンポーネントで増えるボイラープレート
- 初期値の検証まで処理しようとすると、コードはすぐに反復的で扱いづらくなる
- 特に3つの問題が生じる
- 検証ロジックが
onChangeハンドラと初期レンダリング段階に重複する - 初期検証コードが入力要素から離れていて凝集度が低くなり、片方だけ修正してしまうリスクがある
useRef、useLayoutEffect、onChangeの組み合わせは入力が増えるほど大げさになり、一部の入力だけでcustomValidityを使う場合はさらに混乱しやすい
- 検証ロジックが
- この複雑さは、純粋な命令的APIを宣言的コンポーネントで扱うときに特に目立つ
CustomValidityにはネイティブ検証属性のように宣言的に値を設定できる入力属性がない- APIが煩雑だと、機能が強力でも採用は低くなりうる。この点こそが、ネイティブフォーム検証が十分に活用されていない主な原因である
欠けているピース: custom-validity 属性
- 必要なのは、入力に直接指定できる
custom-validityのような 宣言的属性 である - 宣言的フレームワークでは、この属性によって検証状態を入力要素の近くに置ける
- 現在の
HTML Specには実際のcustom-validityは存在しない - デモ目的のユーザーランド実装でこの動作を模倣できる
- 本番向けコンポーネントの例として、より 完成度の高い実装 が提供されている
非同期検証と入力間の依存関係
- 実際のアプリケーションの検証は、ローカルチェックより複雑になることがある
- ユーザー名入力では、すでに使用中の名前かどうか をサーバーに確認する必要があり、リクエスト中はフォームが valid 状態であってはならない
- 例では、空入力を防ぐために
requiredを使い、読み込み状態と応答結果に応じてcustomValidityで入力を invalid 状態にする - 実装は2つの部分で構成される
- ユーザー名の一意性確認リクエストの状態を
react-queryのuseQueryで管理する customValidityprop を受け取れるカスタム<Input />コンポーネントを使う
- ユーザー名の一意性確認リクエストの状態を
- この方法なら、読み込み・エラー・成功状態を含む全体の非同期検証フローを1つの属性で表現できる
- 追加例として、入力したパスワードを再入力させるフォームがあり、相互に依存する入力フィールドの検証を扱っている
結論
setCustomValidityは多様な検証要件を処理できる強力なツールである- 実際の活用度を左右するのは、機能の有無だけでなく、その機能を使いやすくするAPIである
custom-validityのような宣言的抽象化は、ネイティブフォーム検証をより自然に使えるようにする- こうした機能がいつかHTML仕様にネイティブで入ることを期待できる
1件のコメント
Hacker News の意見
最後に確認したときも、最近のウェブブラウザでは 組み込みの HTML バリデーションメッセージ の見た目をスタイリングできなかった https://stackoverflow.com/questions/5328883/how-do-i-style-t...
Chrome と Firefox がせめて OS プラットフォームの UI ガイドラインに従って、
title=""ツールチップのようにシステムが表示したものに見えていればまだましだったが、Chrome は黄色/オレンジのアイコン、白背景に黒文字、固定された角丸の吹き出しを使うので、今のプロジェクトの美観と激しく衝突する昔の Chrome はベンダープレフィックス付きの疑似要素セレクタでバリデーションメッセージのスタイリングを許可していたが、その機能を削除してから再び戻していない。「ネイティブ HTML コンボボックスを何とかしてくれ」「なぜいまだにチェックボックス一覧ではなく使いづらい Ctrl+クリックボックスなんだ」といった任意のイライラ一覧に加えたくなる
validity状態を読んで好きなようにレンダリングすること自体はかなり簡単だという点にある本当の問題は、この妥当性状態の変化を正確に購読するのが厄介なことだ。妥当性イベントはあるが、常に発生するわけではない。
form.reset()の呼び出しやinput.value = '...'のようにフォーム状態をプログラムから変更しても、この種のイベントはトリガーされないこれは別途調査してウェブプラットフォームに提案する価値のある良いテーマだと思う
最も大きく、しかも実装しやすいのにあまり使われていないのは、
email、number、urlのような 具体的なtype属性値 を使うことだモバイルで最適なキーボードを表示でき、ユーザー体験を大きく改善できる
type=numberは避けて、代わりにtype=text inputmode=numericを使う。ほとんどのユーザーが数値入力で必要としない矢印ボタンが付かず、iOS でもキーボードがより良いtypeを設定していない場所が驚くほど多いdate入力でもこうした例を非常によく見る。フロントエンドライブラリごとに独自の日付ウィジェットがあり、小さな画面ではひどく見えるものも多いそのウィジェットひとつのために JS と CSS を追加しなければならず、一部は jQuery に依存している。もちろん設定項目が非常に多いものもあるが、ごく小さなコストで、どこでもそれなりに使えてネイティブらしく見えるウィジェットを得られ、たいていの要件も満たせるし、アップデートや CDN を心配せず放っておける
通常の入力でもかろうじて可能だが、特殊な入力は対応しているイベントがさらに少ない
私の製品は ネイティブHTMLフォーム検証 を使っているという理由でアクセシビリティ監査を通過できず、公式の勧告は独自の検証レイヤーを実装せよというものだった。私もその勧告には同意する
ネイティブHTML検証には欠陥が多く、正直なところ視覚的なカスタマイズは最大の懸念ですらない。ただ、棺に打つ最後の釘に近くはある
たとえばフィールドごとに複数のエラーを一度に表示しようとすると、文字列を連結するしかない。"数字が必要です。記号が必要です。10文字より長くなければなりません" といった具合だが、これはユーザー体験としても悪く、アクセシビリティの面でもよくない。アクセシビリティツリーでは連結した文字列をたどれないからだ。これは実装ごとの問題ではなく、仕様そのものの問題だ。ユーザーに検証エラーのモグラたたきをさせるのは楽しくないのだから、複数のエラーを一度に表示できるべきだ
ブラウザ依存だという点もよくない。制御できず、実装もたいていひどい。Chromeはフォーカス時にポップアップを出すが、ポップアップでありモーダルのようにも見え、フォームの全エラーを一度に表示できないので、それ自体アクセシビリティ的によくない。重要な情報をポップアップで見せるなというのはアクセシビリティの基本だが、ブラウザには実際のドキュメントを邪魔せずに他にできることがあまりない
特定のフィールドに属さないフォーム全体のエラーでも、依然としてカスタム検証が必要だ。"AとBのフィールドは両立しない" のようなエラーがそれで、だったら一貫した検証方式を持つほうがよい
ネイティブ入力タイプに合わないカスタム入力があるなら、一貫性のために隠し入力を置く必要があるが、これもアクセシビリティを壊す。直そうとすると、結局アクセシビリティに配慮した独自の検証体系を作るのと同じくらい難しい
customValidityAPIは命令型で使いづらい。"このフィールドは無効です" のようなひどいメッセージを望まない限り、カスタムバリデーションを使わないのはほぼ選択肢にならない。だからHTMLフォーム検証はひどいそれでもネイティブ検証メカニズムを活用する価値はあると思う。エラーメッセージにネイティブの妥当性ツールチップを必ず使う必要はなく、
input.validityのValidityStateを直接読んで、望む形でメッセージをレンダリングし、必要なら複数のエラーも表示できるブラウザは改善されうるし、標準化されたアプローチを使えばその恩恵を受けられる。"ポップアップを使わなければアクセシビリティが壊れる" という結論は奇妙に聞こえるが、監査対応が必要ならそうするしかないのかもしれない
特定のフィールドに属さないエラーを扱う手法もある。私のプロジェクトでは、他のフィールドに属さないカスタムエラーをレンダリングするために、見えない読み取り専用入力を作る
HiddenValidationInputコンポーネントを使っている。条件付きでレンダリングするだけでよく、実際かなり使いやすいcustomValidityAPIが命令型で煩雑だという点には完全に同意する。まさにその部分を扱う記事を書いたし、この点も時間とともに改善されてほしいChromeの粗雑でアクセシビリティの低いデザインはChromeの問題なので、バグレポートを送るつもりだ
"仕様" というのはこれのことか? https://html.spec.whatwg.org/multipage/dom.html#concept-elem...
検証メッセージをどう表示すべきかを定義した部分は見当たらない。そもそもメッセージが必須だという記述すらない
正直に言うと、こういう仕様を書く人たちは現実とかけ離れていて、自分たちが仕様化したものを実際には使っていないように思える。ごく単純なものには機能するが、フォームが進化し始めると結局は全部自分で書いたほうがいいと気づく
JSを使い始めた瞬間、検証属性をいじくるよりコードで全部やるほうがずっと簡単になる
チェックボックスにラベルがあるなら、ラベルに
for属性を付けて、ラベルのクリックでチェックボックスをオン・オフできるようにしてほしい。個人的には最もいら立つことの一つだが、そう感じるのは私だけかもしれないinputをlabelで包んでも動作する。なぜ人々がこの二つを分けて置く傾向があるのかよく分からないそれと、なぜブラウザがこれらを分離し始めたのかも分からない。テキストがチェックボックスやラジオボタンを含むのではなく、チェックボックスとラジオボタン がテキストを含むべきだと思う
Reactを使わない単純な例はここにある
https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectE...
HTMLフォーム検証は素晴らしい。ただし、とても大きな落とし穴が1つある
Android版Firefoxでは動作しない
https://bugzilla.mozilla.org/show_bug.cgi?id=1510450
赤く表示されたコントロールのエラーメッセージを見られない1%未満のユーザーのために、標準を無視する価値はない。これを使うウェブサイトが増えるほど、Mozillaがブラウザを修正しなければならないという圧力も強まるだろう
これはChromeやSafariが標準化前に実装したWebMIDIのようなAPIではなく、もともとHTML5仕様の一部だ
とくに、アクセシビリティソフトウェアを使う人がサイトをきちんと利用できるようにするために割かれる時間がどれほど少ないかを考えるとなおさらだ。UC Browserまで話題にするつもりでないなら、この種の問題で言及する価値はあまりないと思う
それでも、他の拡張機能と一緒にuBlock Originを使えるのは強力な利点だ
[1] https://bugzilla.mozilla.org/show_bug.cgi?id=1884282
[2] https://bugzilla.mozilla.org/show_bug.cgi?id=1897707
検証がまるごと未実装ならまだましだったが、これは本当にひどい。数年前、長くてつらいデバッグの末に気づいたことで、自分と同じ目に遭った人は最初でも最後でもないと確信している
検索するまでは自分が何を見落としていたのかまったく想像もできず、かなり衝撃だった。独自検証を作るのは構わないが、これは動作すべきだ
使いすぎには注意すべきだ
最近Grouponで払い戻しを受けようとした。Grouponを購入した店舗が新しい経営陣に変わり、自分のGrouponを認めなかったからだ
フォームには「最低15語」という条件があったが、どうやっても検証を通過できず、結局HTMLを調べる羽目になった
\wは単語文字、\bは単語境界、\sは空白で、句読点は文字どおりいっさい許可されていなかったpatternのような既存の妥当性属性は優れているが、それだけでは不十分だたとえば「パスワード再入力」の例は、
setCustomValidityがなくてもpattern属性で実装できる。その場合は最初の入力値から正規表現を動的に組み立てる必要がある記事が長くなりすぎるので解法の比較まではしなかったが、
customValidityを使うと検証がずっと明快で読みやすくなるのが要点だ。ここでは、よりよいAPIが大きな違いを生む「最低15語」という制約も、
value.split(/\s+/).length >= 15のようにはるかに見やすく表現できるあまり使われていないのには理由がある。多くのフレームワークやライブラリが堅牢でスタイル指定可能な検証機能を提供しており、中には非常に洗練され拡張性の高いものもある。わざわざ苦労しなくていいなら、自分を苦しめないほうがいい
curl、あるいは同じフォーム検証を内蔵していない他のツールでフォームをいじろうとする人は常にいるクライアントが検証したか、正しく検証したかを信頼することはできないので、バックエンドは依然として入力を検証し、すべてのフィールドの検証エラーとともにフォームを表示できる必要がある
フロントエンド検証はユーザーを助けるためのものにすぎない。だが、スタイル指定したり送信時にバックエンド側で発火させたりするには、結局は独自のスタイル指定も実装する必要がある
落とし穴が多すぎるし、より複雑なチェックを簡単にサポートしようとすると、結局ライブラリを使うことになる
そのうえライブラリを使えば、場合によってはフロントエンドとバックエンドの間で検証コードの一部を共有できる可能性もある
とくにこの記事は、
useLayoutEffectで問題を1つ回避しているように見えるが、軽い気持ちでやることではないHTMLフォーム検証で嫌な点の1つは、ページ読み込み時点から実行されることだ。たとえばエラー状態のスタイルをこれに結び付けていると、フォームが最初からエラーだらけの状態で読み込まれ、ユーザーに威圧的に見えることがある
:user-invalid疑似クラスがある。ただし柔軟性に欠ける部分があるため、用途によっては十分でないかもしれないhttps://developer.mozilla.org/en-US/docs/Web/CSS/:user-inval...
スクリプトで回避はできるが、その時点ではこの機能が大して役に立っていない。これが広く採用されない最大の理由はまさにそこだと思う