繫結(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"