SPA(Single Page Application) for knockout.js + TwitterBootstrap + Sammy.js + SQL Azure + Visual Studio2013

概要

Mobile(携帯)、PC/Tabletでも動作し、かつローカルアプリのように”さくさく”動かすためにには JavaScriptでSPAで、一生懸命かかないといけないです。

今回、簡単にSPA(Singe Page Application)ができるよに再利用可能な ベースクラスを作成してみました。

MITライセンスでご自由に改変して使ってください。

以下のサイトを事前にみておくと理解が早いです。

要件

  1. iPhone/Android/PC/Tabletで動作すること
    1. レシンポンシブルデザインにするためTwitterBootstrapを使用し各種端末に対応する。
    2. SPAの実現のために、JavaScriptを使用する
    3. Jquery + Knockout.js を使用して各種端末に対応する。
  2. 戻るボタン等のURL履歴に対応のこと
    1. Sammy.js により、URL履歴に対応する。
  3. デバックしやすくするためにVisual Studio 2013を使用する
  4. データベースはSQL Azureを使用する。
  5. WebサイトはWindows Azure 簡易Webサイトを使用する。

Demo Web Site(デモサイト)

http://vs-samples.azurewebsites.net/Home/SPA010

Program Source(プログラムソース)

サンプルコードは以下のGitHubに保存しました。

https://github.com/snoro/VS2013_MVC_Sample/tree/master/Backborn_Todos_WebAPI

イメージ

一覧画面

新規作成をクリックすると新規作成画面へ

明細をクリックすると更新画面へ

MVC5_VS2013_Knockout_SPA_0010

新規作成画面

MVC5_VS2013_Knockout_SPA_0020

更新画面

MVC5_VS2013_Knockout_SPA_0030

課題

  1. データの読み込み時、更新時にLoading画面を表示してボタンを押せないようにする。
  2. 一覧画面にページング処理をいれる。
  3. 明細データの取得や設定のコードを書かないでいいように改良する。
  4. 入力時に日付等にデートピッカーをつける

ソース抜粋

SPA010.cshtml.cshtml

[js htmlscript=”true”]
@{
ViewBag.Title = "Home Page";
}
<div id="disp" style="display:none">

<!–ko if: koSPAIsVisibleList –>
<div class="header">
<h2>一覧</h2>
<button type="button" class="btn btn-primary" data-bind="click: loadDetails">
再読込
</button>

<button type="button" class="btn btn-warning" data-bind="click: locationDetailCreate">
新規作成
</button>
<br>
<span class="label label-default">データ件数</span><span data-bind="text: koSPA_List().length"></span>
<span class="label label-default">Message</span><span data-bind="text: koSPA_List_Message"></span>

</div>
<div class="results">
<table class="table table-striped">
<thead>
<tr>
<th>Id</th>
<th>タイトル</th>
<th>日付</th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: koSPA_List">
<tr data-bind="click: locationDetailUpdate">
<td data-bind="text: Id"></td>
<td data-bind="text: Title"></td>
<td data-bind="text: Timestamp"></td>
<td><span class="glyphicon glyphicon-chevron-right"></span></td>
</tr>
</tbody>
</table>
</div>
<!–/ko–>

<!–ko if: koSPAIsVisibleDetail –>
<div class="header">
<h2>詳細
<!–ko if: koSPAIsVisibleDetail_Update –>
(更新)
<!–/ko–>
<!–ko ifnot: koSPAIsVisibleDetail_Update –>
(新規作成)
<!–/ko–>
</h2>

<button type="button" class="btn btn-primary" onClick="history.back(); return false;">
<span class="glyphicon glyphicon-chevron-left"></span>戻る
</button>
<!–ko if: koSPAIsVisibleDetail_Update –>
<button type="button" class="btn btn-warning" data-bind="click: updateDetailToDB">
更新
</button>
<button type="button" class="btn btn-danger" data-bind="click: deleteDetailFromDB">
削除
</button>
<!–/ko–>

<!–ko ifnot: koSPAIsVisibleDetail_Update –>
<button type="button" class="btn btn-warning" data-bind="click: createDetailToDB">
新規作成
</button>
<!–/ko–>

