繫結(Bindings)
在Knockout.js來解釋MVVM,繫結是什麼內容要加入你的View與ViewModel。繫結是一媒介,它會雙向執行更新:
- 繫結通知ViewModel改變且對應地更新View的DOM
- 繫結截取DOM事件且對應地更新ViewModel屬性
Knockout.js有一套靈活和全面內建的繫結(像是text、click、foreach繫結等),但這不意味就此停止,你也可以使用少數程式碼來建立自訂繫結。在任何實際的應用程式中你會發現它有益來封裝常見的UI模式的繫結,這樣就能重覆的使用這些模式。
自訂繫結
例如,knockoutjs.com網站使用自訂繫結來封裝對話方塊,可拖動視窗,程式碼編輯器。
假設你有一個調查頁面的程式碼:
function Answer(text) { this.answerText = text; this.points = ko.observable(1); } function SurveyViewModel(question, pointsBudget, answers) { var self = this; self.question = question; self.pointsBudget = pointsBudget; self.answers = $.map(answers, function(text) { return new Answer(text) }); self.save = function() { alert('To do') }; self.pointsUsed = ko.computed(function() { var total = 0; for (var i = 0; i < this.answers.length; i++) total += this.answers[i].points(); return total; }, this); } ko.applyBindings(new SurveyViewModel("哪些因素會影響您的技術選擇?", 10, [ "功能、相容性、定價-那些無聊的東西", "在駭客新聞上如何經常被提到", "是否容易學習與使用", "專案上的可信任度" ]));
View:
<h3 data-bind="text: question"></h3> <p>請將 <b data-bind="text: pointsBudget"></b> 點平均分配至選項。</p> <table> <thead><tr><th>選項</th><th>重要性</th></tr></thead> <tbody data-bind="foreach: answers"> <tr> <td data-bind="text: answerText"></td> <td><select data-bind="options: [1,2,3,4,5], value: points"></select></td> </tr> </tbody> </table> <h3 data-bind="visible: pointsUsed() > pointsBudget">你使用超過的點數,請刪減一些。</h3> <p>你有 <b data-bind="text: pointsBudget - pointsUsed()"></b> 點可以使用。</p> <button data-bind="enable: pointsUsed() <= pointsBudget, click: save">完成</button>
現在,我們要來改善此應用程式:
- 使用動畫效果提示「你使用超過的點數,請刪減一些。」
- 改善「完成」按鈕的樣式
- 使用星星評分來換代下拉式選單給點數
使用動畫效果
當訪客分配太多點數時,「你使用超過的點數,請刪減一些。」提示訊息會出現,因為它是使用內建visible繫結。如果您想要使其淡入和淡出,可以編寫可重覆使用的自訂繫結,內部使用jQuery的淡入淡出函數來執行動畫。
你可以定義自訂繫結分配一個新屬性給ko.bindingHandlers物件。
屬性會有兩個回呼函數:
- init:當繫結第一次發生時呼叫。通常用來設定狀態或註冊事件處理常式等。
- update:當相關聯的資料有更新時呼叫。能用來更新符合的DOM元素。
在ViewModel類別最上面自定義fadeVisible繫結:
ko.bindingHandlers.fadeVisible = { update: function(element, valueAccessor) { // 更新時,淡入或淡出 var shouldDisplay = valueAccessor(); shouldDisplay ? $(element).fadeIn() : $(element).fadeOut(); } };
update處理常式取得取得兩個元素的繫結,且回傳關聯資料的現值。基於現值,使用jQuery進行淡入或淡出效果。
下一步,修改View,使用自訂fadeVisible繫結:
<h3 data-bind="fadeVisible: pointsUsed() > pointsBudget">你使用超過的點數,請刪減一些。</h3>
重新執行應用程式,測試分配超過的點數,你能看到提示訊息淡入和淡出的效果。
元素初始化
有一件事不是很正確,當頁面第一次載入時,提示訊息在載入瞬間是可見的並立即淡出(你可以快速按F5,可重現此問題),你需要使用init處理常式,確保該元素的初始狀態與初始ViewModel的資料相符合。
ko.bindingHandlers.fadeVisible = { init: function(element, valueAccessor) { // 依初始值決定可見或不可見 var shouldDisplay = valueAccessor(); $(element).toggle(shouldDisplay); }, update: function(element, valueAccessor) { // // 更新時,淡入或淡出 var shouldDisplay = valueAccessor(); shouldDisplay ? $(element).fadeIn() : $(element).fadeOut(); } };
現在淡入或淡出動畫效果只會發生在ViewModel改變時。
現在fadeVisible自訂繫結是一個可以重覆使用的程式碼,你可以繼續建立其他的自訂繫結。以此範例而言,現在可以在應用程式的任何地方,把fadeVisible自訂繫結用來替代visible繫結,讓應用程式有更好更美觀的效果。
整合第三方元件
如果你想要在View裡使用其他JavaScript函式庫(例如,jQuery UI或YUI)且要繫結它們到ViewModel屬性,最簡單的方法就是建立自訂繫結。自訂繫結會介於ViewModel與第三方元件的中間。
這裡我們使用jQuery UI button widget(http://jqueryui.com/demos/button/)來改善【完成】按鈕。
在ViewModel最上方定義一個jqButton自訂繫結:
ko.bindingHandlers.jqButton = { init: function(element) { $(element).button(); // 讓元素使用jQuery UI button樣式 } };
更新View,使用jqButton自訂繫結:
<button data-bind="jqButton: true, enable: pointsUsed() <= pointsBudget, click: save">完成</button>
現在我們可以看到button樣式已經改善。
更改button的狀態
當訪客分配超過的點數時,應該停用button。enable繫結無法直接與jQuery UI button合作,因為jQuery UI button不會自動回應的慣用的HTML disabled屬性,替代的是,jQuery UI button使用特別的API來控制它們enabled/disabled的呈現。
沒關係,我們可以使用update處理常式來進行enabled/disabled的處理:
ko.bindingHandlers.jqButton = { init: function(element) { $(element).button(); // 讓元素使用jQuery UI button樣式 }, update: function(element, valueAccessor) { var currentValue = valueAccessor(); // 這裡我們只是更新"disabled"狀態,也可以更新其他屬性 $(element).button("option", "disabled", currentValue.enable === false); } };
現在button在超過點數時會沒有作用。
再次提醒,jqButton繫結可以重覆使用在應用程式任何的button,讓你在ViewModel條件裡宣告button的enabled/disabled的狀態。
現在,你應該也可以開發其他自訂繫結來增強應用程式了。
開發自訂小工具(widgets)
最後來改善”重要性”的下拉式選單,改使用星星評分方式。你可以使用現有的外掛插件(例如,http://www.fyneworks.com/jquery/star-rating/)來替代,不過為了學習,讓我們從零開始。
在ViewModel上方自定義一個starRating繫結:
ko.bindingHandlers.starRating = { init: function(element, valueAccessor) { $(element).addClass("starRating"); for (var i = 0; i < 5; i++) $("<span>").appendTo(element); } };
這段程式碼會安插數個<span>元素。以下是對應的CSS樣式:
.starRating span { width: 24px; height: 24px; background-image: url(http://learn.knockoutjs.com/Content/TutorialSpecific/stars.png); display: inline-block; cursor: pointer; background-position: -24px 0; }
更新View,將原始的<select>元素改為使用starRating繫結:
<tbody data-bind="foreach: answers"> <tr> <td data-bind="text: text"></td> <td data-bind="starRating: points"></td> </tr> </tbody>
重新執行應用程式,現可以看到星星。
呈現評分
每當ViewModel變動時,星星評分要能自動更新狀態,我們可以使用update處理常式來處理合適的CSS類別給現有資料:
function SurveyViewModel(question, pointsBudget, answers) { ko.bindingHandlers.starRating = { init: function(element, valueAccessor) { $(element).addClass("starRating"); for (var i = 0; i < 5; i++) $("<span>").appendTo(element); }, update: function(element, valueAccessor) { // 給第一個 x 星級"chosen"類別,條件 x < = rating var observable = valueAccessor(); $("span", element).each(function(index) { $(this).toggleClass("chosen", index < observable()); }); } };
以下是對應的CSS樣式:
.starRating span.chosen { background-position: 0 0; }
重新執行應用程式,可以看到第一個星星預設的綠色。
滑行時高亮度提示
當訪客滑過星星時應該高亮度來突顯它們,這可以幫助他們選擇。這裡使用jQuery的hover函數來轉換星星的狀態:
ko.bindingHandlers.starRating = { init: function(element, valueAccessor) { $(element).addClass("starRating"); for (var i = 0; i < 5; i++) $("<span>").appendTo(element); // 處理滑鼠滑過星星 $("span", element).each(function(index) { $(this).hover( function() { $(this).prevAll().add(this).addClass("hoverChosen") }, function() { $(this).prevAll().add(this).removeClass("hoverChosen") } ); }); }, update: function(element, valueAccessor) { // 給第一個 x 星級"chosen"類別,條件 x < = rating var observable = valueAccessor(); $("span", element).each(function(index) { $(this).toggleClass("chosen", index < observable()); }); } };
以下是對應的CSS樣式:
.starRating:hover span.hoverChosen { background-position: 0 0; }
重新執行應用程式,現在可以看到滑鼠滑過的效果。
以下是完整的CSS:
table { background-color: #cde; padding: 1em; border-radius: 0.5em; } table th { text-align:left; } table th:last-child { min-width: 130px; } .starRating span { width:24px; height:24px; background-image: url(http://learn.knockoutjs.com/Content/TutorialSpecific/stars.png); display:inline-block; cursor: pointer; background-position: -24px 0; } .starRating span.chosen { background-position: 0 0; } .starRating:hover span { background-position: -24px 0; } .starRating:hover span.hoverChosen { background-position: 0 0; }
將資料回傳ViewModel
當訪客點擊星星,將會儲存選擇的評分到ViewModel,然後自動更新UI。這裡使用jQuery的click函數來截取點擊:
$("span", element).each(function(index) { $(this).hover( function() { $(this).prevAll().add(this).addClass("hoverChosen") }, function() { $(this).prevAll().add(this).removeClass("hoverChosen") } ).click(function() { var observable = valueAccessor(); // 取得關聯可觀察物件 observable(index+1); // 寫入新評分 }); });
重新執行應用程式,現在點擊的評分已經可以被儲存且UI相關可觀察物件會連動的更新。
參考資料
http://learn.knockoutjs.com/#/?tutorial=custombindings
Dear
回覆刪除在「更改button的狀態」小節中
一旦換成 jQueryUI 的 button 之後
連帶 HTML 標籤也要修改,不然無法正確呈現
也就是原本 data-bind="jqButton: true, enable: pointsUsed() <= pointsBudget, click: save"
要改成 data-bind="jqButton: {enable: pointsUsed() <= pointsBudget}, click: save"
這一點在官方的教學有提到,再麻煩確認一下
感謝提醒。
回覆刪除官方教學可能隨時有更新,我只是當下翻譯(都9/27/2012年的事了),一切還是請以官方為主。
裡面有一個 更新View,將原始的select元素改為使用starRating繫結:
回覆刪除的html語法裡面:
data-bind="text: text"
是否該改為:answerText
data-bind="text: answerText"