KnockoutJSで、整形された数値入力をする方法

こんなやつ： http://jsfiddle.net/MKGaru/xd9vs2zp/

------------------------------------------------

入力フォームで数値を入力させる際、
　小数点以下は　何位まで～　とか
　３桁ごとに、　カンマで区切りたい　とか
　正の整数だけにしたい　とか
いろいろな要求が出てくることがある。



HTML5の input type=number　とvalidationだけでは対応できないことが多いし、そもそも対応ブラウザが少なすぎる。
ほしかった機能次のとおり。

機能
--------
* IMEの状態に関係なく、半角数値として入力
* 小数点何位まで扱うか　、　またその際の値丸め（四捨五入等）の設定
* ３桁毎の区切り文字の指定
* 小数点の区切り文字の指定
* 正の数のみ　または　負の数のサポート　の設定
* prefixの指定
* suffixの指定
* 0　と　未入力の区別

---------------------

これを実装してみた


依存
----------------
* [knockout.js](http://knockoutjs.com/)
* [knockout-es5](http://blog.stevensanderson.com/2013/05/20/knockout-es5-a-plugin-to-simplify-your-syntax/)  (単純に[ko.computed](http://knockoutjs.com/documentation/computed-writable.html)のreadとwriteに書き換えれば不要にできるかも）
* [sugar.js](http://sugarjs.com/)  
(サンプルでは[knockout.punches](http://mbest.github.io/knockout.punches/)も利用)



仕様
----------------
Objectの、
　生数値用のpropertyに、
　整形数値用のformattedProperty を追加する。
　formattedPropertyに整形された数字をsetすると　生数値がpropertyにsetされる。
　formattedPropertyを参照すると、整形されたpropertyをgetする

空文字を formattedProperty に設定すると、　null が propertyにsetされる。
propertyがnullのとき、formattedPropertyは　空文字になる。

使用方法
-----------------
* numberFormattedProperty(`プロパティの属するオブジェクト`,"`プロパティ名`",{`オプション`});

* オプション 

|key       |default|description         |
|:--------:|:-----:|:------------------:|
|prefix    |''     | 頭に付与する文字列   |
|suffix    |''     | 尻に付与する文字列   |
|place     |0      | 小数点位置          |
|thousands |','    | ３桁毎の区切り文字   |
|decimal   |'.'    | 小数点区切り文字     |
|allowMinus|false  | 負の数の許可         |
|round     |'round'| 丸め方式 (round , ceil , floor  (四捨五入, 切上げ, 切り捨て))    |



例えば、vmのpriceというプロパティを、通貨入力用に拡張する場合
（ここでいう通貨入力とは、

* prefixに円マークを指定して、
* 正の整数で、
* ３桁毎に　カンマ区切り

を期待する場合）



```js
ko.track( vm );
```
で、Objectをtrackした後に、


```js
numberFormattedProperty(vm,"price",{prefix:'￥'});
```

で、vm の `price`プロパティを整形したプロパティ`formattedPrice`をvmに追加する

サンプル
-----------------
```html
<dl>
    <dt>Formatted Price</dt>
    <dd><input type="text" class="number" data-bind="textInput:formattedPrice" /></dd>
    
    <dt>Raw Price</dt>
    <dd>{{price}}</dd>
    
    <dt>Formatted Quantity</dt>
    <dd><input type="text" class="number" data-bind="value:formattedQuantity" /></dd>
    
    <dt>Raw Quantity</dt>
    <dd>{{quantity}}</dd>
</dl>

```

```js
function VM(){
    var vm = this;
    vm.price=130;
    vm.quantity=2.15;
    ko.track(vm);
    numberFormattedProperty(vm,"price",{prefix:'￥'});
    numberFormattedProperty(vm,"quantity",{suffix:'kg',place:2});
    
};
ko.applyBindings(new VM());
```

http://jsfiddle.net/MKGaru/xd9vs2zp/

data-bindの方法は、[value](http://knockoutjs.com/documentation/value-binding.html)や[knockout3.2](http://blog.stevensanderson.com/2014/08/18/knockout-3-2-0-released/)から利用可能な[textInput](http://knockoutjs.com/documentation/textinput-binding.html)どちらでも利用可能だが、
　正の整数のみの　入力は、textInputだと入力中の文字が即時反映されるのでうれしい場合もあるが、
入力毎（キーイベント毎）に正規表現による数値判定が行われるため、
　`3.14`と入力しようと思って
　`3.`まで入力した段階で数値化されて
　`3`に戻されてしまうため具合が悪い。
なので、負の数 や 小数などを扱う場合には、valueバインディングを推奨
(value は　フォーカスが外れてから判定される　）

実装
-------

```js
function numberFormattedProperty(model, prop, option) {
    var _default = {
        prefix: '',
        suffix: '',
        place: 0,
        thousands: ',',
        decimal: '.',
        allowMinus: false,
        round: 'round' // round | ceil | floor  (四捨五入, 切上げ, 切り捨て)
    };
    option = ko.utils.extend(_default, option);
    ko.defineProperty(model, "formatted" + prop.camelize(), {
        get: function () {
            var value = ko.unwrap(model[prop]);
            if (value == null)
                return "";
            return option.prefix + value[option.round](option.place).format(option.place, option.thousands, option.decimal) + option.suffix;
        },
        set: function (value) {
            model[prop] = NaN; //force mutant event
            var replacer = new RegExp('[^' + (option.allowMinus ? '\\-' : '') + '0-9\\.]' + (option.place > 0 ? '||[' + RegExp.escape(option.decimal) + ']' : ''), 'g'); // 丸めるケタが小数点以下なら、　小数点文字の入力を許可
            if (value == '')
                model[prop] = null;
            else
                model[prop] = (+value.hankaku().replace(replacer, ''))[option.round](option.place);
        }
    });
};
```
<div style="text-align:right">＊ぶっちゃけ内部処理はsugarjs依存</div>