Backbone.UndoをTypescriptで使ってみた その3

昨日のサンプルは、クリックイベントの処理がモデルの中に入っているので、ちょっとイマイチだったので、コントローラ側でその辺りの処理をするように再度修正してみました。また、ArrayなどのUndoに対応できる用にコントローラ側でデータ修正する際に、深いコピーでモデルに入れるようにしないといけないのでその辺りも合わせて修正しました。

  • app.ts
/// 
/// 
/// 
/// 

module sample {
		var undoButton = $("#undo");
		var redoButton = $("#redo");
		var addButton = $("#add");
		var updateNameButton = $("#update_name");
		var updateValueButton = $("#update_value");
		var itemButton = $("#item_button");
		var attrButton = $("#attr_button");

		export class App {
				private listview: ItemListView;
				private appModel: AppModel;
				constructor() {
						var self = this;
						self.appModel = new AppModel();

						self.listview = new ItemListView({
								"appModel": self.appModel			// collectionのエイリアスを持つのではダメ、ポインタが書き換わるため
						});

						undoButton.click(function () {
								console.log("undoButton.click");
								self.appModel.undoHandler.undo();
								self.draw();
						});
						redoButton.click(function () {
								console.log("redoButton.click");
								self.appModel.undoHandler.redo();
								self.draw();
						});

						addButton.click(function () {
								console.log("addButton.click");
								var ary = >self.appModel.get("collection");
								var newary = new Array();// 新しくオブジェクトを作らないとstack.onしない
								ary.forEach(function (item) {
										newary.push(item);
								});
								newary.push({ "name": "0", "value": "0" });
								self.appModel.set({ "collection": newary });
								//self.listview.list = self.appModel.get("collection");
								self.draw();
						});

						updateNameButton.click(function () {
								console.log("updateNameButton.click");
								var newary = new Array();
								self.appModel.get("collection").forEach(function (item) {
										newary.push({"value":item.value,"name":_.random(100).toString() }); // 新しくオブジェクトを作らないとstack.onしない
								});
								self.appModel.set({ "collection": newary });
								self.draw();
						});
						updateValueButton.click(function () {
								console.log("updateValueButton.click");
								var newary = new Array();
								self.appModel.get("collection").forEach(function (item) {
										newary.push({ "name": item.name, "value": _.random(100).toString() });
								});
								self.appModel.set({ "collection": newary });
								self.draw();
						});

						itemButton.click(function () {
								console.log("itemButton.click");
								self.appModel.set({
										"item_item": { "item_name": _.random(100).toString(), "item_value": _.random(100).toString() }
								});
								self.draw();
						});
						attrButton.click(function () {
								console.log("attrButton.click");
								self.appModel.set({ "attr": _.random(100).toString() });
								self.draw();
						});

				}

				draw(): void {
						console.log("draw");
						this.listview.render();
						$("#item_name").text(this.appModel.get("item_item").item_name);
						$("#item_value").text(this.appModel.get("item_item").item_value);
						$("#attr").text(this.appModel.get("attr"));
				}
		}

		export class AppModel extends Backbone.Model {
				undoHandler: UndoHandler;

				constructor(options?) {
						super(options);
						this.undoHandler = new UndoHandler(this);
						this.bind("change", this.change);
						
				}
				defaults() {
						return {
								collection:[
										{ "name": "0", "value": "0" }
								],
								item_item: {
										"item_name": "",
										"item_value":""
								},
								attr:"",
						}
				}

				change(): void {

						console.log("AppModel.change");
				}

		}

		export class UndoHandler {
				private manager: Backbone.UndoManager;
				 constructor(collections:any) {


						var self = this;
						 self.manager = new Backbone.UndoManager({ register: collections, track: true });

						 self.manager.removeUndoType("change:isChanging");
						 self.manager.addUndoType("change:isChanging", {
								"on": function (model, isChanging, options) {
										console.log("manager.addUndoType.on");
								},
								"undo": function (model, before, after, options) {
										console.log("manager.addUndoType.undo");
										model.set(before)
								},
								"redo": function (model, before, after, options) {
										console.log("manager.addUndoType.redo");
										model.set(after)
								}
						});


						 self.manager.on("all", function (type) {
								console.log("all.type:" + type);
								switch (type) {
										case "undo": {
												console.log("undo");
												break;
										}
										case "redo": {
												console.log("redo");
												break;
										}
								}
						});
						 self.manager.trigger("undo redo");

						 self.manager.stack.on("add", function () {
								console.log("stack.on.add");
						});

						

				 }
				undo(): void{
						this.manager.undo(true);
				}
				redo(): void {
						this.manager.redo(true);
				}
		}


