繫結(Bindings)
在Knockout.js來解釋MVVM,繫結是什麼內容要加入你的View與ViewModel。繫結是一媒介,它會雙向執行更新:
- 繫結通知ViewModel改變且對應地更新View的DOM
- 繫結截取DOM事件且對應地更新ViewModel屬性
Knockout.js有一套靈活和全面內建的繫結(像是text、click、foreach繫結等),但這不意味就此停止,你也可以使用少數程式碼來建立自訂繫結。在任何實際的應用程式中你會發現它有益來封裝常見的UI模式的繫結,這樣就能重覆的使用這些模式。
自訂繫結
例如,knockoutjs.com網站使用自訂繫結來封裝對話方塊,可拖動視窗,程式碼編輯器。
假設你有一個調查頁面的程式碼:
01 | function Answer(text) { |
02 | this .answerText = text; |
03 | this .points = ko.observable(1); |
06 | function SurveyViewModel(question, pointsBudget, answers) { |
08 | self.question = question; |
09 | self.pointsBudget = pointsBudget; |
10 | self.answers = $.map(answers, function (text) { return new Answer(text) }); |
11 | self.save = function () { alert( 'To do' ) }; |
13 | self.pointsUsed = ko.computed( function () { |
15 | for ( var i = 0; i < this .answers.length; i++) |
16 | total += this .answers[i].points(); |
21 | ko.applyBindings( new SurveyViewModel( "哪些因素會影響您的技術選擇?" , 10, [ |
View:
01 | < h3 data-bind = "text: question" ></ h3 > |
02 | < p >請將 < b data-bind = "text: pointsBudget" ></ b > 點平均分配至選項。</ p > |
05 | < thead >< tr >< th >選項</ th >< th >重要性</ th ></ tr ></ thead > |
06 | < tbody data-bind = "foreach: answers" > |
08 | < td data-bind = "text: answerText" ></ td > |
09 | < td >< select data-bind = "options: [1,2,3,4,5], value: points" ></ select ></ td > |
14 | < h3 data-bind="visible: pointsUsed() > pointsBudget">你使用超過的點數,請刪減一些。</ h3 > |
15 | < p >你有 < b data-bind = "text: pointsBudget - pointsUsed()" ></ b > 點可以使用。</ p > |
16 | < button data-bind = "enable: pointsUsed() <= pointsBudget, click: save" >完成</ button > |
現在,我們要來改善此應用程式:
- 使用動畫效果提示「你使用超過的點數,請刪減一些。」
- 改善「完成」按鈕的樣式
- 使用星星評分來換代下拉式選單給點數
使用動畫效果
當訪客分配太多點數時,「你使用超過的點數,請刪減一些。」提示訊息會出現,因為它是使用內建visible繫結。如果您想要使其淡入和淡出,可以編寫可重覆使用的自訂繫結,內部使用jQuery的淡入淡出函數來執行動畫。
你可以定義自訂繫結分配一個新屬性給ko.bindingHandlers物件。
屬性會有兩個回呼函數:
- init:當繫結第一次發生時呼叫。通常用來設定狀態或註冊事件處理常式等。
- update:當相關聯的資料有更新時呼叫。能用來更新符合的DOM元素。
在ViewModel類別最上面自定義fadeVisible繫結:
1 | ko.bindingHandlers.fadeVisible = { |
2 | update: function (element, valueAccessor) { |
4 | var shouldDisplay = valueAccessor(); |
5 | shouldDisplay ? $(element).fadeIn() : $(element).fadeOut(); |
update處理常式取得取得兩個元素的繫結,且回傳關聯資料的現值。基於現值,使用jQuery進行淡入或淡出效果。
下一步,修改View,使用自訂fadeVisible繫結:
1 | < h3 data-bind="fadeVisible: pointsUsed() > pointsBudget">你使用超過的點數,請刪減一些。</ h3 > |
重新執行應用程式,測試分配超過的點數,你能看到提示訊息淡入和淡出的效果。
元素初始化
有一件事不是很正確,當頁面第一次載入時,提示訊息在載入瞬間是可見的並立即淡出(你可以快速按F5,可重現此問題),你需要使用init處理常式,確保該元素的初始狀態與初始ViewModel的資料相符合。
01 | ko.bindingHandlers.fadeVisible = { |
02 | init: function (element, valueAccessor) { |
04 | var shouldDisplay = valueAccessor(); |
05 | $(element).toggle(shouldDisplay); |
07 | update: function (element, valueAccessor) { |
09 | var shouldDisplay = valueAccessor(); |
10 | 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自訂繫結:
1 | ko.bindingHandlers.jqButton = { |
2 | init: function (element) { |
更新View,使用jqButton自訂繫結:
1 | < 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的處理:
01 | ko.bindingHandlers.jqButton = { |
02 | init: function (element) { |
05 | update: function (element, valueAccessor) { |
06 | var currentValue = valueAccessor(); |
08 | $(element).button( "option" , "disabled" , currentValue.enable === false ); |
現在button在超過點數時會沒有作用。
再次提醒,jqButton繫結可以重覆使用在應用程式任何的button,讓你在ViewModel條件裡宣告button的enabled/disabled的狀態。
現在,你應該也可以開發其他自訂繫結來增強應用程式了。
開發自訂小工具(widgets)
最後來改善”重要性”的下拉式選單,改使用星星評分方式。你可以使用現有的外掛插件(例如,http://www.fyneworks.com/jquery/star-rating/)來替代,不過為了學習,讓我們從零開始。
在ViewModel上方自定義一個starRating繫結:
1 | ko.bindingHandlers.starRating = { |
2 | init: function (element, valueAccessor) { |
3 | $(element).addClass( "starRating" ); |
4 | for ( var i = 0; i < 5; i++) |
5 | $( "<span>" ).appendTo(element); |
這段程式碼會安插數個<span>元素。以下是對應的CSS樣式:
7 | background-position : -24px 0 ; |
更新View,將原始的<select>元素改為使用starRating繫結:
1 | < tbody data-bind = "foreach: answers" > |
3 | < td data-bind = "text: text" ></ td > |
4 | < td data-bind = "starRating: points" ></ td > |
重新執行應用程式,現可以看到星星。
呈現評分
每當ViewModel變動時,星星評分要能自動更新狀態,我們可以使用update處理常式來處理合適的CSS類別給現有資料:
01 | function SurveyViewModel(question, pointsBudget, answers) { |
02 | ko.bindingHandlers.starRating = { |
03 | init: function (element, valueAccessor) { |
04 | $(element).addClass( "starRating" ); |
05 | for ( var i = 0; i < 5; i++) |
06 | $( "<span>" ).appendTo(element); |
08 | update: function (element, valueAccessor) { |
10 | var observable = valueAccessor(); |
11 | $( "span" , element).each( function (index) { |
12 | $( this ).toggleClass( "chosen" , index < observable()); |
以下是對應的CSS樣式:
1 | .starRating span.chosen { |
2 | background-position : 0 0 ; |
重新執行應用程式,可以看到第一個星星預設的綠色。
滑行時高亮度提示
當訪客滑過星星時應該高亮度來突顯它們,這可以幫助他們選擇。這裡使用jQuery的hover函數來轉換星星的狀態:
01 | ko.bindingHandlers.starRating = { |
02 | init: function (element, valueAccessor) { |
03 | $(element).addClass( "starRating" ); |
04 | for ( var i = 0; i < 5; i++) |
05 | $( "<span>" ).appendTo(element); |
08 | $( "span" , element).each( function (index) { |
10 | function () { $( this ).prevAll().add( this ).addClass( "hoverChosen" ) }, |
11 | function () { $( this ).prevAll().add( this ).removeClass( "hoverChosen" ) } |
15 | update: function (element, valueAccessor) { |
17 | var observable = valueAccessor(); |
18 | $( "span" , element).each( function (index) { |
19 | $( this ).toggleClass( "chosen" , index < observable()); |
以下是對應的CSS樣式:
1 | .starRating:hover span.hoverChosen { background-position : 0 0 ; } |
重新執行應用程式,現在可以看到滑鼠滑過的效果。
以下是完整的CSS:
1 | table { background-color : #cde ; padding : 1em ; border-radius: 0.5em ; } |
2 | table th { text-align : left ; } |
3 | table th:last-child { min-width : 130px ; } |
6 | .starRating span.chosen { background-position : 0 0 ; } |
7 | .starRating:hover span { background-position : -24px 0 ; } |
8 | .starRating:hover span.hoverChosen { background-position : 0 0 ; } |
將資料回傳ViewModel
當訪客點擊星星,將會儲存選擇的評分到ViewModel,然後自動更新UI。這裡使用jQuery的click函數來截取點擊:
1 | $( "span" , element).each( function (index) { |
3 | function () { $( this ).prevAll().add( this ).addClass( "hoverChosen" ) }, |
4 | function () { $( this ).prevAll().add( this ).removeClass( "hoverChosen" ) } |
6 | var observable = valueAccessor(); |
重新執行應用程式,現在點擊的評分已經可以被儲存且UI相關可觀察物件會連動的更新。
參考資料
http://learn.knockoutjs.com/#/?tutorial=custombindings
- Knockout教學1 - Knockout.js與MVVM模式簡介
- Knockout.js教學2 - 清單與集合
- Knockout教學3 - Single Page Applications, SPA
- Knockout教學4 - 自訂繫結
- Knockout教學5 - 由伺服器載入與儲存資料
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"