KnockoutのObservableは非常に強力なデータバインディングを提供してくれますが、動作をカスタマイズしたい場合はどうすればよいでしょうか?KnockoutJSではcomputedやobservable,observableArraysに関数を追加したり、エクステンダーを追加することで動作を拡張できるようになっています。エクステンダーを使用することで新しいobservableの代わりにカスタマイズしたcomputed(pureComputed)を使用して動作を大きく変更したり、デフォルトのobservableを使いつつ 独自の拡張処理を(subscribeなどにより)追加できます。

今回の記事では observable を エクステンダーで拡張して任意の正規表現にマッチするテキストのみを受け入れる computed オブジェクトを使用するように変更します。また observableに関数を追加して監査ログを出力する機能をサンプル実装します。

動作検証は KnockoutJS 3.3 で行いました。

1. サンプルプログラム

さっそくですがサンプルソースを次のように実装してみました。サンプルでは、 regex というエクステンダーを作成しています。引数で受け取った 正規表現にマッチする場合のみ入力された文字列を受け入れるようにカスタマイズされた pureComputed を使うようにしています。pureComputedの処理では参照は既定の observable の参照をそのまま返し、書き込みの場合は、入力をチェックし問題がなければ 入力された値をオリジナルのobservableに設定するようにしています。入力をキャンセルしてオリジナルの値を保持する場合に関係のあるオブジェクトのUIの変更通知が発生するように notifySubscribers の呼び出しと notifyエクステンダーを使って常に変更通知が行われるようにしています。またenableAudit 関数を追加して 監査用のログを有効化できるようにしています。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Data Binding with KnockoutJS</title>
</head>
<body>
    <textarea data-bind="textinput: text"></textarea><br/>
    入力された値<span data-bind="text:text"></span><br/>

    <input data-bind="value: text2" />
    <script type="text/javascript" src="/Scripts/knockout-3.3.0.js"></script>
    <script type="text/javascript">
        ko.extenders.regex = function (target, option) {
            var regex = option;
            var result = ko.pureComputed(
                    {
                        read: target,
                        write: function (newValue) {
                            var current = target();

                            if (regex.test(newValue)) {
                                target(newValue);
                            } else {
                                if(newValue !== current)
                                    target.notifySubscribers(current);
                            }
                        }
                    }).extend({ notify: 'always' });
            return result;
        }
        ko.observable.fn.enableAudit = function (prefix) {
            this.subscribe(function (newValue) {
                console.log(prefix + ':' + newValue);
            })
        }
    </script>
    <script type="text/javascript">
        function ViewModel() {
            var self = this;
            self.text = ko.observable(0).extend({ regex: /^[0-9]*$/ });
            self.text2 = ko.observable();
            self.text2.enableAudit('text2');
        }
        ko.applyBindings(new ViewModel());
    </script>
</body>
</html>

簡単ですがサンプルプログラムは以上です。

公式のサイトのドキュメントのページにも extender (エクステンダー)使って Observable オブジェクトを変更する様々なサンプルが提供されていますのでこちらもご参照ください。