		class ItemModel extends Backbone.Model {
				constructor(options?) {
						if (options) {
								if (options.name) this.set({ "name": options.name }, { "validate": true });
								if (options.value) this.set({ "value": options.value }, { "validate": true });
						}
						super(options);
						this.bind("change", this.change);
				}
				change(): void {
						console.log("ItemModel.change:name="+this.get("name")+",value="+this.get("value"));
				}
				defaults() {
					return {
								"id": _.uniqueId(),
								"name": "",
								"value":""
						}
				}
				validate(attrs) {
						if (!attrs.name || _.isEmpty(attrs.name)) {
								return "name must not be empty";
						}
						if (!attrs.value || _.isEmpty(attrs.value)) {
								return "value must not be empty";
						}
				}
		}

		class ItemCollection extends Backbone.Collection{
				constructor(options) {
						super(options);
				}
		}

		class ItemView extends Backbone.View {
				template: (data: any) => string;
				constructor(options?) {
						var html = $("#template-item").html();
						this.template = _.template(html);
						this.events = {
								"click": "onclick",
								"change":"onchange"
						};
						super(options);
						this.model.bind("change", this.render, this);
				}
				private onclick() {
						console.log("ItemView.onclick");
				}
				private change() {
						console.log("ItemView.onchange");
						this.render();
				}

				render(): ItemView {
						var html = this.template(this.model.toJSON());
						$(this.el).html(html);
						return this;
				}

		}

		class ItemListView extends Backbone.View{
				appModel: any; // backbone.model
				constructor(options?) {
						var self = this;
						this.el = "#item-list";
						if (options) {
								if (options.appModel) this.appModel = options.appModel;
						}
						
						this.collection = new Backbone.Collection();
						this.appModel.get("collection").forEach(function (item) {
								self.collection.add(new ItemModel(item));
						});

						super(options);
						this.listenTo(this.collection, 'add', this.render);
						this.listenTo(this.collection, 'remove', this.render);
						this.listenTo(this.collection, 'reset', this.render);
				}
				render(): ItemListView {
						var el = $(this.el);
						el.empty();
						var self = this;

						self.collection = new Backbone.Collection();
						this.appModel.get("collection").forEach(function (item) {
								self.collection.add(new ItemModel(item));
						});
						this.collection.forEach(item=> {
								console.log("ItemListView.render");
								var view=new ItemView({ "model": item }).render();
								el.append(view.el);
						});
						return this;
				}
		}
}

new sample.App().draw();

  • index.html




		
		






		

item_name:
,item_value:

attr:




次に単体テストをmochaを使用して記述してみます。

予めmocha.js,mocha.d.ts,mocha.css,asert.js,assert.d.tsを用意しておく必要があります。

書いていてわかったのですが、実はBackbone.undo.jsはデータをスタックする際にタイムスタンプを用いているらしいので、テストユニットでババっと流すと、同じタイムスタンプでスタックされてしまい、思い通りの動きになりません。

そこで、テストユニットにはsettimeoutで少し実行間隔を開けてやることにします。

  • app-test.ts
///
///


module sample {

		describe("テスト", () => {

				it("UndoRedoTest", (done) => {// done:これで同期でテストできる

						var model = new AppModel();
						
						// どうやらBackboneUndoはタイムスタンプを用いているらしいので同じ時刻にstack.onした場合には認識できない
						setTimeout(() => {
								model.set({ "attr": "attrvalue" });
								assert(model.get("attr") == "attrvalue");
								console.log("attr:" + model.get("attr"));

								setTimeout(() => {
										model.set({
												"item_item": { "item_name": "item_name1", "item_value": "item_value1" }
										});
										assert(model.get("item_item").item_name == "item_name1");
										console.log("item_item.item_name:" + model.get("item_item").item_name);
 
										setTimeout(() => {
												// undo
												model.undoHandler.undo();
												assert(model.get("item_item").item_name == "");
												console.log("item_item.item_name:" + model.get("item_item").item_name);

												setTimeout(() => {
														// redo
														model.undoHandler.redo();
														assert(model.get("item_item").item_value == "item_value1");
														console.log("item_item.item_value:" + model.get("item_item").item_value);

														done(); // これで同期でテストできる

												}, 100);

										}, 100);

								}, 100);


						}, 100);

				});

		});
}

