[Knockout.js基本編]View側での関数の使い方まとめ

2014年06月25日

カテゴリー:

Knockout.jsでは、View(HTML)側のバインディングに対してViewModelオブジェクトのプロパティだけではなく、JavaScriptの関数を用いることもできます。例えばtextバインディングに関数の実行結果を渡したり、clickバインディングにイベントハンドラを関連付けたりすることが可能です。

今回は、そのようなView側での関数の使い方についてまとめてみました。

関数の実行結果をバインディングに渡す

■引数なしでの呼び出し

まずは最も基本的なケースで、ViewModelオブジェクトに属する関数(ViewModelオブジェクトのメソッド)を引数無しで呼び出し、その実行結果をバインディングに渡す例です。特に注意する点も無く、通常の関数呼び出しと同じです。

下記のコード例では画面に「Taro」と表示されます。

<p data-bind="text: getName()"></p>
var viewModel = {
        getName: function () {
            return 'Taro';
        }
    };

ko.applyBindings(viewModel);

■引数を渡して呼び出し

次に関数に引数を渡す場合のコードですが、この場合も通常の関数呼び出しと同じなので、特別注意することはありません。

下記のコード例では画面に「My name is Taro」と表示されます。

<p data-bind="text: getName('Taro')"></p>
var viewModel = {
        getName: function (name) {
            return 'My name is ' + name;
        }
    };

ko.applyBindings(viewModel);

■ViewModel外の関数の呼び出し

View側で用いる関数は、必ずしもViewModelオブジェクトに属する関数である必要はありません。次のコード例の様に、グローバルスコープ(windowオブジェクト)から辿ることができる関数ならばView側でも使用可能です。

<p data-bind="text: getName('Taro')"></p>
var getName = function (name) {
        return 'My name is ' + name;
    };

window.onload = function () {
    ko.applyBindings();
};

本来、ko.applyBindingsメソッドにはViewModelオブジェクトが渡されますが、上記コード例の場合、ViewModelオブジェクトを一切使用していないので、6行目のko.applyBindingsメソッドは引数無しで実行しています。だとすると、そもそも6行目のko.applyBindings()は不要に思えるかもしれません。
ですが、ko.applyBindings()を呼び出さないとView側でバインディングが有効にならないので、ViewModelオブジェクトの有無にかかわらずko.applyBindingsメソッドは必ずコールする必要があります。

ループ内でViewModelに属する関数を使う際の注意

通常、View側からViewModelオブジェクト(ko.applyBindingsメソッドの引数として渡したオブジェクト)を参照する際、ViewModelオブジェクトそのものが参照の基点となります。
しかし、ループ(foreachバインディング)の内側でViewModelオブジェクトを参照する場合、「現在のアイテム(foreachバインディングが参照している配列の各要素)」が参照の基点になるということに注意する必要があります。

例えば、次の様なViewModelオブジェクトがあったとします。

var viewModel = {
        names: ['taro', 'jiro', 'saburo'],
        showName: function (name) {
            return 'My name is ' + name;
        }
    };

ko.applyBindings(viewModel);

上記のviewModel.showNameメソッドを呼び出す場合、foreachループ以外の場所からは次の様に呼び出すことができます。ここまでは何も問題ありませんね。

<body>
  <p data-bind="text: showName('taro')"></p>
</body>

しかし、次のコードではforeachループ内でshowName関数に現在のアイテム(namesプロパティの各要素)を渡そうとしていますが、これはエラーになります

<body>
  <ul data-bind="foreach: names">
    <li data-bind="text: showName($data)"></li>
  </ul>
</body>

foreachバインディングの内側ではviewModelオブジェクトそのものではなく、現在のアイテム(上記のコード例のviewModel.namesプロパティの各要素)が参照の基点になるので、上記の例だとtaro.showNameなどという存在しないメソッドを呼び出すことになってしまうので、これはエラーになります。

ループの内側からshowNameメソッドを正しく呼び出すためには、次のコードの様に「バインディング・コンテキスト」を使用し、現在のアイテムからの相対パス(パスという表現が正しいかどうかは分かりませんが)、もしくは絶対パスでメソッドを指定します。

<ul data-bind="foreach: names">
  <li data-bind="text: $root.showName($data)"></li>
</ul>

または…

<ul data-bind="foreach: names">
  <li data-bind="text: $parent.showName($data)"></li>
</ul>

バインディング・コンテキストについては、以下で詳しく解説されています。
ドキュメント | Knockout.js 日本語ドキュメント

