Knockout教學4 - 自訂繫結

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

  1. Knockout教學1 - Knockout.js與MVVM模式簡介
  2. Knockout.js教學2 - 清單與集合
  3. Knockout教學3 - Single Page Applications, SPA
  4. Knockout教學4 - 自訂繫結
  5. Knockout教學5 - 由伺服器載入與儲存資料

3 則留言:

  1. Dear
    在「更改button的狀態」小節中
    一旦換成 jQueryUI 的 button 之後
    連帶 HTML 標籤也要修改,不然無法正確呈現
    也就是原本 data-bind="jqButton: true, enable: pointsUsed() <= pointsBudget, click: save"
    要改成 data-bind="jqButton: {enable: pointsUsed() <= pointsBudget}, click: save"
    這一點在官方的教學有提到,再麻煩確認一下

    回覆刪除
  2. 感謝提醒。
    官方教學可能隨時有更新,我只是當下翻譯(都9/27/2012年的事了),一切還是請以官方為主。

    回覆刪除
  3. 裡面有一個 更新View,將原始的select元素改為使用starRating繫結:

    的html語法裡面:
    data-bind="text: text"

    是否該改為:answerText
    data-bind="text: answerText"


    回覆刪除

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