SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

TypeScriptでknockout.jsを使う・その2~基本的な使い方~

前回の手順で、knockout.jsをTypeScriptから使う準備ができました。

今回は、knockoutの機能を使って、様々なデータバインディングを行ってみたいと思います。

knockout.jsで中心となるクラス/メソッドなど

knockout.jsで頻繁に使うクラスをXAML系言語と比較してみました。
以下の表のような対応関係になるかと思います。

knockout.jsとWPFの型/メソッドなどの比較
knockout.jsの型/メソッド WPFで似た役割のもの
KnockoutObservalbe<T>型 BindableBase/NotificationObjectなどといった、INotifyPropertyChangedを実装する任意のクラス
KnockoutObservableArray<T>型 ObservableCollection<T>型
KnockoutComputed<T>型 標準のWPFでは、対応する概念なし。
ko.applyBindings() this.DataContext=new ○○ViewModel()、のようなデータコンテキスト設定処理。

基本的なデータバインディング

さっそくknockout.jsを使っていきたいと思います。
最初は、前回のサンプルコードと似たコードで、基本的な部分を見てみたいと思います。

ViewModelの作成

データバインディングを行うためのViewModelを作成します。
双方向データバインドができるようなプロパティを作るには、KnockoutObservable<T>型を使用します。
ここではKnockoutObservable<string>型のメンバーを持つ、AppViewModelというクラスを作りました。

class AppViewModel {
    message: KnockoutObservable<string> = ko.observable<string>("Hello World!!");
}

次にwindowのonloadイベントで、このAppViewModelクラスのインスタンスVMとしてセットします。
(※バインド対象のDOM要素が構築されてからapplyBindする必要があるので、onloadイベントで行ってます。)

window.onload = () => {
    var viewModel = new AppViewModel();
    ko.applyBindings(viewModel);
};

Viewの作成

次に表示する画面をhtmlで作成します。
VMとバインドして表示したい部分にdata-bind属性を付けていきます。
以下では、bodyタグの下に<p>タグを作り、data-bind属性を指定してtextバインディングVMのmessageプロパティをバインドしています。

<body>
    <h1>TypeScript HTML App</h1>
    <p data-bind="text: message"></p>
</body>


これで実行してみると、<p>タグの部分にHello World!!と表示されることが確認できます。
f:id:minami_SC:20150222235854p:plain


ここまでのコードは以下の通り。

index.html
<!DOCTYPE html>

<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>TypeScript HTML App</title>
    <link rel="stylesheet" href="app.css" type="text/css" />
    <script src="Scripts/knockout-3.2.0.js"></script>
    <script src="app.js"></script>
</head>
<body>
    <h1>TypeScript HTML App</h1>
    <p data-bind="text: message"></p>
</body>
</html>
app.ts
/// <reference path="scripts/typings/knockout/knockout.d.ts" />

class AppViewModel {
    message: KnockoutObservable<string> = ko.observable<string>("Hello World!!");}

window.onload = () => {
    var viewModel = new AppViewModel();
    ko.applyBindings(viewModel);
};

複数のタグからバインドしてみる

次は、このmessageプロパティをinputタグにもバインドし、Viewからプロパティの変更を行ってみます。また、同じプロパティにバインドしているpタグ側の値も、一緒に更新されることを確認します。

bodyタグの下に、以下のinputタグを追加します。

<input data-bind="value: message, valueUpdate :'afterkeydown'" /></body>

ここでは、valueバインディングを用いています。
valueUpdateという追加パラメータで、'afterkeydown'という値を指定しています。
こうすると、テキストボックス内でキーを押したらすぐに、VM側のプロパティが更新されるようになります。(これを付けてないと、フォーカスが外れたタイミングでの同期となります。)

こんな風に、inputタグでの入力内容にpタグの表示内容が追従します。
f:id:minami_SC:20150222235903p:plain


KnockoutComputed<T>型を使ってみる

この型を使うと、複数のKnockoutObservable型のプロパティに依存して値が決定するプロパティを定義することができます。

↓では、string型を持つKnockoutObservableとして、firstNameとlastNameというプロパティを作り、「firstName + lastName」の値を返すプロパティを、KnockoutComputedとして作成しています。
KnockoutComputed型のプロパティを作るには、ko.computed<T>()メソッドに引数として、評価する内容を決定する関数を渡します。
こうしておくと、ko.computed()メソッドに渡した評価関数内のKnockoutObservableの値が変わると、KnockoutComputedのプロパティ自身の値も一緒に更新されるようになります。

また第二引数には、この評価関数内でthisとして扱われるものを渡します。ここでは、このVMインスタンスのthisを関数内でもそのまま利用したいので、第二引数にthisを渡します。
この辺は、JavaScriptのthisの扱い方の面倒なポイントですね。

        this.fullName = ko.computed<string>(function() {
            return this.firstName() + " " + this.lastName();
        }, this);

