【knockout.js】チェックボックスをビットフィールドにバインド【bitField-binding】

仕事にて、ビットフィールドをそのままチェックボックスにバインドしたい場面があったので作りました。
本家の [checked バインディング](https://github.com/knockout/knockout/blob/master/src/binding/defaultBindings/checked.js) を真似ています。

![KnockoutJS bifField binding](http://gifzo.net/bGRJYOP0y0.gif)

# やりたいこと

[ビットフィールド（ビットマスクを使ってフラグを合成した数値）](http://ja.wikipedia.org/wiki/%E3%83%93%E3%83%83%E3%83%88%E3%83%95%E3%82%A3%E3%83%BC%E3%83%AB%E3%83%89) の各フラグの ON/OFF をチェックボックスに対応づける

```
 0  1  0  1　←2進数) ビットフィールド (10進数→ 4)
[ ][+][ ][+]　←チェックボックス
 0  0  0  1　ビットマスク（flag1）
 0  0  1  0  ビットマスク（flag2）
 0  1  0  0  ビットマスク（flag3）
...
```

# 使い方

[checked バインディング](http://kojs.sukobuto.com/docs/checked-binding) の基本的な使い方とほぼ同じです。

```html:View
<input type="checkbox" data-bind="bitField: bitFieldValue, checkedFlag: flag1"/>
<input type="checkbox" data-bind="bitField: bitFieldValue, checkedFlag: flag2"/>
<input type="checkbox" data-bind="bitField: bitFieldValue, checkedFlag: flag3"/>
値: <span data-bind="text: bitFieldValue"> </span>
```

```js:ViewModel
ko.applyBindings({
    bitFieldValue: ko.observable(0),
    flag1: 1 << 0,  // = 0b0001
    flag2: 1 << 1,  // = 0b0010
    flag3: 1 << 2   // = 0b0100
});
```

## チェックボックス以外に one-way バインディングする方法

チェックボックス以外へバインドする場合、ViewModel → View への one-way バインディングのみ可能です。
特別な方法ではなく、ビット演算 `property & flag` をするだけです。

```html:View
<!-- visible binding の例 -->
<span data-bind="visible: bitFieldValue() & flag1">
    flag1 が ON です
</span>

<!-- Twitter Bootstrap で checkbox の見た目をトグルボタンにする例 -->
<label class="btn" data-bind="css: { active: bitFieldValue() & flag1 }">
    <input type="checkbox" style="display: none;"
             data-bind="bitFIeld: bitFieldValue, checkedFlag: flag1"/>
    FLAG1
</label>
```

# 動作サンプル

[KnockoutJS bitField binding : jsFiddle](http://jsfiddle.net/sukobuto/52UUY/5/)

Knockout ES5 プラグインを使ったバージョンはこちら
[KnockoutJS bitField binding (ES5) : jsFiddle](http://jsfiddle.net/sukobuto/qunnj6s9/)

曜日をビットフィールド化したものと、Linux のファイル権限をモデルにサンプルを作成しました。

# カスタムバインディング

下記コードを ko.applyBindings 呼び出し前に挿入すれば使えます。
knockout.js 以外に依存しているライブラリはありません。

```js:bitFIeld-binding
ko.bindingHandlers.bitField = {
	'after': ['value', 'attr'],
	'init': function (element, valueAccessor, allBindings) {
		var propWriters = allBindings()['_ko_property_writers'];

		function checkedFlag() {
			return allBindings['has']('checkedFlag')
				? ko.unwrap(allBindings.get('checkedFlag'))
				: element.value;
		}

		function updateModel() {
			// This updates the model value from the view value
			// It runs in response to DOM events (click) and changes in checkedFlag.
			var isChecked = element.checked,
				elemValue = checkedFlag();

			// When we're first setting up this computed, don't change any model state.
			if (!shouldSet) {
				return;
			}

			var modelValue = valueAccessor();
			var unwrapped = ko.unwrap(modelValue);
			if (oldElemValue !== elemValue) {
				if (isChecked) {
					if (oldElemValue !== undefined) {
						unwrapped &= ~oldElemValue;
					}
					unwrapped |= elemValue;
				}
				oldElemValue = elemValue;
			} else {
				if (isChecked) {
					unwrapped |= elemValue;
				} else {
					unwrapped &= ~elemValue;
				}
			}
			if (ko.isObservable(modelValue)) {
				modelValue(unwrapped);
			} else if (propWriters['bitField']) {
				propWriters.bitField(unwrapped);
			} else {
				valueAccessor(unwrapped);
			}
		}

		function updateView() {
			// This updates the view value from the model value.
			// It runs in response to changes in the bound (checked) value.
			var modelValue = ko.unwrap(valueAccessor());

			element.checked = (modelValue & checkedFlag()) > 0;
		}

		if (element.type !== "checkbox") return;

		var oldElemValue = checkedFlag(),
			shouldSet = false;

		// Set up two computeds to update the binding:

		// The first responds to changes in the checkedFlag value and to element clicks
		ko.computed(updateModel, null, { disposeWhenNodeIsRemoved: element });
		ko.utils.registerEventHandler(element, "click", updateModel);

		// The second responds to changes in the model value (the one associated with the bitField binding)
		ko.computed(updateView, null, { disposeWhenNodeIdRemoved: element });

		shouldSet = true;
	}
};
ko.expressionRewriting._twoWayBindings['bitField'] = true;
```