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

TypeScript + Knockout ES5 でさらにシンプルに

以前の Twitter を使った DEMO が動かなくなっていたことと、TypeScript が正式リリースされたこともあり 本記事はリニューアルいたしました。 過去のサンプルをご覧になりたい方は こちら をご参照ください。

TypeScript という言語のサンプルをご用意しました。

TypeScript は CoffeeScript などと同様に JavaScript へコンパイルするタイプの言語 (altJS と総称される) で、C# に近いオブジェクト指向表現が特徴です。 ここでは TypeScript と Knockout ES5 の力を借りて グリッドエディタ を書きなおしてみます。

TypeScript での開発の始め方については TypeScript クイックガイド がわかりやすいです。

ViewModel

/// <reference path="../d.ts/knockout/knockout.d.ts" />
/// <reference path="../d.ts/knockout.es5/knockout.es5.d.ts" />

class ItemViewModel {
    constructor(public name: string, public price: number) {
        ko.track(this); // プロパティ (name, number) を監視できるようにする
    }
}

class GiftSetViewModel {
    constructor(public gifts: ItemViewModel[]) {
        ko.track(this); // プロパティ (gifts) を監視できるようにする

        // メンバ関数内の this を自身のインスタンスに固定する
        this.addGift = this.addGift.bind(this);
        this.removeGift = this.removeGift.bind(this);
        this.save = this.save.bind(this);
    }

    addGift(): void {
        this.gifts.push({
            name: "",
            price: 0
        });
    }

    removeGift(gift: ItemViewModel): void {
        this.gifts.remove(gift);
    }

    save(): void {
        alert('次のようにサーバに送信できます:' + ko.toJSON(this.gifts));
    }
}

ko.applyBindings(new GiftSetViewModel([
    new ItemViewModel('高帽子', 39.95),
    new ItemViewModel('長いクローク', 120.00)
]));

View

<form data-bind="submit: save">
    <p>欲しいものリスト: <span data-bind="text: gifts.length"> </span> 点</p>
    <table data-bind="visible: gifts.length > 0">
        <thead>
        <tr>
            <th>名前</th>
            <th>価格</th>
            <th></th>
        </tr>
        </thead>
        <tbody data-bind="foreach: gifts">
        <tr>
            <td><input type="text" data-bind="value: name" /></td>
            <td><input type="text" data-bind="value: price" /></td>
            <td><a href="#" data-bind="click: $root.removeGift">削除</a></td>
        </tr>
        </tbody>
    </table>

    <button data-bind="click: addGift">追加</button>
    <button type="submit" data-bind="enable: gifts.length > 0">登録</button>
</form>

TypeScript で Knockout を使う際の問題点と対処法

GiftSetViewModelconstructor で、なにやら謎の処理が行われていますね。

JavaScript の問題児 this

TypeScript という言語は、JavaScript の言語仕様を置き換えるものではなく あくまで JavaScript のスーパーセット≒表現拡張です。 つまり JavaScript でクセモノの this は TypeScript でも同様にクセモノなのです。

GiftSetViewModel のメンバ関数である addGift, removeGift, save は、 次のように JavaScript にコンパイルされます。

GiftSetViewModel.prototype.addGift = function () {
    this.gifts.push({
        name: "",
        price: 0
    });
};

GiftSetViewModel.prototype.removeGift = function (gift) {
    this.gifts.remove(gift);
};

GiftSetViewModel.prototype.save = function () {
    alert('次のようにサーバに送信できます:' + ko.toJSON(this.gifts));
};

それぞれの関数内で参照される this は、関数を次のように呼び出すことにより 簡単に置き換えることができます。

var giftSet = new GiftSetViewModel([]);
giftSet.addGift.call(undefined); // this を undefined で置き換えて実行
// 内部で this.gifts を参照しようとしてエラーとなる

さらに悪いことに、click, event などのイベント系バインディングでは、 Knockout は必ず this を置き換えつつ、ハンドラ関数を呼び出します。

Function.bind による this の束縛

そこでこの3行によって this を書き換えられないように固定していたのです。

// メンバ関数内の this を自身のインスタンスに固定する
this.addGift = this.addGift.bind(this);
this.removeGift = this.removeGift.bind(this);
this.save = this.save.bind(this);

this.memberFunc = this.memberFunc.bind(this) とすることで this を束縛した新たな関数を自身のプロパティとして保持します。 インスタンスごとに関数オブジェクトが生成されてしまう、というパフォーマンス上のデメリットはありますが、 this が容易く書き換わる JavaScript の世界ですから諦めどころといえるでしょう。

まとめて束縛しよう

次のような関数を定義することで、メンバ関数の数だけ bind せずに一括で this を束縛できます。 関数名がなにやら不穏ではありますが、これでコンストラクタがすこしスッキリしますね。

function bindMySelf(viewModel: any, functionNames: string[]): void {
    functionNames.forEach((functionName) => {
        viewModel[functionName] = viewModel[functionName].bind(viewModel);
    })
}

class GiftSetViewModel {
    constructor(public gifts: ItemViewModel[]) {
        ko.track(this);
        // メンバ関数を View 公開できるようにする
        bindMySelf(this, ['addGift', 'removeGift', 'save']);
    }

    addGift(): void {
        this.gifts.push({
            name: "",
            price: 0
        });
    }

    removeGift(gift: ItemViewModel): void {
        this.gifts.remove(gift);
    }

    save(): void {
        alert('次のようにサーバに送信できます:' + ko.toJSON(this.gifts));
    }
}

side menu