バインディングにイベントハンドラを追加

バインディングにイベントハンドラを追加する場合は、バインディングに対して関数の実行結果を渡すのではなく、関数そのもの(関数オブジェクト)を渡す必要があります。

<button data-bind="click: myHandler">Click me</button>

<!--
※間違った例
<button data-bind="click: myHandler()">Click me</button>

これだと、myHandler関数の実行結果を
clickバインディングに渡すことになるのでエラーになります。

clickバインディングには、関数そのものを渡す必要があるので、
「myHandler()」ではなく正しくは「myHandler」です。
-->
var viewModel = {
        myHandler: function () {
            alert('hoge');
        }
    };

ko.applyBindings(viewModel);

上記サンプルは、button要素をクリックした場合にイベントハンドラ(myHandler)が呼び出されるコード例です。
ボタンをクリックすると「hoge」とアラート表示されます。

■イベントハンドラに自動的に渡されるパラメータ

knockout.jsではイベントハンドラを実行すると自動的に2つのパラメータが渡されます。
最初のパラメータはViewModelオブジェクトの「現在のアイテム」で、2つ目のパラメータはイベントオブジェクトです。


「現在のアイテム」とは、先ほどループ内での関数の呼び出しの項でも説明しましたが、通常はko.applyBindings()メソッドの引数として渡したオブジェクトを指します。もしイベントハンドラがループ(foreachバインディング)の内側で呼び出された場合は、ループ対象である配列の各要素を指します。これにより、例えば「ループ内のどのボタンが押されたのか」といったことを特定することができます。

イベントオブジェクトは発生したイベントに関する様々な情報を格納しています。このオブジェクトを使えば、例えば「どのキーが押されたのか?」といったことを判別することができます。また、イベントバブリングの制御を行うこともできます。

これら2つのパラメータをハンドラ内で利用するには、ハンドラ内でargumentsキーワードを使うか、以下の様に関数の引数に明示的にパラメータを指定することで利用することができます。

<button data-bind="click: myHandler">Click me</button>
var viewModel = {
        myHandler: function (data, event) {
            //dataには現在のアイテム、eventにはイベントオブジェクトが格納されます。

            //ハンドラ内の処理・・・

        }
    };

ko.applyBindings(viewModel);

イベントハンドラに任意の引数を渡す2つの方法

イベントハンドラ内で「現在のアイテム」と「イベントオブジェクト」へのアクセスの仕方は紹介しましたが、これ以外に任意のパラメータをイベントハンドラへ渡すことも可能です。

バインディングにイベントハンドラを追加するには、バインディングに対して「関数そのもの」を渡す必要がある、と説明しましたね。そのため、例えばdata-bind="myHandler('任意の引数')"の様な書き方は出来ません。これだと「関数そのもの」ではなく、「関数の実行結果」をバインディングに渡してしまうからです。

以下に紹介する2つの方法を利用すれば、任意の引数を渡しつつ、関数そのものをバインディングに渡すことが可能です。いずれの方法もknockout.js固有の機能などではなく、JavaScriptがもつ基本的な機能をうまく利用してこの問題を解決しています。

■無名関数を使う方法

任意の引数を渡したいイベントハンドラを内包(ラップ)した無名関数を作成し、それをバインディングに渡すことで、イベントハンドラに対して任意の引数を渡すことができます。
もっとも簡単で分かりやすい方法ですが、View側で関数を定義することになってしまうのが短所とも言えます。

<button data-bind="click: function (data, event) {myHandler('引数1', '引数2', ... ,data, event); }">Click me</button>

■bind関数を使う方法

JavaScriptの関数にはbindメソッドというものが存在します。これを利用することで任意の引数をイベントハンドラに渡すことも可能です。
bindメソッドは、元となる関数を指定されたパラメータと関連付け(=バインド)し、新たな関数を返すメソッドです。
このメソッドの第1引数は、新たな関数内のthisキーワードとして使用する値を渡します。第2引数以降は実際に渡したい任意のパラメータを指定します。

<button data-bind="click: myHandler.bind($data, '引数1', '引数2', ...)">Click me</button>
var viewModel = {
        myHandler: function (param1, param2) {
            console.dir(this);  //$dataの内容を出力
            console.dir(param1);  //引数1の内容を出力
            console.dir(param2);  //引数2の内容を出力
        }
    };
 
ko.applyBindings(viewModel);

【参考】

ドキュメント | Knockout.js 日本語ドキュメント