しかしココはTypeScriptならではの方法でもう少し簡潔に書いてみたいと思います。
以下のようにアロー関数で評価関数を作ると、関数内のthisはそのまま元のクラスのthisを表すので、以下のように書くことができます。

this.fullName = ko.computed<string>(() => this.firstName() + " " + this.lastName());


実行してみると、以下のようにpタグの表示内容が、firstName/lastName両方のプロパティに依存して更新されていくことが確認できます。
f:id:minami_SC:20150222235949p:plain
コードは以下のとおりです。

index.html
<!DOCTYPE html>

<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>TypeScript HTML App</title>
    <link rel="stylesheet" href="app.css" type="text/css" />
    <script src="Scripts/knockout-3.2.0.js"></script>
    <script src="app.js"></script>
</head>
<body>
    <p>First name: <input data-bind="value: firstName, valueUpdate :'afterkeydown'" /></p>
    <p>Last name: <input data-bind="value: lastName, valueUpdate :'afterkeydown'" /></p>
    <p data-bind="text: fullName"></p>
</body>
</html>
app.ts
/// <reference path="scripts/typings/knockout/knockout.d.ts" />

class AppViewModel {
    firstName: KnockoutObservable<string>;
    lastName: KnockoutObservable<string>;

    fullName: KnockoutComputed<string>;

    constructor() {
        this.firstName = ko.observable<string>("taro");
        this.lastName = ko.observable<string>("yamada");

        this.fullName = ko.computed<string>(() => this.firstName() + " " + this.lastName());

        // ↓functionで匿名関数を渡す場合は、ko.computedの第二引数にthisを渡す必要があるので注意
        //this.fullName = ko.computed<string>(function() {
        //    return this.firstName() + " " + this.lastName();
        //}, this);
    }
}

window.onload = () => {
    var viewModel = new AppViewModel();
    ko.applyBindings(viewModel);
};

KnockoutObservableArrayでリストを扱う

今度は、バインド可能なリスト要素を作るために使用する、KnockoutObservableArrayです。

ViewModelの準備

ko.observableArray<T>()メソッドで KnockoutObservableArray型のインスタンスを作ります。
また、push/pop/remove/removeAllなどのメソッドなどで、リストを操作することができます。

// リストに格納される型の定義
class Person {
    constructor(public name: string, public age: number) {
    }
}

// ↓プロパティ定義
list: KnockoutObservableArray<Person>;

// インスタンス生成
this.list = ko.observableArray<Person>();

// 要素の追加
this.list.push(new Person("taro", 18));

View側でのforeachバインディング

view側では、以下のようにforeachバインドを用います。
繰り返し生成したい要素の親要素で、「data-bind="foraech:・・・」という属性を付加します。
すると、バインドしたKnockoutObservableArrayの要素数分だけ、子要素(この場合liタグ)が繰り返し生成されます。
foreach内部の子要素タグ内では、$dataでリスト内の各要素を指し示すことができます。

    <ul data-bind="foreach: list">
        <!-- ↓ここから -->
        <li>
            <!-- foreach内部では、$dataで現在のアイテムを示すことができる。 -->
            <strong data-bind="text: $data.name"></strong>さん
            <!-- ↓$dataは省略できる -->
            (<span data-bind="text: age"></span>才)
        </li>
        <!-- ↑ここまでの範囲が、リストの要素数分だけ繰り返される -->
    </ul>

XAMLで開発する時の、ItemsControl系のバインドとDataTemplateの定義をまとめて行っているような感じです。


実行すると、以下のようにliタグの箇所が、リストの要素数分生成されているのが確認できます。
f:id:minami_SC:20150222235958p:plain
コードは以下の通り。

index.html
<!DOCTYPE html>

<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>TypeScript HTML App</title>
    <link rel="stylesheet" href="app.css" type="text/css" />
    <script src="Scripts/knockout-3.2.0.js"></script>
    <script src="app.js"></script>
</head>
<body>
    <ul data-bind="foreach: list">
        <li>
            <strong data-bind="text: name"></strong>さん
            (<span data-bind="text: age"></span>才)
        </li>
    </ul>
</body>
</html>
app.ts
/// <reference path="scripts/typings/knockout/knockout.d.ts" />

class Person {
    constructor(public name: string, public age: number) {
    }
}

class AppViewModel {
    list: KnockoutObservableArray<Person>;

    constructor() {
        this.list = ko.observableArray<Person>();
        this.list.push(new Person("taro", 18));
        this.list.push(new Person("yamada", 20));
        this.list.push(new Person("hoge", 25));
    }
}

window.onload = () => {
    var viewModel = new AppViewModel();
    ko.applyBindings(viewModel);
};

「要素数分だけDOM要素を作る」という処理が、js側でガリガリと書くのではなく、データバインディングによりhtml側で宣言的に書けるのがとてもいいですね。