Knockout ES5 に対応したカスタムバインディングを書く作法

- これを知っておけば `observable` じゃないバインディングフィールドも書き換えられる！
- `ko.expressionRewriting._twoWayBindings['binding_name'] = true;` これを追記
- `allBindingAccessor()['_ko_property_writers']` を使ってフィールド書き換え

最近は Knockout に [Punches](http://mbest.github.io/knockout.punches/), [ES5](http://kojs.sukobuto.com/tips/knockout-es5) などの強力なシンタックスプラグインを全部のせし、言語は TypeScript、しめに WebPack などで武装しつつプロダクトコードを書いています。規模が大きくなっても複雑化しないのでわりと定時で帰れます。

## Knockout ES5 の受難

KO の `observable` は `setter` 関数として機能します。これが View → ViewModel の変更通知を実現してくれるので、ドキュメントを読む限り基本的にカスタムバインディングは `observable` を前提に作るものだ、と思いますよね。
ところが ES5 ではプロパティが本当の意味で「プロパティ」ですから、ViewModel → View への 1way バインディングとなってしまいます。

たとえばこんなカスタムバインディングだった場合。(value binding の劣化コピーです)

```js
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;
		});
	}
}
```

```html
<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` を使ったエレガントな世界が台無しです。

```html
<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` というオブジェクトが生えてきます。

![79cc054d86ad2a3510c0655f1c21277a.png](https://qiita-image-store.s3.amazonaws.com/0/25145/00184525-954c-cbb1-bb24-0b796c265b6a.png)

### `allBindings()._ko_property_writers`

これはその名の通りプロパティを書き換えるための `setter` を寄せ集めたものです。
allBindings に生えているのもミソで、別のバインディングハンドラからもアクセスできるようになっています。
これを使って以下のように書き換えます。

```js
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 に対応させたので良かったらご参照ください。

- [【knockout.js】チェックボックスをビットフィールドにバインド【bitField-binding】](http://qiita.com/sukobuto/items/8ddb14c41870dc08605a)
- [キャレット位置をバインド&キャレット位置に文字列を挿入「caret binding」](http://qiita.com/sukobuto/items/30232a833a0f06e421d8)