網頁

Knockout教學2 - 清單與集合

清單與集合

我們經常在UI元素裡產生重覆的區塊,尤其是呈現清單時,使用者可以在其中新增和移除元素。knockout.js讓你很容易達成此目標,使用可觀察陣列物件(observable arrays)與foreach繫結。

訂位系統

接下來將會建置一個動態UI來訂座位與餐點,在ViewModel裡已經有:

// 姓名與餐點
function SeatReservation(name, initialMeal) {
    var self = this;
    self.name = name;
    self.meal = ko.observable(initialMeal);
}

// 初始化狀態
// 預約ViewModel
function ReservationsViewModel() {
    var self = this;

    // 可選擇餐點(與價格),不能編輯,應該來自伺服器。
    self.availableMeals = [
        { mealName: "Standard (sandwich)", price: 0 },
        { mealName: "Premium (lobster)", price: 34.95 },
        { mealName: "Ultimate (whole zebra)", price: 290 }
    ];    

    // 可編輯資料。
    // 使用可觀察陣列物件,內含兩筆初始化SeatReservation實體資料。
    self.seats = ko.observableArray([
        new SeatReservation("Steve", self.availableMeals[0]),
        new SeatReservation("Bert", self.availableMeals[0])
    ]);
}

ko.applyBindings(new ReservationsViewModel());
  • SeatReservation:一個簡單的JavaScript類別建構式,儲存姓名和餐點的選擇。
  • ReservationsViewModel:ViewModel類別,進行以下處理:
    • availableMeals: 提供餐點資料的JavaScript物件。
    • seats:初始化SeatReservation實體集合的陣列。注意,它是ko.observableArray,這是一個與正規陣列當相的可觀察物件。每次新增或移除項目時,意味著它可以自動觸發ui更新。

ViewModel準備好了之後,以下是預設的View:

<h2>座位預約</h2>

<table>
    <thead>
        <tr>
        <th>姓名</th><th>餐點</th><th>額外費用</th><th></th>
        </tr>
    </thead>
    <!-- Todo: 在tbody產生主體 -->
    <tbody></tbody>
</table>

現在<tbody>還沒有內容,更新<tbody>元素使用foreach繫結,它將呈現位於陣列裡的每個元素:

<tbody data-bind="foreach: seats"></tbody>

我們指定可觀察陣列元素seats來與foreach繫結關聯。然後補充一些<tr>元素以呈現每一個項目:

<tbody data-bind="foreach: seats">
    <tr>
        <td data-bind="text: name"></td>
        <td data-bind="text: meal().mealName"></td>
        <td data-bind="text: meal().price"></td>
    </tr>    
</tbody>

注意,因為meal屬性是一個可觀察物件,前篇說明過,讀取或寫入可觀察物件的值,必須當成函式呼叫。所以必須寫成meal().price來取得次屬性的值,而不是meal.price。

執行應用程式,你應該可以看到簡單關於座位預約的表格資訊。

foreach是流程控制繫結一個份子,包含有foreach、if、ifnot和with。這些使能夠構建任何種類的反覆運算(iterative)、條件運算或或基於動態ViewModel的巢狀UI。

新增項目

MVVM模式很容易與多變的物件關係合作,像是陣列或結構等。更新基礎資料,UI自動同步更新。

新增一個約預訂位功能,使用click繫結來關聯函式與ViewModel:

<button data-bind="click: addSeat">新增訂位</button>

然後實作addSeat函式,讓它加入其他的座位資訊到陣列中:

self.addSeat = function() {
    self.seats.push(new SeatReservation("", self.availableMeals[0]));
}

前面說明過,self.seats是與正規陣列相當的可觀察陣列物件,所以它擁有JavaScript陣列所有特性,利用push方法來將新的SeatReservation實體推入陣列之中。現在點擊新增訂位按鍵,UI會自動更新狀態,因為seats是可觀察物件,新增或移除項目都會觸發UI的自動更新。

注意,新增SeatReservation實體的動作並不會涉及重新產生的整個UI。為提高效率,Knockout.js追蹤什麼已經在你的ViewModel中變更且執行最小化的DOM更新以符合狀態。這意味著可以擴展到非常龐大或複雜的UI,並且保持它的短小精悍、靈敏甚甚至在行動裝置上。

讓資料可編輯

你可以使用將foreach區塊繫結在其他地方,這讓它很容易升級讓我們得到一個完整的資料編輯器。更新View:

<tbody data-bind="foreach: seats">
    <tr>
        <td><input data-bind="value: name" /></td>
        <td><select data-bind="options: $root.availableMeals, value: meal, optionsText: 'mealName'"></select></td>
        <td data-bind="text: meal().price"></td>
    </tr>    
</tbody>

這裡使用兩個新的繫結,options繫結和optionsText繫結。它們能控制下拉式選單並設定可用的選項和物件屬性(例如,mealName)來呈現於畫面上。

  • options繫結:和那一個可觀察陣列物件關聯
  • optionsText:呈現文字

