- これを知っておけば
observable
じゃないバインディングフィールドも書き換えられる! -
ko.expressionRewriting._twoWayBindings['binding_name'] = true;
これを追記 -
allBindingAccessor()['_ko_property_writers']
を使ってフィールド書き換え
最近は Knockout に Punches, ES5 などの強力なシンタックスプラグインを全部のせし、言語は TypeScript、しめに WebPack などで武装しつつプロダクトコードを書いています。規模が大きくなっても複雑化しないのでわりと定時で帰れます。
Knockout ES5 の受難
KO の observable
は setter
関数として機能します。これが View → ViewModel の変更通知を実現してくれるので、ドキュメントを読む限り基本的にカスタムバインディングは observable
を前提に作るものだ、と思いますよね。
ところが ES5 ではプロパティが本当の意味で「プロパティ」ですから、ViewModel → View への 1way バインディングとなってしまいます。
たとえばこんなカスタムバインディングだった場合。(value binding の劣化コピーです)
ko.bindingHandlers['customValue'] = {
init: function (element, valueAccessor, allBindings) {
var suspend = false;
// ViewModel 更新する関数
function updateModel() {
suspend = true;
var value = element.value;
var modelValue = valueAccessor();
if (ko.isObservable(modelValue)) modelValue(value);
}
// テキストボックスが変更されたら ViewModel 更新
element.onchange = updateModel;
// ViewModel が更新されたらテキストボックスに反映
ko.computed(function() {
if (suspend) {
suspend = false;
return;
}
var modelValue = ko.unwrap(valueAccessor());
element.value = modelValue;
});
}
}
<input type="text" data-bind="customValue: hoge"/> ←変更しても
<span data-bind="text: hoge"> </span> ←反映されない
<script>
function ExampleViewModel() {
this.hoge = "サンプル";
ko.track(this);
}
var vm = new ExampleViewModel();
ko.applyBindings(vm);
setTimeout(function() {
vm.hoge = "プログラムから変更"; // ←これは反映される (1way)
}, 2000);
</script>
場当たり的な対処方法
そういう場合 ko.getObservable
を使って、observable
な代理プロパティを作ることで対処したりしていました。
せっかく getter/setter
を使ったエレガントな世界が台無しです。
<input type="text" data-bind="customeValue: _hoge"/>
<span data-bind="text: hoge"> </span>
<script>
function ExampleViewModel() {
this.hoge = "サンプル";
ko.track(this);
// 代理プロパティを設置
this._hoge = ko.getObservable(this, 'hoge');
}
var vm = new ExampleViewModel();
ko.applyBindings(vm);
</script>
カスタムバインディングを ES5 に対応させる書き方
でもせっかくだから最後までシンプルにバインド書きたいじゃないですか。別名の observable
を使い分けるなんてことはしたくないわけです。で、デバッグしてたらたまたまこの方法を見つけました。Knockout のコードをよく読んでいれば、もっと早くわかったと思います。。。
ko.expressionRewriting._twoWayBindings
これはバインディングハンドラごとに設定するフラグの一種で、以下のように true
をセットすることで...
ko.bindingHandlers['customValue'] = {
// 省略
};
ko.expressionRewriting._twoWayBindings['customValue'] = true;
allBindings()
オブジェクトに _ko_property_writers
というオブジェクトが生えてきます。
allBindings()._ko_property_writers
これはその名の通りプロパティを書き換えるための setter
を寄せ集めたものです。
allBindings に生えているのもミソで、別のバインディングハンドラからもアクセスできるようになっています。
これを使って以下のように書き換えます。
ko.bindingHandlers['customValue'] = {
init: function (element, valueAccessor, allBindings) {
var suspend = false,
propWriters = allBindings()['_ko_property_writers'];
function updateModel() {
suspend = true;
var value = element.value;
var modelValue = valueAccessor();
if (ko.isObservable(modelValue)) {
// プロパティが observable ならそのまま書き換え
modelValue(value);
} else if (propWriters && propWriters.customValue) {
// ライターがあればそれを使って書き換え
propWriters.customValue(value);
}
}
element.onchange = updateModel;
ko.computed(function() {
if (suspend) {
suspend = false;
return;
}
var modelValue = ko.unwrap(valueAccessor());
element.value = modelValue;
});
}
};
ko.expressionRewriting._twoWayBindings['customValue'] = true;
これで observable じゃない単なるメンバ変数なら View → ViewModel の 1way バインディング、 ES5 のアップグレード済みプロパティなら 2way バインディングが可能となります。
作例
例として、今までに公開していたカスタムバインディングをこの方法で ES5 に対応させたので良かったらご参照ください。