フォームコントロールのデフォルト値2009年01月19日 23時25分

WebKit のコントロール値キャッシュ対策

日本野望の会-Yabooo.org > Safari/Webkitのおせっかいキャッシュとその対策。」にコメントしたはずなのですが、いつの間にかコメントが消えているようなので推敲の上再掲。

上記のページで問題にしているのは、Safari において bfcache を無効にしていても、動的に追加したフォームコントロールの値がキャッシュされた値に書き換えられてしまうことです。これに対し、文書中のコントロールをすべて記憶し、文書のアンロード時にそれらの名前 (name 属性の値) を変更することで解決を図っています。

しかし、書き換えられるのは一時的な値のみで、コントロールのデフォルト値 (フォームをリセットしたときに設定される値) まで変化するわけではありません。ならば、コントロールが文書中に挿入されたときに、その値をデフォルト値に設定してやればすむことではないでしょうか。この考えに基づいた解決策が以下になります。

// bfcache を無効化しているときにこの問題は発生する。
window.onunload = window.onunload || function () {};

if (navigator.userAgent.indexOf("WebKit") != -1) {
  // 動的 (DOMContentLoaded イベント発生後) に
  // 追加されるコントロールのみを処理の対象とする。
  document.addEventListener("DOMContentLoaded", function () {
    var states = {
      radio:    "checked",
      checkbox: "checked",
      option:   "selected",
      OPTION:   "selected"
    };
    var defaults = {
      value:    "defaultValue",
      checked:  "defaultChecked",
      selected: "defaultSelected"
    };
    function resetState(control) {
      // option 要素ノードに type プロパティは存在しないので、
      // その場合は nodeName プロパティを参照する。
      var state = states[control.type || control.nodeName] || "value";
      control[state] = control[defaults[state]];
    }
    document.body.addEventListener("DOMNodeInserted", function (event) {
      var target = event.target;
      // target はテキストノードの場合もあるので、
      // tagName プロパティではなく nodeName プロパティを用いる。
      var name = target.nodeName.toLowerCase();
      if (name == "input" || name == "textarea" || name == "option") {
        resetState(target);
      } else if (target.nodeType == target.ELEMENT_NODE) {
        var controls = target.querySelectorAll("input, textarea, option");
        Array.prototype.forEach.call(controls, resetState);
      }
    }, false);
  }, false);
}

DOMContentLoaded イベントに対応しているのは Safari 3.1 以上なので、同じく Safari 3.1 以上で利用可能な Selectors API の querySelectorAll メソッドを使っています。Safari 3.0 にも対応させたいときは、DOMContentLoaded の使用を避け、querySelectorAll の代わりに getElementsByTagName なり XPath なりを使う必要があります。配列拡張の forEach メソッドに関しては Safari 3.0 でも使えるので問題ありません。

注意点として、以下のように value プロパティだけ設定したコントロールを追加すると、その値が空になってしまうことがあります。defaultValue プロパティも同時に設定するか、setAttribute メソッドで value 属性の値を設定しなければなりません。

// NG
var input = document.createElement("input");
input.type = "text";
input.value = "foo";
container.appendChild(input);
// OK
var input = document.createElement("input");
input.type = "text";
input.defaultValue = input.value = "foo";
container.appendChild(input);

コントロール値に関するプロパティ

上で触れたように、コントロールの値または状態を取得するプロパティには、現在の値を表すものとデフォルトの値を表すものがあります。これらのプロパティは DOM 2 HTML で以下のように定義されています。

