TypeScript で Knockout を使ってみる
TypeScript という言語を使って、Knockout のサンプルコードを書いてみました。
TypeScript は CoffeeScript などと同様に JavaScript へコンパイルするタイプの言語で、C# に近いオブジェクト指向が特徴です。
ここでは、Knockout, jQuery, linq.js を組み合わせて使うシンプルな例を示します。
(linq.js を用いたコレクション操作については、
neue.cc 先生のライブコーディング
をパク参考にさせて頂きました。)
デモ
Twitter キーワード検索
検索キーワード: ("test"と入力すると比較的多くの言語のツイートがヒットします) ヒットしたツイート:コード: View
<h1>Twitter キーワード検索</h1>
検索キーワード: ("test"と入力すると比較的多くの言語のツイートがヒットします)
<form data-bind="submit: search">
<input type="text" data-bind="value: searchKeyword, valueUpdate: 'afterkeydown'" placeholder="検索キーワード"/>
<button type="submit">SEARCH</button>
</form>
ヒットしたツイート:
<div data-bind="foreach: searchResult">
<div class="tweet">
<span class="username" data-bind="text: '@' + from_user"> </span>
<span class="tweet_text" data-bind="text: text"> </span>
</div>
</div>
使用言語:
<div data-bind="foreach: tweetLanguages">
<span class="tweet_lang" data-bind="text: $data"> </span>
</div>
コード: ViewModel (TypeScript)
// 外部参照
/// <reference path="Scripts/jquery.d.ts" />
/// <reference path="Scripts/knockout.d.ts" />
/// <reference path="Scripts/linq.js.d.ts" />
// ツイッタークライアント(モデル)定義
class TwitterClient {
static search(keyword: string, callback?: (json: any) => any) {
$.getJSON("http://search.twitter.com/search.json?callback=?", { q: keyword }, callback);
}
}
// メイン ViewModel 定義
class AppViewModel {
searchKeyword: string = ""; // 検索キーワード
searchResult: KnockoutObservableArray = ko.observableArray([]); // 検索結果配列
tweetLanguages: KnockoutComputed; // 検索結果のツイートの使用言語ごとの集計
constructor() {
// 検索結果から使用言語ごとの集計を得る LINQ クエリを ko.computed で使用
this.tweetLanguages = ko.computed(() =>
Enumerable.from(this.searchResult())
.groupBy(t => t.iso_language_code) // 言語でグループ化
.select(g => g.key() + " (" + g.count() + ")") // 文字列として射影
.toArray() // 配列に変換
);
}
search() { // 検索アクション
TwitterClient.search(this.searchKeyword, (json) => {
this.searchResult(json.results);
});
}
}
$(() => ko.applyBindings(new AppViewModel()));
著者雑記
TypeScript は JavaScript の上に、純粋なオブジェクト指向, 型推論, ラムダ式などのシンタックスシュガーを構築する とてもパワフルな言語です。しかし、このサンプルを書いた時に1つだけ疑問に思った点があります。
それは、this が使えないコンテキストについてです。TypeScript において、コンパイラレベルで this が使えないコンテキストがあります。
TypeScript「this」が使えるコンテキスト
- グローバルコンテキスト, 通常の関数定義および
function式 - クラスのコンストラクタ, メンバ関数およびメンバアクセサ
上記2つ以外のコンテキストでは、this を使おうとするとコンパイルエラーになります。
たとえば、このサンプルの AppViewModel.tweetLanguages を、定義と同時に
ko.computed で初期化しようとすると、内部で参照している this.searchResult()
にてコンパイルエラーが発生してしまいます。
つまり、しかたなくコンストラクタの内部で初期化しているのです。
この挙動は、JavaScript のオブジェクトリテラルについて理解されている方は 「そりゃそうだろう」と思われるかもしれません。 そこで、次のコードを見て下さい。上記の TypeScript コードは、次のようにコンパイルされます。
コード:ViewModel (コンパイル結果の JavaScript)
var TwitterClient = (function () {
function TwitterClient() { }
TwitterClient.search = function search(keyword, callback) {
$.getJSON("http://search.twitter.com/search.json?callback=?", {
q: keyword
}, callback);
};
return TwitterClient;
})();
var AppViewModel = (function () {
function AppViewModel() { // プロパティもコンストラクタで生成してるじゃん!
var _this = this;
this.searchResult = ko.observableArray([]);
this.searchKeyword = "";
this.tweetLanguages = ko.computed(function () {
return Enumerable.from(_this.searchResult()).groupBy(function (t) {
return t.iso_language_code;
}).select(function (g) {
return g.key() + " (" + g.count() + ")";
}).toArray();
});
}
AppViewModel.prototype.search = function () {
var _this = this;
TwitterClient.search(this.searchKeyword, function (json) {
_this.searchResult(json.results);
});
};
return AppViewModel;
})();
$(function () {
ko.applyBindings(new AppViewModel());
});
コメントにも書いたとおり、すべてのプロパティはコンストラクタの内部で宣言・初期化されます。
つまり、コンストラクタの先頭で宣言している var _this = this; が使えるスコープにあるわけです。
それならば、プロパティ宣言の初期化処理で this が使えたっていいのではないか、と思うのです。
TypeScript はまだ正式リリースされていないので、どうにか this を使える子にしてあげて欲しいです。
追記: TypeScript 0.9.5 にて確認したところ上記のように改善されていました!