2 ポイント 投稿者 GN⁺ 2024-10-29 | 1件のコメント | WhatsAppで共有
  • HTMLフォームには requiredtype="email"patternmaxlengthsetCustomValidity などのネイティブ検証機能がすでに備わっているが、実際の活用はその機能の幅に比べて低い
  • 最も強力な setCustomValidity は任意の検証ロジックや複雑なケースを処理できるが、DOMメソッドとしてしか提供されておらず、宣言的コンポーネントとは相性がよくない
  • Reactのような環境では、初期値の検証まで合わせようとすると useRefuseLayoutEffectonChange が絡み合い、同じ検証ロジックが初期レンダリングと変更ハンドラに重複しやすい
  • custom-validity のような属性ベースの抽象化があれば、非同期のユーザー名重複チェックやパスワード確認のように、状態と入力の間に依存関係がある検証を1か所で表現できる
  • ネイティブフォーム検証の採用率が低いのは、機能不足というより APIの使い勝手 の問題に近く、宣言的APIがHTML仕様に入れば活用の余地は大きくなる

HTMLフォーム検証がすでに提供しているもの

  • 空入力を防ぐ最も簡単な方法は required 属性を追加すること
  • 入力制約は大きく3つの方法でかけられる
    • type="email"type="number"type="url" のような 入力タイプ を使う
    • patternmaxlength のような 制約属性 を使う
    • 入力の setCustomValidity DOMメソッドを使う
  • setCustomValidity は任意の検証ロジックや複雑なケースを扱えるため、最も強力な手段である
  • 前の2つの方法はHTML属性として宣言できるが、setCustomValidityメソッド呼び出し が必要だという点が異なる
  • DOM属性とプロパティの違いを扱った参考資料として Attributes vs Properties がリンクされている

setCustomValidity が不便になるポイント

  • setCustomValidity は対応するHTML属性がなく、メソッドとしてしか公開されていない
  • 文字列を渡すと入力は invalid 状態になり、ブラウザはその文字列を検証失敗の理由として表示する
  • 空文字列を渡すと、他の制約がない限り入力は valid 状態になる
  • required のような動作を自前で実装するには、値が変わるたびに setCustomValidity を呼び出さなければならない
  • ただし入力は最初は valid 状態なので、コンポーネントをリセットした直後に submit ボタンを押すとフォーム送信が通ってしまうことがある
    • 値が空でも、変更イベントが発生する前は検証コードが実行されないため
    • 初期値まで合わせるには、コンポーネントのマウント時点でも同じ検証を実行する必要がある

宣言的コンポーネントで増えるボイラープレート

  • 初期値の検証まで処理しようとすると、コードはすぐに反復的で扱いづらくなる
  • 特に3つの問題が生じる
    • 検証ロジックが onChange ハンドラと初期レンダリング段階に重複する
    • 初期検証コードが入力要素から離れていて凝集度が低くなり、片方だけ修正してしまうリスクがある
    • useRefuseLayoutEffectonChange の組み合わせは入力が増えるほど大げさになり、一部の入力だけで customValidity を使う場合はさらに混乱しやすい
  • この複雑さは、純粋な命令的APIを宣言的コンポーネントで扱うときに特に目立つ
  • CustomValidity にはネイティブ検証属性のように宣言的に値を設定できる入力属性がない
  • APIが煩雑だと、機能が強力でも採用は低くなりうる。この点こそが、ネイティブフォーム検証が十分に活用されていない主な原因である

欠けているピース: custom-validity 属性

  • 必要なのは、入力に直接指定できる custom-validity のような 宣言的属性 である
  • 宣言的フレームワークでは、この属性によって検証状態を入力要素の近くに置ける
  • 現在の HTML Spec には実際の custom-validity は存在しない
  • デモ目的のユーザーランド実装でこの動作を模倣できる
  • 本番向けコンポーネントの例として、より 完成度の高い実装 が提供されている

