14
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

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

最近は Knockout に Punches, ES5 などの強力なシンタックスプラグインを全部のせし、言語は TypeScript、しめに WebPack などで武装しつつプロダクトコードを書いています。規模が大きくなっても複雑化しないのでわりと定時で帰れます。

Knockout ES5 の受難

KO の observablesetter 関数として機能します。これが 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 というオブジェクトが生えてきます。

79cc054d86ad2a3510c0655f1c21277a.png

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

14
16
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?