清單與集合
我們經常在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運算式會自動處理,不需要再撰寫額外的程式碼。
請教版主,
回覆刪除關於使用 foreach 的方式產生元素,若 seats 是個會變動(ex:從資料庫而來),重新指定給 seats 屬性,
那這樣是否會重新產生元素?
類似的 MVVM Solution 是都"動態"繫結,也就是說,你的 ViewModel 一有異動,UI 會自動跟著異動。
刪除而 Model 通常都是透過 AJAX 方式後台進行互動。