<br>
<span class="label label-default">Message</span><span data-bind="text: koSPA_Detail_Message"></span>

</div>

<div class="results">

<table class="table">
<thead>
<tr>
<th>項目名</th>
<th>値</th>
</tr>
</thead>
<tbody >
<!–ko if: koSPAIsVisibleDetail_Update –>
<tr>
<td>Id</td>
<td><span data-bind="text: koSPA_Detail.Id"></span></td>
</tr>
<!–/ko–>
<tr>
<td>Title</td>
<td><input type="text" data-bind="value: koSPA_Detail.Title" /></td>
</tr>
<tr>
<td>Detail</td>
<td><input type="text" data-bind="value: koSPA_Detail.Content" /></td>
</tr>
<tr>
<td>Timestamp</td>
<td><input type="text" data-bind="value: koSPA_Detail.Timestamp" /></td>
</tr>
</tbody>
</table>

</div>
<!–/ko–>

</div>

@section scripts{
<script type="text/javascript" src="/Scripts/knockout-3.0.0.js"></script>
<script type="text/javascript" src="/Scripts/sammy-0.7.4.js"></script>
<script type="text/javascript" src="/Scripts/Users/knockout-SPA-0.1.1.js"></script>

<script type="text/javascript">
$(document).ready(function () {
//knockout SPAのパラメータを設定する
//※このオブジェクトはNewする必要がないのでFunctionではない
var koSPAParm = {
url_List: "/api/Memo/",
url_Detail_Create: "/api/Memo/",
url_Detail_Update: "/api/Memo/",
url_Detail_Delete: "/api/Memo/",

//Knockout Model
koSPA_Model_Detail: function () {
this.Id = ko.observable();
this.Title = ko.observable();
this.Content = ko.observable();
this.Timestamp = ko.observable();
},
//Knockout View-Model
koSPA_ViewModel : {
koSPA_List: ko.observableArray(),
koSPA_List_Message: ko.observable(""),
koSPA_Detail: {
Id: ko.observable(""),
Title: ko.observable(""),
Content: ko.observable(""),
Timestamp: ko.observable(""),

},
koSPA_Detail_Message: ko.observable("")
}

}
//Using Post Detail to Database
//when Detail-Create-Button is clicked
//or Detail-Update-Button is clicked
koSPAParm.koSPA_ViewModel.createDetailToObj = function () {
var objDetail = {
//ID不要
Title: this.koSPA_Detail.Title(),
Content: this.koSPA_Detail.Content(),
Timestamp: this.koSPA_Detail.Timestamp()
};

return objDetail
}

//Using Initialize Detail
//When List-Create-button is clicked
koSPAParm.koSPA_ViewModel.initDetail = function () {
this.koSPA_Detail.Id(-1);
this.koSPA_Detail.Title("");
this.koSPA_Detail.Content("");
this.koSPA_Detail.Timestamp(new Date());
}

//Using Set List to Detail
//When List-Row is clicked
koSPAParm.koSPA_ViewModel.setListToDetail = function (detailOfList) {
this.koSPA_Detail.Id(detailOfList.Id());
this.koSPA_Detail.Title(detailOfList.Title());
this.koSPA_Detail.Content(detailOfList.Content());
this.koSPA_Detail.Timestamp(detailOfList.Timestamp());

}
//Using Set Detal to List
//When Detail-Update-button is clicked
koSPAParm.koSPA_ViewModel.setDetailToList = function (detailOfList) {
detailOfList.Id(this.koSPA_Detail.Id());
detailOfList.Title(this.koSPA_Detail.Title());
detailOfList.Content(this.koSPA_Detail.Content());
detailOfList.Timestamp(this.koSPA_Detail.Timestamp());
}

//Using Add Detail to List
//When List-Load-button is Clicked
//or Detail-Create-button is clicked
koSPAParm.koSPA_ViewModel.createDBDetailToObj = function (DBDetail) {
var objDetail = new koSPAParm.koSPA_Model_Detail()
.Id(DBDetail.Id)
.Title(DBDetail.Title)
.Content(DBDetail.Content)
.Timestamp(DBDetail.Timestamp);
return objDetail;

}
//knockout SPAを実行する
var koSPAApp = $.koSPA.CreateApp(koSPAParm);
koSPAApp.run();
document.getElementById("disp").style.display = "block";
});
</script>
}
[/js]

 