非同期検証と入力間の依存関係

  • 実際のアプリケーションの検証は、ローカルチェックより複雑になることがある
  • ユーザー名入力では、すでに使用中の名前かどうか をサーバーに確認する必要があり、リクエスト中はフォームが valid 状態であってはならない
  • 例では、空入力を防ぐために required を使い、読み込み状態と応答結果に応じて customValidity で入力を invalid 状態にする
  • 実装は2つの部分で構成される
    • ユーザー名の一意性確認リクエストの状態を react-queryuseQuery で管理する
    • customValidity prop を受け取れるカスタム <Input /> コンポーネントを使う
  • この方法なら、読み込み・エラー・成功状態を含む全体の非同期検証フローを1つの属性で表現できる
  • 追加例として、入力したパスワードを再入力させるフォームがあり、相互に依存する入力フィールドの検証を扱っている

結論

  • setCustomValidity は多様な検証要件を処理できる強力なツールである
  • 実際の活用度を左右するのは、機能の有無だけでなく、その機能を使いやすくするAPIである
  • custom-validity のような宣言的抽象化は、ネイティブフォーム検証をより自然に使えるようにする
  • こうした機能がいつかHTML仕様にネイティブで入ることを期待できる

1件のコメント

 
GN⁺ 2024-10-29
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 = '...' のようにフォーム状態をプログラムから変更しても、この種のイベントはトリガーされない
      これは別途調査してウェブプラットフォームに提案する価値のある良いテーマだと思う
    • なぜそこまでスタイリングしたがるのか、あまり理解できない。色やレイアウトで表現するのはよいが、ある時点からは ブランディングよりユーザビリティ のほうが重要だ
    • その通り。ただ、デフォルトメッセージを隠して自作のメッセージに置き換えることはできる。それでもフォームバリデーションの利点はそのまま得られる
    • そういう意味では 単一の select のスタイリング も、当然もっと簡単であるべきなのにそこまで簡単ではない
  • 最も大きく、しかも実装しやすいのにあまり使われていないのは、emailnumberurl のような 具体的な type 属性値 を使うことだ
    モバイルで最適なキーボードを表示でき、ユーザー体験を大きく改善できる

    • type=number は避けて、代わりに type=text inputmode=numeric を使う。ほとんどのユーザーが数値入力で必要としない矢印ボタンが付かず、iOS でもキーボードがより良い
    • ログインフォームに「email」と表示しておきながら、正しい type を設定していない場所が驚くほど多い
    • date 入力でもこうした例を非常によく見る。フロントエンドライブラリごとに独自の日付ウィジェットがあり、小さな画面ではひどく見えるものも多い
      そのウィジェットひとつのために JS と CSS を追加しなければならず、一部は jQuery に依存している。もちろん設定項目が非常に多いものもあるが、ごく小さなコストで、どこでもそれなりに使えてネイティブらしく見えるウィジェットを得られ、たいていの要件も満たせるし、アップデートや CDN を心配せず放っておける
    • 残念ながら number 入力 は不十分で、ブラウザごとの一貫性もない。結局いつも JavaScript バリデーションに戻ることになった
    • 最近はもっと複雑な入力要素を使わないよう確認する必要があった。いくつもの理由で、ページ内に実装された オンスクリーンキーボード で入力を制御しなければならなかったからだ
      通常の入力でもかろうじて可能だが、特殊な入力は対応しているイベントがさらに少ない
  • 私の製品は ネイティブHTMLフォーム検証 を使っているという理由でアクセシビリティ監査を通過できず、公式の勧告は独自の検証レイヤーを実装せよというものだった。私もその勧告には同意する
    ネイティブHTML検証には欠陥が多く、正直なところ視覚的なカスタマイズは最大の懸念ですらない。ただ、棺に打つ最後の釘に近くはある
    たとえばフィールドごとに複数のエラーを一度に表示しようとすると、文字列を連結するしかない。"数字が必要です。記号が必要です。10文字より長くなければなりません" といった具合だが、これはユーザー体験としても悪く、アクセシビリティの面でもよくない。アクセシビリティツリーでは連結した文字列をたどれないからだ。これは実装ごとの問題ではなく、仕様そのものの問題だ。ユーザーに検証エラーのモグラたたきをさせるのは楽しくないのだから、複数のエラーを一度に表示できるべきだ
    ブラウザ依存だという点もよくない。制御できず、実装もたいていひどい。Chromeはフォーカス時にポップアップを出すが、ポップアップでありモーダルのようにも見え、フォームの全エラーを一度に表示できないので、それ自体アクセシビリティ的によくない。重要な情報をポップアップで見せるなというのはアクセシビリティの基本だが、ブラウザには実際のドキュメントを邪魔せずに他にできることがあまりない
    特定のフィールドに属さないフォーム全体のエラーでも、依然としてカスタム検証が必要だ。"AとBのフィールドは両立しない" のようなエラーがそれで、だったら一貫した検証方式を持つほうがよい
    ネイティブ入力タイプに合わないカスタム入力があるなら、一貫性のために隠し入力を置く必要があるが、これもアクセシビリティを壊す。直そうとすると、結局アクセシビリティに配慮した独自の検証体系を作るのと同じくらい難しい
    customValidity APIは命令型で使いづらい。"このフィールドは無効です" のようなひどいメッセージを望まない限り、カスタムバリデーションを使わないのはほぼ選択肢にならない。だからHTMLフォーム検証はひどい

    • ブラウザが提供する機能が アクセシビリティ要件 を満たせないことがある、というのはたしかに皮肉だ。私たちはいつも、プラットフォームを使いセマンティックな要素に従う動機の一つがアクセシビリティだと教わってきたからだ
      それでもネイティブ検証メカニズムを活用する価値はあると思う。エラーメッセージにネイティブの妥当性ツールチップを必ず使う必要はなく、input.validityValidityState を直接読んで、望む形でメッセージをレンダリングし、必要なら複数のエラーも表示できる
      ブラウザは改善されうるし、標準化されたアプローチを使えばその恩恵を受けられる。"ポップアップを使わなければアクセシビリティが壊れる" という結論は奇妙に聞こえるが、監査対応が必要ならそうするしかないのかもしれない
      特定のフィールドに属さないエラーを扱う手法もある。私のプロジェクトでは、他のフィールドに属さないカスタムエラーをレンダリングするために、見えない読み取り専用入力を作る HiddenValidationInput コンポーネントを使っている。条件付きでレンダリングするだけでよく、実際かなり使いやすい
      customValidity APIが命令型で煩雑だという点には完全に同意する。まさにその部分を扱う記事を書いたし、この点も時間とともに改善されてほしい
    • 面白いが、私のアプリではそうした検証をすべて実装している。フォーム全体の検証 はサーバーで処理する。基本的には複数の検証レイヤーを置き、使えるものはすべて活用する方式だ
      Chromeの粗雑でアクセシビリティの低いデザインはChromeの問題なので、バグレポートを送るつもりだ
      "仕様" というのはこれのことか? https://html.spec.whatwg.org/multipage/dom.html#concept-elem...
      検証メッセージをどう表示すべきかを定義した部分は見当たらない。そもそもメッセージが必須だという記述すらない
    • 監査担当者たちがそう判断した理由が何だったのか気になる
  • 正直に言うと、こういう仕様を書く人たちは現実とかけ離れていて、自分たちが仕様化したものを実際には使っていないように思える。ごく単純なものには機能するが、フォームが進化し始めると結局は全部自分で書いたほうがいいと気づく

    • その通り。他のフィールドとの 相互参照 のような比較的よくあることをするだけでも、ほぼすぐにJSが必要になる。たとえばユーザーが国を指定していれば今入力した郵便番号を検証できるが、そうでなければ国の選択を待たなければならない
      JSを使い始めた瞬間、検証属性をいじくるよりコードで全部やるほうがずっと簡単になる
    • その通り。クロスブラウザの日付ピッカー が必要になるまでは素晴らしいが、その時点からは多くを自前で実装しなければならない。これだけ長い年月が経っているのにHTMLフォームがこんなに原始的なのはもどかしい
    • フォーム機能の大半は90年代初頭のものだ。仕様作成者の一部と同じ千年紀で仕事をしていないのかもしれない
  • チェックボックスにラベルがあるなら、ラベルに for 属性を付けて、ラベルのクリックでチェックボックスをオン・オフできるようにしてほしい。個人的には最もいら立つことの一つだが、そう感じるのは私だけかもしれない

    • inputlabel で包んでも動作する。なぜ人々がこの二つを分けて置く傾向があるのかよく分からない
      それと、なぜブラウザがこれらを分離し始めたのかも分からない。テキストがチェックボックスやラジオボタンを含むのではなく、チェックボックスとラジオボタン がテキストを含むべきだと思う
    • あなただけではない。ADA/WCAGに従う アクセシブルなサイト では必要な機能だ
  • Reactを使わない単純な例はここにある
    https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectE...

    • 標準HTML検証の話をしながら、すべての例を React で見せるのはあまりに奇妙だと思った。標準がどう動くかを教えるなら、Reactをデフォルト前提にすべきではない
    • 今なら大半は CSSだけで もできそうだ。デフォルトのポップアップが出ないようにする部分だけJavaScriptが必要かもしれない
  • HTMLフォーム検証は素晴らしい。ただし、とても大きな落とし穴が1つある
    Android版Firefoxでは動作しない
    https://bugzilla.mozilla.org/show_bug.cgi?id=1510450

    • モバイル機器ではすべてFirefoxを使っているが、この件で開発者がAndroid版Firefoxを無視することを責めるのは難しい。他のすべてのブラウザでは10年前から動いているAPIであり、この混乱を整理する責任はFirefoxチームにある
      赤く表示されたコントロールのエラーメッセージを見られない1%未満のユーザーのために、標準を無視する価値はない。これを使うウェブサイトが増えるほど、Mozillaがブラウザを修正しなければならないという圧力も強まるだろう
      これはChromeやSafariが標準化前に実装したWebMIDIのようなAPIではなく、もともとHTML5仕様の一部だ
    • Android版FirefoxはSamsung InternetやOperaよりもユーザー基盤が小さく、0.5%程度だ。サポートに時間を使うのはほとんど無駄に近い
      とくに、アクセシビリティソフトウェアを使う人がサイトをきちんと利用できるようにするために割かれる時間がどれほど少ないかを考えるとなおさらだ。UC Browserまで話題にするつもりでないなら、この種の問題で言及する価値はあまりないと思う
    • AndroidでFirefoxを毎日使う立場として、標準に追いつけていないことがいちばんつらい。とくに[1]のようなWebGL関連の問題や、[2]のような些細な不便さがそうだ
      それでも、他の拡張機能と一緒にuBlock Originを使えるのは強力な利点だ
      [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1884282
      [2] https://bugzilla.mozilla.org/show_bug.cgi?id=1897707
    • 「動作しない」よりずっと悪い。検証自体は動いているのに、エラーがまったく表示されない
      検証がまるごと未実装ならまだましだったが、これは本当にひどい。数年前、長くてつらいデバッグの末に気づいたことで、自分と同じ目に遭った人は最初でも最後でもないと確信している
    • 最近AndroidでFirefoxに乗り換えてから、ようやくこれに気づいた。アプリを作っている最中、必須の空欄フィールドを送信しようとしても、文字どおり何も表示されなかった
      検索するまでは自分が何を見落としていたのかまったく想像もできず、かなり衝撃だった。独自検証を作るのは構わないが、これは動作すべきだ
  • 使いすぎには注意すべきだ
    最近Grouponで払い戻しを受けようとした。Grouponを購入した店舗が新しい経営陣に変わり、自分のGrouponを認めなかったからだ
    フォームには「最低15語」という条件があったが、どうやっても検証を通過できず、結局HTMLを調べる羽目になった
    \wは単語文字、\bは単語境界、\sは空白で、句読点は文字どおりいっさい許可されていなかった

    • これはHTML検証に限った問題ではないと思う。どんな種類の検証でも同じことは起こる。同じルールがサーバー側に適用されていた可能性もあり、そうなら救いはなかっただろう
    • これは私が言いたかった要点のよい根拠でもある。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...
    • 人々がJavaScriptを使い始めた主な理由の1つがこれだ。フォームが「触られた」あとや送信直前にだけエラーを表示するためだ
    • なぜこれがデフォルトの体験として選ばれたのか理解したことがない。大半のユーザーは、まだ行動する機会すらないのに、すべてのフォーム要素が自分に向かって叫んでくるのを好まない
      スクリプトで回避はできるが、その時点ではこの機能が大して役に立っていない。これが広く採用されない最大の理由はまさにそこだと思う