done()を入れることにより、同期をとったテストを実行できます

  • app-test.html



		
		drawing test
		


		

本来ならば、gruntとphantom.jsを用いてコマンドラインから実行するのが普通でしょうが、このapp-test.htmlをブラウザで開くだけでもテストを実行できます。エラーなどはchromeのデバッグツールで確認して下さい

サンプルはこちらにアップしておきました

https://github.com/anagotan/typescript_sample/tree/master/undo

Backbone.UndoをTypescriptで使ってみた その2

前回のサンプルではModelとView辺がうまく分離できていなかったので修正してみた

Backbone.Modelに状態を保存し、イベントを拾って、Undoなどの処理を行うようにしてみた

  • app.ts
/// 
/// 
/// 
/// 

module sample {
		var undoButton = $("#undo");
		var redoButton = $("#redo");
		var addButton = $("#add");
		var updateNameButton = $("#update_name");
		var updateValueButton = $("#update_value");
		export class App {
				private listview: ItemListView;
				private appModel: AppModel;
				constructor() {
						this.appModel = new AppModel();
						this.listview = new ItemListView({
								"collection":this.appModel.get("collection")
						});
						var self = this;

						undoButton.click(function () {
								console.log("undoButton.click");
								self.appModel.set({ "undoButtonClicked": true });
						});
						redoButton.click(function () {
								console.log("redoButton.click");
								self.appModel.set({ "redoButtonClicked": true });
						});

						addButton.click(function () {
								console.log("addButton.click");
								self.appModel.set({ "addButtonClicked": true });
						});

						updateNameButton.click(function () {
								console.log("updateNameButton.click");
								self.appModel.set({ "updateNameButtonClicked": true });
						});
						updateValueButton.click(function () {
								console.log("updateValueButton.click");
								self.appModel.set({ "updateValueButtonClicked": true });
						});

				}

				draw(): void {

						this.listview.render();
				}
		}

		class AppModel extends Backbone.Model {
				private undoHandler: UndoHandler;

				constructor(options?) {
						super(options);
						this.undoHandler = new UndoHandler(this.get("collection"));
						this.bind("change", this.change);
				}
				defaults() {
						return {
								collection: new ItemCollection([
										new ItemModel({ "name": "0", "value": "0" }),
								]),
								undoButtonClicked: false,
								redoButtonClicked: false,
								addButtonClicked: false,
								updateNameButtonClicked: false,
								updateValueButtonClicked:false
						}
				}

				change(): void {
						if (this.get("undoButtonClicked")) {
								this.undoHandler.undo();
								this.set({ "undoButtonClicked": false });
						}
						if (this.get("redoButtonClicked")) {
								this.undoHandler.redo();
								this.set({ "redoButtonClicked": false });
						}
						if (this.get("addButtonClicked")) {
								this.get("collection").add(new ItemModel({ "name": "0", "value": "0" }));
								this.set({ "addButtonClicked": false });
						}
						if (this.get("updateNameButtonClicked")) {
								this.get("collection").forEach(function (item) {
										item.set("name", _.random(100).toString());
								});
								this.set({ "updateNameButtonClicked": false });
						}
						if (this.get("updateValueButtonClicked")) {
								this.get("collection").forEach(function (item) {
										item.set("value", _.random(100).toString());
								});
								this.set({ "updateValueButtonClicked": false });
						}
						console.log("AppModel.change");
				}

		}

		class UndoHandler {
				private manager: Backbone.UndoManager;
				 constructor(collections:Backbone.Collection) {

						var self = this;
						 self.manager = new Backbone.UndoManager({ register: collections, track: true });

						 self.manager.removeUndoType("change:isChanging");
						 self.manager.addUndoType("change:isChanging", {
								"on": function (model, isChanging, options) {
										console.log("manager.addUndoType.on");
								},
								"undo": function (model, before, after, options) {
										console.log("manager.addUndoType.undo");
										model.set(before)
								},
								"redo": function (model, before, after, options) {
										console.log("manager.addUndoType.redo");
										model.set(after)
								}
						});

						 self.manager.on("all", function (type) {
								console.log("all.type:" + type);
								switch (type) {
										case "undo": {
												console.log("undo");
												break;
										}
										case "redo": {
												console.log("redo");
												break;
										}
								}
						});
						 self.manager.trigger("undo redo");

						 self.manager.stack.on("add", function () {
								console.log("stack.on.add");
						});

				 }
				undo(): void{
						this.manager.undo(true);
				}
				redo(): void {
						this.manager.redo(true);
				}
		}

