Backbone.jsをTypeScriptから使用する


自分のウェブサイトで公開している[circlecheck](http://mizuvm01.cloudapp.net/gadgets/circlecheck/)というウェブアプリに[Backbone.js](http://backbonejs.org)を仕込んでみました。

Backbone.jsは機能毎に機能が分割された設計になっており、それぞれの説明を読んでいけば、迷う事は殆どありません。
ただ、[DefinitelyTyped](http://definitelytyped.org)が公開されているわりには、TypeScriptからどの様に記述したら良いのかが分かりづらい箇所がありましたので、覚え書きを残す事にしました。

以下の様な `interface` を使用する `Model`, `Collection`, `View` を想定しています。

## interface
```Javascript
interface ISAMPLE {
	idx: number;
	str_value: string;
	num_value: number;
}
```

## Backbone.Model

```Javascript
class model_Sample extends Backbone.Model {

	constructor(attributes?: any, options?: any) {
		super(attributes, options);
	}
}
```

## Backbone.Collection

```Javascript
class collection_Sample extends Backbone.Collection<model_Sample> {

	constructor(models?: model_Sample[] | Object[], options?: any) {
		// どちらの記述でも可。
		this.url = "/sample_url";
		this.url = function() { return ("/sample_url"); };
		super(models, options);

		this.on("add", this.evt_append);
		this.on("remove", this.evt_remove);
	}

	// Modelを一意に識別する方法を設定しておくと、add時に重複を弾く事が出来る。
	modelId(attributes: ISAMPLE) {
		return attributes.idx;
	}

	// 比較条件を設定しておけばコレクション内のモデルは常にこの並びとなる。
	comparator(compare: model_Sample, to?: model_Sample): number {
		if (compare.attributes.num_value > to.attributes.num_value) return 1;
		if (compare.attributes.num_value < to.attributes.num_value) return -1;
		return 0;
	}

	evt_append(m: model_Sample) {
		console.log("item append");
	}
	
	evt_remove(m: model_Sample) {
		console.log("item remove");
	}
}
```

## Backbone.View

```Javascript
class view_Sample extends Backbone.View<model_Sample> {

	constructor(options?: Backbone.ViewOptions<model_Sample>) {
		// superの呼び出し前に設定しておく必要がある。
		this.el = "body";
		super(options);

		this.listenTo(this.model, "change", this.evt_model_change)
	}

	events(): Backbone.EventsHash {
		return {
			"click button#id_btn": this.evt_btn_click
		}
	}

	evt_model_change(m: model_Sample) {
		console.log("evt_model_change");
	}

	evt_btn_click() {
		console.log("evt_btn_click");
		this.render();
	}

	render() {
		return this;
	}
}
```

## テストコード

```html:index.html
<!DOCTYPE html>
<html lang="ja">

    <head>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <meta name="author" content="MizunagiKB" />
    </head>

    <body>
        <div>
            <button id="id_btn">TEST</button>
        </div>
        <script type="text/javascript">
            window.onload = function()
            {
                main();
            }
        </script>

        <!-- jQuery http://jquery.com/ -->
        <script type="text/javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.0/jquery.js"></script>

        <!-- Bootstrap http://twitter.github.io/bootstrap/-->
        <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.min.js"></script>

        <!-- Underscore.js http://jashkenas.github.io/underscore/ -->
        <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>

        <!-- Backbone.js http://backbonejs.org/ -->
        <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.2.3/backbone-min.js"></script>

        <script type="text/javascript" src="sample.js"></script>

    </body>
</html>

```

```Javascript:sample.ts
interface ISAMPLE {
    idx: number;
    str_value: string;
    num_value: number;
}

class model_Sample extends Backbone.Model {

    constructor(attributes?: any, options?: any) {
        super(attributes, options);
    }
}

class collection_Sample extends Backbone.Collection<model_Sample> {

    constructor(models?: model_Sample[] | Object[], options?: any) {
        // どちらの記述でも可。
        this.url = "/sample_url";
        this.url = function() { return ("/sample_url"); };
        super(models, options);

        this.on("add", this.evt_append);
        this.on("remove", this.evt_remove);
    }

    // Modelを一意に識別する方法を設定しておくと、add時に重複を弾く事が出来る。
    modelId(attributes: ISAMPLE) {
        return attributes.idx;
    }

    // 比較条件を設定しておけばコレクション内のモデルは常にこの並びとなる。
    comparator(compare: model_Sample, to?: model_Sample): number {
        if (compare.attributes.num_value > to.attributes.num_value) return 1;
        if (compare.attributes.num_value < to.attributes.num_value) return -1;
        return 0;
    }

    evt_append(m: model_Sample) {
        console.log("item append");
    }
    
    evt_remove(m: model_Sample) {
        console.log("item remove");
    }
}

class view_Sample extends Backbone.View<model_Sample> {

	constructor(options?: Backbone.ViewOptions<model_Sample>) {
		// superの呼び出し前に設定しておく必要がある。
		this.el = "body";
		super(options);

		this.listenTo(this.model, "change", this.evt_model_change)
	}

	events(): Backbone.EventsHash {
		return {
			"click button#id_btn": this.evt_btn_click
		}
	}

	evt_model_change(m: model_Sample) {
		console.log("evt_model_change");
	}

	evt_btn_click() {
		console.log("evt_btn_click");
		this.render();
	}

	render() {
		return this;
	}
}

function main() {
	let value_1: ISAMPLE = { idx: 1, str_value: "A", num_value: 123 };
	let value_2: ISAMPLE = { idx: 2, str_value: "B", num_value: 456 };
	let value_3: ISAMPLE = { idx: 3, str_value: "C", num_value: 789 };

	let value_A: ISAMPLE = { idx: 1, str_value: "A", num_value: 123 };

	let m = new model_Sample();
	let c = new collection_Sample();
	let v = new view_Sample(
		{
			model: m
		}
	);

	// 同じモデルを与えてもchangeイベントは発生しない。（変更とみなされない）
	m.set(value_1);
	m.set(value_1);
	// attributesの変更をチェックしているので、この場合もchangeイベントは発生しない。
	m.set(value_A);
	// changeイベントが発生する。
	m.set(value_2);
	m.set(value_3);

	c.add(value_1);
	// 同じidxを持っている場合はaddイベントは発生しない。（追加されない）
	c.add(value_1);
	c.remove(value_1);
	// 存在しないものを削除しようとしてもremoveイベントは発生しない。
	c.remove(value_1);

	c.add(value_3);
	c.add(value_1);
	c.add(value_2);

	// comparatorが設定されているため、
	// collection内は（設定を解除しない限り）ソートされる。
	for (let i: number = 0; i < c.length; i++) {
		console.log(c.at(i))
	}

	// 配列を渡す場合は、JSON.stringifyで事前に文字列化しておくこと。
	c.fetch(
		{
			data: {
				param_1: "A",
				param_2: 1,
				param_3: JSON.stringify(["B", 2])
			}
		}
	);
}
```

## おまけ
### DefinitelyTypedのbackbone-global.d.tsについて

定義内で以下の様に書かれていますが、

```Javascript
	remove(model: TModel, options?: Silenceable): TModel;
	remove(models: TModel[], options?: Silenceable): TModel[];
```

addの記述と対応している方が妥当だと思われます。

```Javascript
	remove(model: {}|TModel, options?: Silenceable): TModel;
	remove(models: ({}|TModel)[], options?: Silenceable): TModel[];
```

こう記述されるはず…