value プロパティ (input 要素)
type 属性の値が "text"、"file"、"password" のいずれかならば、コントロールの現在の値を表す。このとき、このプロパティの値を変更しても value 属性の値は変更されない。
type 属性の値が "button"、"hidden"、"submit"、"reset"、"image"、"checkbox"、"radio" のいずれかならば、value 属性の値を表す。
defaultValue プロパティ (input 要素)
type 属性の値が "text"、"file"、"password" のいずれかならば、value 属性の値を表す。
value プロパティ (textarea 要素)
コントロールの現在の値を表す。このプロパティの値を変更しても要素の内容は変更されない。
defaultValue プロパティ (textarea 要素)
要素の内容を表す。
checked プロパティ (input 要素)
type 属性の値が "radio" または "checkbox" ならば、コントロールの現在の状態を表す。このプロパティの値を変更しても checked 属性の存在は変更されない。
defaultChecked プロパティ (input 要素)
type 属性の値が "radio" または "checkbox" ならば、checked 属性の存在を表す。
selected プロパティ (option 要素)
コントロールの現在の状態を表す。このプロパティの値を変更しても selected 属性の存在は変更されない。
defaultSelected プロパティ (option 要素)
selected 属性の存在を表す。

各ブラウザの実装は以下のようになっています。下の表で、A は要素の属性、P は現在値を表すプロパティ、DP はデフォルト値を表すプロパティを意味します。type が text、設定が A、取得が P の行は、type 属性の値が "text" である input 要素に対して、setAttribute メソッドを用いて value 属性の値を "foo" に設定し、その要素を文書中に挿入した後、value プロパティを用いて取得した値を表しています。type が option、設定が DP、取得が A の行は、option 要素に対して、defaultSelected プロパティの値を true に設定し、その要素を文書中に挿入した後、getAttribute メソッドを用いて取得した selected 属性の値です。(IE 8 RC1 はバージョン 8.0.6001.18344 時点のもの。いずれも標準モードの場合。)

フォームコントロールの値または状態の設定・取得結果一覧
type 設定 取得 IE 7 IE 8 RC1 Firefox 3 Safari 3.1 Opera 9.6
text A A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
P A "foo" "foo" null null null
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "" "" ""
DP A "" null "foo" "foo" "foo"
P "" "" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
password A A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
P A "foo" "foo" null null null
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "" "" ""
DP A "" null "foo" "foo" "foo"
P "" "" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
checkbox A A false "checked" "checked" "checked" "checked"
P false true true true true
DP false true true true true
P A false "" null null null
P false true true true true
DP false false false false false
DP A true "checked" "" "" "true"
P true true true true true
DP true true true true true
radio A A false "checked" "checked" "checked" "checked"
P false true true true true
DP false true true true true
P A false "" null null null
P false true true true true
DP false false false false false
DP A true "checked" "" "" "true"
P true true true true true
DP true true true true true
submit A A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
P A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
DP A "クエリ送信" "Submit Query" "foo" "foo" "foo"
P "クエリ送信" "Submit Query" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
reset A A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
P A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
DP A "リセット" "Reset" "foo" "foo" "foo"
P "リセット" "Reset" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
button A A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
P A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
DP A "" null "foo" "foo" "foo"
P "" "" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
image A A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
P A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
DP A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
file A A "" null "foo" "foo" "foo"
P "" "" "" "" ""
DP "" "" "foo" "foo" "foo"
P A "" null 例外発生 null null
P "" "" 例外発生 "" ""
DP "" "" 例外発生 "" ""
DP A "" null "foo" "foo" "foo"
P "" "" "" "" ""
DP "foo" "foo" "foo" "foo" "foo"
hidden A A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
P A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
DP A "" null "foo" "foo" "foo"
P "" "" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
textarea A A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
P A "foo" "foo" "" "" ""
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "" "" ""
DP A "" "" "foo" "foo" "foo"
P "" "" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
option A A true "selected" "selected" "selected" "selected"
P true true true true true
DP false false true true true
P A true "selected" null null null
P true true true true true
DP false false false false false
DP A true "selected" "" "" "true"
P true true true true true
DP true true true true true

IE 7 以下での getAttribute / setAttribute メソッドはプロパティの取得・設定と基本的に同じです。ファイル選択コントロールの value プロパティに値を代入しても単に無視されるだけですが、Firefox では例外が発生します。このことと、デフォルトの状態を表すプロパティを true に設定したときの論理型属性の値、そして IE を除けば、各ブラウザとも同じ結果になっています。

まとめ

フォームコントロールのデフォルトの値または状態を用いると、スクリプトが簡単に書けるようになることがあります。