接著來看如何用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> 分

如此,一旦users資料有所增刪,不但清單表格會馬上反應,筆數及積分統計也會立刻呈現最新結果。
 
廢話不多說,馬上來體驗一下吧!

附上完整程式碼:

<!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>
[KO系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post

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([]),表格內容就會清空)

Post a comment