重新執行應用程式。現在已經能選擇餐點,而且能即時更新選擇餐點額外費用的Price資訊到畫面上。

費用格式化

我們得到一個好的物件導向來呈現的資料,所以我們可以將額外屬性和功能新增至物件關聯中。讓我們為SeatReservation類別使用自訂邏輯來呈現費用格式。

因為格式化費用的計算基於選擇餐點(meal)時,我們能使用ko.computed()來呈現此效果(這在餐點選擇改變時會自動更新):

self.formattedPrice = ko.computed(function() {
        var price = self.meal().price;
        return price ? "NTD $" + price.toFixed(2) : "NTD $0.0";        
});

下一步更新View,使用formattedPrice在每一個SeatReservation實體:

<tr>
    <td><input data-bind="value: name" /></td>
    <td><select data-bind="options: $root.availableMeals, value: meal, optionsText: 'mealName'"></select></td>
    <td data-bind="text: formattedPrice"></td>
</tr>

刪除項目

我們能新增項目,應該也能刪除它,我們只需要更新基礎資料。更新View,讓每一個項目呈現一個”刪除”連結:

<tr>
    <td><input data-bind="value: name" /></td>
    <td><select data-bind="options: $root.availableMeals, value: meal, optionsText: 'mealName'"></select></td>
    <td data-bind="text: formattedPrice"></td>
    <td><a href="#" data-bind="click: $root.removeSeat">刪除</a></td>
</tr>

注意$root,它讓Knockout.js搜尋removeSeat函式在頂層的ViewModel實體,而不被繫結在SeatReservation實體上。會需要這樣做的原因是,foreach繫結裡(<tbody>部分)處理的是SeatReservation實體,我們必須跟Knockout.js說明現在這個方法是位於ViewModel體裡而不是在SeatReservation實體。現在新增removeSeat函式到ViewModel類別(ReservationsViewModel):

// 新增
self.addSeat = function() {
    self.seats.push(new SeatReservation("", self.availableMeals[0]));
}
// 刪除
self.removeSeat = function(seat) { self.seats.remove(seat) }

重新執行應用程式,現在已經能夠刪除任一項目。

呈現額外費用總額

我們希望客戶知道他們的費用是否正確,對吧。我們會定義一個total計算屬性(computed),它會自動處理重新計算和更新UI的動作。

新增ko.computed屬性到ReservationsViewModel:

// 計算總費用
self.totalSurcharge = ko.computed(function() {
   var total = 0;
   for (var i = 0; i < self.seats().length; i++)
       total += self.seats()[i].meal().price;
   return total;
});

再次提醒,seats和meal都是可觀察物件,我們讀取與寫入必須像函式一樣,例如,self.seats().length取得現有多少項目,self.seats()[i].meal().price取得此項目餐點的額外費用。更新View,以呈現額外費用:

<h3 data-bind="visible: totalSurcharge() > 0">
    總計費用:NTD $<span data-bind="text: totalSurcharge().toFixed(2)"></span>
</h3>

這裡示範兩個作法:

  • visible繫結:這能決定是否讓元素呈現或不呈現,它是修改CSS的display屬性。後面我們以費用是否大於零來決定是否呈現此<h3>元素內容。
  • 你能使用任意的JavaScript運算式來決定繫結的結果。例如,「totalSurcharge() > 0」或「totalSurcharge().toFixed(2)」。

重新執行應用程式,選擇餐點後,Knockout.js會自動追蹤所有相關項目且更新UI上所有必須資訊。

最後調整

依循MVVM模式取得了物件導向表現的UI的資料和行為。你現在能在任何位置加上額外的行為,而且非常輕鬆自然。

例如,你想呈現訂位數量在標題上。現在,你不需要再撰寫其他額外程式碼來進行計數(count),只需要為<h2>標題加上簡單的text繫結:

<h2>座位預約(<span data-bind="text: seats().length"></span>)</h2>

不管新增或刪除,訂位數量會自動更新。

類似的需求,我們想要設定預約數量最大只有有5筆。我們能在View使用enable繫結來限制:

<button data-bind="click: addSeat, enable: seats().length < 5">新增訂位</button>

當訂位數量到達5筆時,enable會自動轉換為disable,button就無法再點擊,當按下刪除後,數量小於5筆時,狀態會由disable自動轉換為enable。Knockout.js運算式會自動處理,不需要再撰寫額外的程式碼。

2 則留言:

  1. 請教版主,
    關於使用 foreach 的方式產生元素,若 seats 是個會變動(ex:從資料庫而來),重新指定給 seats 屬性,
    那這樣是否會重新產生元素?

    回覆刪除
    回覆
    1. 類似的 MVVM Solution 是都"動態"繫結,也就是說,你的 ViewModel 一有異動,UI 會自動跟著異動。
      而 Model 通常都是透過 AJAX 方式後台進行互動。

      刪除

感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。