HTMLのreadonly
属性はちょっと変わった属性で、名前と値の組ではなくて属性名だけで指定します1。この属性をattr
バインディングを使って設定しようとすると意外と難しかったりします。どう難しいか順を追ってみていきましょう。
まず、attr
バインディングを使ってreadonly
属性を変更する場合は、以下のように記述することになります。
<input type="text" data-bind="attr: { readonly: readonlyAttr }">
readonlyAttr
は属性値を返すobservableなプロパティです。が、これはいったい何を返せば良いのでしょうか。値など要らないわけですが、何かを返さないとattr
バインディングが満足してくれません。
ここでHTMLの仕様を記憶からひねり出します。実は属性名のみの指定はreadonly="readonly"
の略記です。ということはつまり、readonlyAttr
は以下のようにすればいいはずです。
self.readonlyAttr = ko.computed(function() {
return self.isReadonly() ? 'readonly' : '';
});
が、うまくいきません。isReadonly()
が偽の時もreadonlyが有効になってしまいます。なぜかというとreadonly=""
とreadonly="readonly"
は同じ意味に解釈されるからです2。これはつまりreadonly
を無効にするためには属性名ごと削除しないといけないということです。
なんだか急に難易度が上がりました。attr
バインディングのパラメータごと切り替えるみたいなことが果たしてできるのか、あるいは独自のバインディングを作ったほうが色々早いのではないか……
ここで独自バインディング実装の参考という名目でknockout.jsのソースコード探索に逃げてみましょう。するとattr
バインディングになにやら意味深なコメントがあるのが見つかります。
// To cover cases like "attr: { checked:someProp }", we want to remove the attribute entirely
// when someProp is a "no value"-like value (strictly null, false, or undefined)
// (because the absence of the "checked" attr is how to mark an element as not checked, etc.)
var toRemove = (attrValue === false) || (attrValue === null) || (attrValue === undefined);
if (toRemove)
element.removeAttribute(attrName);
なんと、属性値としてnull
、false
、undefined
を指定すると属性を削除してくれるようです。まさにこんな時のための機能があるではありませんか3。
ということで、このように指定すれば目指した挙動になりました。めでたしめでたし。
self.readonlyAttr = ko.computed(function() {
return self.isReadonly() ? 'readonly' : null;
});
ちなみにisReadonlyをそのまま使っても動作するようですが、その場合はtrue
の時にreadonly="true"
になってしまいます。属性値がtrue
の場合の挙動はHTMLの仕様からは見つけられませんでしたが、概ねreadonly
属性が有効だと解釈されるようです。