knockout-SPA-0.1.1.js

[js]
/*
* Knockout SPA(Single Page Application)
* Created By Seiji Noro (https://github.com/snoro)
*
* Source:
* MIT License: http://www.opensource.org/licenses/MIT
*/
(function ($, window) {
//knockout-3.0.0.jsでテスト済み
if (typeof (ko) === undefined) { throw ‘Knockout is required, please ensure it is loaded before loading this knockout-SPA’; }
//sammy-0.7.4.jsでテスト済み
if (typeof (Sammy) === undefined) { throw ‘Sammy is required, please ensure it is loaded before loading this knockout-SPA’; }
(function (factory) {
//公開する関数をkoSPAだけにする

// Support module loading scenarios
if (typeof define === ‘function’ && define.amd) {
// AMD Anonymous Module
define([‘jquery’], factory);
} else {
// No module loader (plain <script> tag) – put directly in global namespace
$.koSPA = window.koSPA = factory($);
}
})(function ($) {
//koSPAオブジェクト
var koSPA = {};
koSPA.VERSION = ‘0.1.0’;
koSPA.location = window.location.toString();

//CreateApp
koSPA.CreateApp = function () {
var koSPAAppFunc = arguments[0];
//koSPAApp オブジェクトをNewする。
var koSPAApp = new koSPA.App(koSPAAppFunc);
return koSPAApp;
};

//koSPAApp オブジェクト
//初期値を設定するためにFunctionにしている。
koSPA.App = function (koSPAParm)
{
//////////////////////////////////
//knockout Model
//////////////////////////////////
//これはprototypeでないとメモリにコピーされるためよくない
koSPAParm.koSPA_Model_Detail.prototype.locationDetailUpdate = function () {
window.location.hash = "#/DetailUpdate/" + this.Id();
};
//////////////////////////////////
//knockout View-Model
//////////////////////////////////
//thisに直接代入すると一覧取得のときにkoSPA_ViewModelを発見できない
this.koSPA_ViewModel = koSPAParm.koSPA_ViewModel;

//knockout View-Model Property
koSPAParm.koSPA_ViewModel.koSPAIsVisibleList = ko.observable(true);
koSPAParm.koSPA_ViewModel.koSPAIsVisibleDetail = ko.observable(true);
koSPAParm.koSPA_ViewModel.koSPAIsVisibleDetail_Update = ko.observable(true);

&nbsp;

//knockout View-Model Method
//(knockoutはViewModelのをprototypeで書くと、clickメソッド等でエラーにになる)
koSPAParm.koSPA_ViewModel.loadDetails = function () {
this.koSPA_List.removeAll(); // reset
$.ajax({
type: "GET",
url: koSPAParm.url_List,
dataType: "json"
})
.done(function (result) {
$.each(result, function (i, p) {
koSPAParm.koSPA_ViewModel.koSPA_List.push(
koSPAParm.koSPA_ViewModel.createDBDetailToObj(p)
);
koSPAParm.koSPA_ViewModel.koSPA_List_Message("データを取得しました:" + new Date());
});
});
};
koSPAParm.koSPA_ViewModel.locationList = function () {
window.location.hash = "#/List/";
};
koSPAParm.koSPA_ViewModel.locationDetailCreate = function () {
window.location.hash = "#/DetailCreate";
};
koSPAParm.koSPA_ViewModel.createDetailToDB = function () {
var dbDetailData = this.createDetailToObj();

$.ajax({
type: "POST",
url: koSPAParm.url_Detail_Create,
contentType: "application/json;charset=UTF-8",
dataType: "json",
data: JSON.stringify(dbDetailData)
})
.done(function (result) {
koSPAParm.koSPA_ViewModel.koSPA_List.push(
koSPAParm.koSPA_ViewModel.createDBDetailToObj(result)
);
koSPAParm.koSPA_ViewModel.koSPA_Detail_Message("データを新規作成しました:" + new Date());
})
};
koSPAParm.koSPA_ViewModel.updateDetailToDB = function () {
var dbDetailData = this.createDetailToObj();

$.ajax({
type: "POST",
url: koSPAParm.url_Detail_Update + koSPAParm.koSPA_ViewModel.koSPA_Detail.Id(),
contentType: "application/json;charset=UTF-8",
dataType: "json",
data: JSON.stringify(dbDetailData)
})
.done(function (result) {
var detailOfList = koSPAParm.koSPA_ViewModel.findDetailFromList(koSPAParm.koSPA_ViewModel.koSPA_Detail.Id());
koSPAParm.koSPA_ViewModel.setDetailToList(detailOfList)
koSPAParm.koSPA_ViewModel.koSPA_Detail_Message("データを更新しました:" + new Date());
})
};

koSPAParm.koSPA_ViewModel.deleteDetailFromDB = function () {
$.ajax({
type: "DELETE",
url: koSPAParm.url_Detail_Delete + koSPAParm.koSPA_ViewModel.koSPA_Detail.Id(),
contentType: "application/json;charset=UTF-8",
})
.done(function (result) {
koSPAParm.koSPA_ViewModel.removeDetailFromList(koSPAParm.koSPA_ViewModel.koSPA_Detail.Id());
koSPAParm.koSPA_ViewModel.locationList();
})
};
koSPAParm.koSPA_ViewModel.findDetailFromList = function (Id) {
var detail = ko.utils.arrayFirst(this.koSPA_List(), function (t) {
return Id === t.Id();
}, this);
return detail;
};
koSPAParm.koSPA_ViewModel.removeDetailFromList = function (Id) {
this.koSPA_List.remove(function (item) {
return item.Id() === Id;
})
};

//////////////////////////////////
//Sammy
//////////////////////////////////
var SammyDipatcher = function (Method, Param1) {

switch (Method) {
case "":
case "List":
koSPAParm.koSPA_ViewModel.koSPA_List_Message("");
koSPAParm.koSPA_ViewModel.koSPAIsVisibleList(true);
koSPAParm.koSPA_ViewModel.koSPAIsVisibleDetail(false);
break;
case "DetailUpdate":
koSPAParm.koSPA_ViewModel.koSPA_Detail_Message("");
koSPAParm.koSPA_ViewModel.koSPAIsVisibleList(false);
koSPAParm.koSPA_ViewModel.koSPAIsVisibleDetail(true);
koSPAParm.koSPA_ViewModel.koSPAIsVisibleDetail_Update(true);

var detailFromList = koSPAParm.koSPA_ViewModel.findDetailFromList(Param1);
koSPAParm.koSPA_ViewModel.setListToDetail(detailFromList);

break;
case "DetailCreate":
//koSPAParm.koSPA_ViewModel.koSPA_Detail_Message("");

koSPAParm.koSPA_ViewModel.koSPAIsVisibleList(false);
koSPAParm.koSPA_ViewModel.koSPAIsVisibleDetail(true);
koSPAParm.koSPA_ViewModel.koSPAIsVisibleDetail_Update(false);

koSPAParm.koSPA_ViewModel.initDetail();

break;
}
}
// Sammy を使ってブラウザの履歴に対応する(順番関係があるので、一番したにTOPを設置する。)
this.SammyApp = $.sammy(function () {

//Method/Parm1
this.get("#/:Method/:Param1", function () {
var Method = this.params["Method"];
var Param1 = parseInt(this.params["Param1"]);
SammyDipatcher(Method, Param1);
});

//Method
this.get("#/:Method", function () {
var Method = this.params["Method"];
var Param1 = -1;
SammyDipatcher(Method, Param1);
});

//Topページ( location.hashが空だった時の処理)
this.get("", function () {
//Ver 1.0.1 修正 同じサイト以外は移動する。
var word = String(window.location);
if (word.indexOf(koSPA.location) !== -1) {
var Method = "";
var Param1 = -1;
SammyDipatcher(Method, Param1);
}
else {
location.href = word;
}

});
});
};
//run
koSPA.App.prototype = {
run: function () {
ko.applyBindings(this.koSPA_ViewModel);
this.SammyApp.run();
this.koSPA_ViewModel.loadDetails();

}
};
return koSPA;
});
})(jQuery, window);
[/js]

参考

// ]]>

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA