KO範例4 - 以清單方式呈現資料
8 |
接著來看如何用KO處理超級基本的網頁設計議題--以清單方式呈現資料。
想處理以陣列形式儲存的多筆資料,要先認識foreach。在ViewModel定義一個JavaScript Array或是ko.observableArray() (observableArray在新增或剔除陣列元素時,KO會立刻察覺反應到UI,普通Array則不會),然後在某個容器形HTML元素(例如: div, ul, tbody... )宣告data-bind="foreach: arrayPropName",就可以指定KO將容器內的子元素範本(Template,或稱樣版)以每筆資料一次的方式重複產生多次,例如:
<tbody data-bind="foreach: users">
<tr>
<td><span data-bind="text: id"></span></td>
<td><span data-bind="text: name"></span></td>
<td><span data-bind="text: score" style='text-align: right'></span></td>
<td><a href='#' data-bind="click: $root.removeUser">移除</a></td>
</tr>
</tbody>
在以上的例子中,我們假設ViewModel有一個陣列屬性—users,其中每筆資料物件都有id, name, score三個屬性,在tbody上宣告data-bind="foreach: users",意味者<tbody>到</tbody>間的內容,會依users陣列的元素多寡重複出現n次。而其中元素(如<span>, <a>)繫結對象便是users中的每一筆使用者資料,因此只要寫上data-bind="text: id"就可以對應到使用者的id屬性。
最後一個<td>中出現了<a data-bind="click: $root.removeUser">,聰明如你一定可以猜想它可用來移除該筆使用者資料,$root是一個特殊變數,會指向ViewModel個體。在這裡必須加上的原因是我們在ViewModel定義了remoteUser方法,但在<tbody>中,預設繫結的對象是使用者資料物件,若只寫data-bind="click: removeUser",KO會誤認成使用者資料物件上的removeUser方法。加上$root,KO會改由ViewModel的最上層尋找removeUser方法。
至於removeUser,寫法出奇簡單:
self.removeUser = function(user) {
self.users.remove(user);
}
當它在foreach範圍被點擊觸發時,會接收到一個參數,指向被點擊的那筆資料物件。所以,只需self.users.remove(user)就可以將該筆資料自observableArray移除,網頁也會馬上做出回應,該筆資料的<tr>會立刻從<tbody>中消失。
如果要新增使用者資料,在observableArray中加入一筆具有id, name, score三個屬性的物件即可,為了規範元件包含所有必要屬性,我們將user定義成function模擬ViewModel形式的物件:
function UserViewModel(id, name, score) {
var self = this;
self.id = id;
self.name = name;
self.score = score;
}
如此新增資料時即可寫成viewModel.users.push(new UserViewModel("M1", "Jeffrey", 32767));
最後再來點小把戲,為了展現KO可以即時掌握obervableArray的風吹草動,我們寫一個ko.computed計算所有使用者的score總和:
self.totalScore = ko.computed(function () {
var total = 0;
$.each(self.users(), function (i, u) {
total += u.score;
});
return total;
});
並在表格最上方加上
共 <span data-bind="text: users().length"></span> 筆,
合計 <span data-bind="text: totalScore"></span> 分
附上完整程式碼:
<!DOCTYPE html>
<html>
<head>
<title>Lab 4 - 物件陣列繫結清單顯示</title>
<script src="../Scripts/jquery-1.7.2.js"></script>
<script src="../Scripts/knockout-2.1.0.debug.js"></script>
<script>
//很簡單的User資料物件
function UserViewModel(id, name, score) {
var self = this;
self.id = id;
self.name = name;
self.score = score;
}
function MyViewModel() {
var self = this;
self.users = ko.observableArray();
//移除User,輸入參數為user物件
//foreach產生的元素,click事件時會帶入該元素所繫結的資料物件
self.removeUser = function(user) {
self.users.remove(user);
}
//分數加總,透過神奇的Dendency Tracking功能
//一旦有任何User分數更動,它就會自動更新
self.totalScore = ko.computed(function () {
var total = 0;
$.each(self.users(), function (i, u) {
total += u.score;
});
return total;
});
}
$(function () {
var vm = new MyViewModel();
//預先增加一些User
vm.users.push(
new UserViewModel("M1", "Jeffrey", 32767));
vm.users.push(
new UserViewModel("M2", "Darkthread", 65535));
//按鈕時動態增加User
var c = 2;
$("#btnAddUser").click(function () {
var now = new Date(); //用時間產生隨機屬性值
vm.users.push(new UserViewModel(
"M" + c++,
"P" + "-" + now.getSeconds() * now.getMilliseconds(),
now.getMilliseconds()));
});
ko.applyBindings(vm);
});
</script>
<style>
table { width: 400px }
td,th { border: 1px solid gray; text-align: center }
</style>
</head>
<body>
<input type="button" value="新增User" id="btnAddUser" />
共 <span data-bind="text: users().length"></span> 筆,
合計 <span data-bind="text: totalScore"></span> 分
<table>
<thead>
<tr><th>Id</th><th>姓名</th><th>積分</th><th></th></tr>
</thead>
<tbody data-bind="foreach: users">
<tr>
<td><span data-bind="text: id"></span></td>
<td><span data-bind="text: name"></span></td>
<td><span data-bind="text: score" style='text-align: right'></span></td>
<td><a href='#' data-bind="click: $root.removeUser">移除</a></td>
</tr>
</tbody>
</table>
</body>
</html>
Comments
# by Alex Lee
1, 為什麼$root.removeUser 是會去找ViewModel的remoteUser? 2. 找不到 remoteUser 方法 3. 程式好像是重覆?
# by Jeffrey
to Alex Lee, 鳴... 謝謝指正! remoteUser錯字(應為remo"v"eUser才對)及程式碼重複問題已修正。
# by 小黑
請問黑大 此類清單是否可以實作分類?
# by 小黑
不好意思應該是,分「頁」
# by Jeffrey
to 小黑,技術上可行,但細節繁瑣,對於需要分頁、排序的清單需求,我偏好用現成的解決方案,例如: Kendo UI Grid、jqGrid等。
# by 小黑
謝謝黑大
# by 傑森
請問版主,若已繫結了陣列屬性( ko.observableArray() ), 下一次取得資料來源後,直接指定給 vm.users, 而不是清空後再一個個 push 進去,此時 UI 並沒有隨著 vm.users 的新陣列更新內容? 一定要一個個 push 進去陣列?
# by Jeffrey
to 傑森, 用vm.users(資料陣列)的方式可以一次指定整個陣列內容並觸發UI變更才對。(例如: vm.users([]),表格內容就會清空)