		class ItemModel extends Backbone.Model {
				constructor(options?) {
						if (options) {
								if (options.name) this.set({ "name": options.name }, { "validate": true });
								if (options.value) this.set({ "value": options.value }, { "validate": true });
						}
						super(options);
						this.bind("change", this.change);
				}
				change(): void {
						console.log("ItemModel.change:name="+this.get("name")+",value="+this.get("value"));
				}
				defaults() {
						return {
								"id": _.uniqueId(),
								"name": "",
								"value":"" 
						}
				}
				validate(attrs) {
						if (!attrs.name || _.isEmpty(attrs.name)) {
								return "name must not be empty";
						}
						if (!attrs.value || _.isEmpty(attrs.value)) {
								return "value must not be empty";
						}
				}
		}

		class ItemCollection extends Backbone.Collection{
				constructor(options) {
						super(options);
				}
		}

		class ItemView extends Backbone.View {
				template: (data: any) => string;
				constructor(options?) {
						var html = $("#template-item").html();
						this.template = _.template(html);
						this.events = {
								"click": "onclick",
								"change":"onchange" 
						};
						super(options);
						this.model.bind("change", this.render, this);
				}
				private onclick() {
						console.log("ItemView.onclick");
				}
				private change() {
						console.log("ItemView.onchange");
						this.render();
				}

				render(): ItemView {
						var html = this.template(this.model.toJSON());
						$(this.el).html(html);
						return this;
				}

		}

		class ItemListView extends Backbone.View{
				constructor(options?) {
						this.el = "#item-list";
						if (options) {
								if (options.collection) this.collection = options.collection;
						}
						super(options);
						this.listenTo(this.collection, 'add', this.render);
						this.listenTo(this.collection, 'remove', this.render);
						this.listenTo(this.collection, 'reset', this.render);
				}
				render(): ItemListView {
						var el = $(this.el);
						el.empty();
						this.collection.forEach(item=> {
								console.log("ItemListView.render");
								var view=new ItemView({ "model": item }).render();
								el.append(view.el);
						});
						return this;
				}
		}
}

new sample.App().draw();
  • index.html




		
		




		



AppModelクラスが状態を保存している。このBackboneModelに対し、各マウスイベントで発生したClickイベントの関数からModelに対し値をセット。Backboneのchangeイベントが発生するのでそれを拾って処理を行う感じ

Backboneをこのように使うのかどうかは不明。。自己流です

Backbone.UndoをTypescriptで使ってみた

backbone.undo.jsというBackboneJS用のUNDOモジュールが有ります。

http://backbone.undojs.com/

こちらをTypescriptで使用してみました。うまくいったのでサンプルを掲載しておきます

通常、Typescriptで既存のライブラリを使用する際には、Typescript用に型定義ファイルを作成してやる必要が有ります。

世の中にはいい人がいて、たいていのライブラリは型定義されていたりします。

https://github.com/borisyankov/DefinitelyTyped

しかし、今回使用するbackbone.undo.jsはまだこちらでは定義されていませんでしたので、適当に作成してみました。

https://github.com/anagotan/DefinitelyTyped/tree/master/backbone.undo

これを利用してUndo機能を実装してみます。

仕様としては、addボタンを押すと「name=0,value=0」のボタンが追加されていきます。update_nameおよびupdate_valueのボタンでランダムな数値に置き換わります。

これをもとにundoおよびredoを実装してみました。

  • index.html




		
		




		



  • app.ts
/// 
/// 
/// 
/// 

module sample {
		var undoButton = $("#undo");
		var redoButton = $("#redo");
		var addButton = $("#add");
		var updateNameButton = $("#update_name");
		var updateValueButton = $("#update_value");

