HTML5ではブラウザーベースのフォームバリデーションを実装するために、新しい属性がいくつか導入されました。たとえばpattern属性はtextarea要素やinputに対して有効な入力値を正規表現で定義し、required属性はフィールドが必須かどうか指定します。新しい属性が実装されていないレガシーなブラウザーでは、ポリフィルを実装するためのベースとして値を利用できます。これらの属性は、フォームの即時バリデーションを実現するなど、面白い機能強化に使えます。
バリデーションを厳しくしすぎると、ユーザーの自然なブラウジング動作を遮り、邪魔になってしまいます。たとえば、あるフィールドが不正だと、そこからTabキーで移れないフォームです。JavaScriptで、正しい入力となるまでフォーカスがそのフィールド内に強制的にとどまるようになっていたのです。これでは使い勝手が悪く、アクセシビリティガイドラインにも違反します。
クライアント側の完全なバリデーションではなく、アクセシブルに実装して、使い勝手を少しだけ向上させる程度の解説をします。スクリプトをテストしているときに気がつきましたが、実はFirefoxの現行バージョンがネイティブでやっていることとほぼ同じです。
基本コンセプト
Firefoxの最近のバージョンではrequiredフィールドが未入力だったり、フィールドの値がpatternにマッチしなかったりすると、フィールドが赤く囲まれます。
すべての必須入力フィールドがデフォルトで赤枠で囲まれるのではなく、ユーザーがフィールドに入力しようとしたときに赤枠が現れます。正確には違いますが、onchangeイベントに似た動きになります。
onchangeをトリガーイベントにします。oninputイベントは、フィールドに値が入力あるいはペーストされた瞬間、発火しますが、これでは早すぎです。タイピング中に文字を打つごとに何回も赤枠が出たり消えたりすると、ユーザーが邪魔に感じます。oninputはプログラムで発火させることはできませんが、onchangeは可能です。サードパーティのアドオンでオートコンプリートなどを処理する場合は必要です。
HTMLとCSSを定義する
実装に移ります。HTMLのベースになるコードです。
<form action="#" method="post">
<fieldset>
<legend><strong>Add your comment</strong></legend>
<p>
<label for="author">Name <abbr title="Required">*</abbr></label>
<input
aria-required="true"
id="author"
name="author"
pattern="^([- \w\d\u00c0-\u024f]+)$"
required="required"
size="20"
spellcheck="false"
title="Your name (no special characters, diacritics are okay)"
type="text"
value="">
</p>
<p>
<label for="email">Email <abbr title="Required">*</abbr></label>
<input
aria-required="true"
id="email"
name="email"
pattern="^(([-\w\d]+)(\.[-\w\d]+)*@([-\w\d]+)(\.[-\w\d]+)*(\.([a-zA-Z]{2,5}|[\d]{1,3})){1,2})$"
required="required"
size="30"
spellcheck="false"
title="Your email address"
type="email"
value="">
</p>
<p>
<label for="website">Website</label>
<input
id="website"
name="website"
pattern="^(http[s]?:\/\/)?([-\w\d]+)(\.[-\w\d]+)*(\.([a-zA-Z]{2,5}|[\d]{1,3})){1,2}(\/([-~%\.\(\)\w\d]*\/*)*(#[-\w\d]+)?)?$"
size="30"
spellcheck="false"
title="Your website address"
type="url"
value="">
</p>
<p>
<label for="text">Comment <abbr title="Required">*</abbr></label>
<textarea
aria-required="true"
cols="40"
id="text"
name="text"
required="required"
rows="10"
spellcheck="true"
title="Your comment"></textarea>
</p>
</fieldset>
<fieldset>
<button name="preview" type="submit">Preview</button>
<button name="save" type="submit">Submit Comment</button>
</fieldset>
</form>
簡単なコメント入力用のフォームで、入力が必須のフィールド、入力内容がチェックされるフィールド、両方のフィールドがあります。requiredのあるフィールドにはaria-requiredもあるので、新しいinputタイプを理解できない支援技術にも代替のセマンティクスを提供します。
ARIAの仕様書ではaria-invalid属性が定義されており、フィールドが無効なことに示すことができます(HTML5には同様な属性はありません)。aria-invalid属性はもちろんアクセシブルな情報を提供しますが、赤枠を描くCSSフックとしても使えます。
input[aria-invalid="true"], textarea[aria-invalid="true"] {
border: 1px solid #f00;
box-shadow: 0 0 4px 0 #f00;
}
borderを気にせず、box-shadowだけにすれば見栄えは良くなりますが、IE8をはじめ、box-shadowをサポートしていないブラウザーではなにも表示されません。
JavaScriptを加える
スタティックなコードができたので、スクリプトを加えます。まずは、基本的なaddEvent()関数です。
function addEvent(node, type, callback) {
if (node.addEventListener) {
node.addEventListener(type, function(e) {
callback(e, e.target);
}, false);
} else if (node.attachEvent) {
node.attachEvent('on' + type, function(e) {
callback(e, e.srcElement);
});
}
}
与えられたフィールドをチェックするか判定する関数で、フィールドが無効になっていないか、読み出し専用でないか、patternあるいはrequired属性があるかを調べます。
function shouldBeValidated(field) {
return (
!(field.getAttribute("readonly") || field.readonly) &&
!(field.getAttribute("disabled") || field.disabled) &&
(field.getAttribute("pattern") || field.getAttribute("required"))
);
}
冗長に見えるかもしれませんが、要素のdisabledとreadonlyプロパティは必ずしも属性の状態を反映しないため、最初の2つの条件も必要です。たとえば、Operaでは、ハードコード属性readonly="readonly"を持つ要素は、readonlyプロパティに対してundefinedを返します(ドットプロパティはスクリプトで設定された状態とのみ一致します)。
ユーティリティの準備ができたら、メインのチェック関数を定義します。メインの関数はフィールドをテストして、該当する場合、実際のバリデーションを実行します。
function instantValidation(field) {
if (shouldBeValidated(field)) {
var invalid =
(field.getAttribute("required") && !field.value) ||
(field.getAttribute("pattern") &&
field.value &&
!new RegExp(field.getAttribute("pattern")).test(field.value));
if (!invalid && field.getAttribute("aria-invalid")) {
field.removeAttribute("aria-invalid");
} else if (invalid && !field.getAttribute("aria-invalid")) {
field.setAttribute("aria-invalid", "true");
}
}
}
フィールドの入力が必須なのに値が入力されていない、もしくはパターンや値が指定されていて値がマッチしない場合、不正なフィールドとなります。
patternは正規表現の文字列形式を定義しているので、RegExpコンストラクターにその文字列を渡します。regexオブジェクトが生成され、値をテストできます。値が空でないか確かめるために事前にテストすることで、正規表現は空の文字列に対処する必要がなくなります。
フィールドの不正が確定したら、状態を示すaria-invalid属性で制御します。不正なフィールドにaria-invalid属性が設定されていなければ追加し、正当なフィールドなのにaria-invalid属性を持っていれば取り除きます。最後に、バリデーション関数をonchangeイベントにバインドして動かします。
addEvent(document, "change", function(e, target) {
instantValidation(target);
});
onchangeイベントが伝播することで動きますが(event delegationとして知られている技術を使います)、Internet Explorer 8以前ではonchangeイベントは伝播しません。簡単な回避策があるので無視せず対応しましょう。少しだけ複雑なコードになります。inputとtextarea要素のコレクションを取得し、for文で各フィールドに対してonchangeイベントを個別にバインドします。
var fields = [
document.getElementsByTagName("input"),
document.getElementsByTagName("textarea")
];
for (var a = fields.length, i = 0; i < a; i++) {
for (var b = fields[i].length, j = 0; j < b; j++) {
addEvent(fields[i][j], "change", function(e, target) {
instantValidation(target);
});
}
}
結論と今後の課題
以上で、フォームの即時バリデーション機能が追加されました。簡単で邪魔になりません。ユーザーがフォームを入力するときに、アクセシブルで視覚的なヒントを出してくれます。下のデモでチェックしてください。
紹介したスクリプトを実装すれば、完全なポリフィルとほとんど変わらなくなります。完全なポリフィルのスクリプトはこの記事の範囲外ですが、さらに開発を進めるための基本的な要素(フィールドをバリデーションすべきかどうかのテスト、フィールドがパターンにマッチするか、および/またはフィールドが必須かどうかのバリデーション、トリガーインベントのバインディング)はすべて含まれています。
正直に告白すると、そこまでやる価値があるかどうかは分かりません。すでにモダンなブラウザーで動くエンハンスメントを持っており、サーバーサイドのバリデーションを実装する以外に選択肢がない、patternとrequiredがサポートされているブラウザーが対象で送信前のバリデーションとしてそれらを使っているなら、新たにポリフィルを加える理由はあるのでしょうか。
(原文:Instant Form Validation Using JavaScript)
[翻訳:関 宏也/編集:Livit]