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属性が有効だと解釈されるようです。