		export class App {
				private listview: ItemListView;
				constructor() {

						this.listview = new ItemListView({
								"collection": new ItemCollection([
										new ItemModel({ "name": "0" ,"value":"0"}) ,
								])
						});
						var self = this;
						var manager = new Backbone.UndoManager({ register: [this.listview.collection], track: true });

						manager.removeUndoType("change:isChanging");
						manager.addUndoType("change:isChanging", {
								"on": function (model, isChanging, options) {
										console.log("manager.addUndoType.on");
								},
								"undo": function (model, before, after, options) {
										console.log("manager.addUndoType.undo");
										model.set(before)
								},
								"redo": function (model, before, after, options) {
										console.log("manager.addUndoType.redo");
										model.set(after)
								}
						});

						manager.on("all", function (type) {
								console.log("all.type:" + type);
								switch (type) {
										case "undo": {
												console.log("undo");
												break;
										}
										case "redo": {
												console.log("redo");
												break;
										}
								}
						});
						manager.trigger("undo redo");

						manager.stack.on("add", function () {
								console.log("stack.on.add");
						});

						undoButton.click(function () {
								console.log("undoButton.click");
								manager.undo(true);
						});
						redoButton.click(function () {
								console.log("redoButton.click");
								manager.redo(true);
						});

						addButton.click(function () {
								console.log("addButton.click");

								self.listview.collection.add(new ItemModel({ "name": "0", "value": "0" }));
						});

						updateNameButton.click(function () {
								console.log("updateNameButton.click");
								self.listview.collection.forEach(function (item) {
										item.set("name", _.random(100).toString());
								});
						});
						updateValueButton.click(function () {
								console.log("updateValueButton.click");
								self.listview.collection.forEach(function (item) {
										item.set("value", _.random(100).toString());
								});
						});

				}

				draw(): void {

						this.listview.render();
				}
		}

		class ItemModel extends Backbone.Model {
				constructor(options?) {
						if (options) {
								if (options.name) this.set({ "name": options.name }, { "validate": true });
								if (options.value) this.set({ "value": options.value }, { "validate": true });
						}
						super(options);
						this.bind("change", this.change);
				}
				change(): void {
						console.log("ItemModel.change:name="+this.get("name")+",value="+this.get("value"));
				}
				defaults() {
						return {
								"id": _.uniqueId(),
								"name": "",
								"value":"" 
						}
				}
				validate(attrs) {
						if (!attrs.name || _.isEmpty(attrs.name)) {
								return "name must not be empty";
						}
						if (!attrs.value || _.isEmpty(attrs.value)) {
								return "value must not be empty";
						}
				}
		}

		class ItemCollection extends Backbone.Collection{
				constructor(options) {
						super(options);
						this.on("change", this.onchange, this);
						this.on("add", this.onchange, this);
				}
				onchange(model,value,options):void {
						console.log("ItemCollection.change");
				}
		}

		class ItemView extends Backbone.View {
				template: (data: any) => string;
				constructor(options?) {
						var html = $("#template-item").html();
						this.template = _.template(html);
						this.events = {
								"click": "onclick",
								"change":"onchange" 
						};
						super(options);
						this.model.bind("change", this.render, this);
				}
				private onclick() {
						console.log("ItemView.onclick");
				}
				private change() {
						console.log("ItemView.onchange");
						this.render();
				}

				render(): ItemView {
						var html = this.template(this.model.toJSON());
						$(this.el).html(html);
						return this;
				}

		}

		class ItemListView extends Backbone.View{
			 // list: ItemCollection;
				constructor(options?) {
						this.el = "#item-list";
						if (options) {
								if (options.collection) this.collection = options.collection;
						}
						super(options);
						//_.bindAll(this);
						this.listenTo(this.collection, 'add', this.render);
						this.listenTo(this.collection, 'remove', this.render);
						this.listenTo(this.collection, 'reset', this.render);
				}
				render(): ItemListView {
						var el = $(this.el);
						el.empty();
						this.collection.forEach(item=> {
								console.log("ItemListView.render");
								var view=new ItemView({ "model": item }).render();
								el.append(view.el);
						});
						return this;
				}
		}
}

new sample.App().draw();

実行する際には別途,jquery,underscore.js,backbone.js を用意しておく必要が有ります

javascriptでサブドメインの異なるサイトでクッキーの共有

こちらの記事を参考にJavascriptでサブドメインの異なるサイト間でクッキーの恭有してみました。

http://d.hatena.ne.jp/dirablue/20090506/1241607961

http://www1.example.com/setCookie.html

  • setCookie.html

  





http://www2.example.com/getCookie.html

  • getCookie.html

  



ポイントはHTMLを設置するパスを同じにすることとdomain指定の前にドットをつけることでしょうか?

あとIDやパスワードをクッキーに保存してはいけません^^);

JavaScriptでコールバック関数に引数をとりAjaxでデータを取得してみる

JavaScriptでコールバック関数の実装ですが、いろいろなところで書かれているのですがちょっとよくわからなかったので試してみました。

a.html


	  
	

これで

	a=d,b=c

と表示されます。

ここでjQueryを使ってAjaxでデータを取得してみます。

b.txt

test

これを用意しておきhttpで取得してみます


    


a=test,b=d

うまくとれましたが、これでいいのかどうか。