Knockout教學4 - 自訂繫結

繫結(Bindings)

在Knockout.js來解釋MVVM,繫結是什麼內容要加入你的View與ViewModel。繫結是一媒介,它會雙向執行更新:

  • 繫結通知ViewModel改變且對應地更新View的DOM
  • 繫結截取DOM事件且對應地更新ViewModel屬性

Knockout.js有一套靈活和全面內建的繫結(像是text、click、foreach繫結等),但這不意味就此停止,你也可以使用少數程式碼來建立自訂繫結。在任何實際的應用程式中你會發現它有益來封裝常見的UI模式的繫結,這樣就能重覆的使用這些模式。

自訂繫結

例如,knockoutjs.com網站使用自訂繫結來封裝對話方塊,可拖動視窗,程式碼編輯器。

假設你有一個調查頁面的程式碼:

01function Answer(text) {
02 this.answerText = text;
03 this.points = ko.observable(1);
04}
05 
06function SurveyViewModel(question, pointsBudget, answers) {
07    var self = this;
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') };
12                        
13    self.pointsUsed = ko.computed(function() {
14        var total = 0;
15        for (var i = 0; i < this.answers.length; i++)
16            total += this.answers[i].points();
17        return total;       
18    }, this);
19}
20 
21ko.applyBindings(new SurveyViewModel("哪些因素會影響您的技術選擇?", 10, [
22   "功能、相容性、定價-那些無聊的東西",
23   "在駭客新聞上如何經常被提到",   
24   "是否容易學習與使用",       
25   "專案上的可信任度"
26]));

View:

01<h3 data-bind="text: question"></h3>
02<p>請將 <b data-bind="text: pointsBudget"></b> 點平均分配至選項。</p>
03 
04<table>
05    <thead><tr><th>選項</th><th>重要性</th></tr></thead>
06    <tbody data-bind="foreach: answers">
07        <tr>
08            <td data-bind="text: answerText"></td>
09            <td><select data-bind="options: [1,2,3,4,5], value: points"></select></td>
10        </tr>   
11    </tbody>
12</table>
13 
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繫結:

1ko.bindingHandlers.fadeVisible = {
2    update: function(element, valueAccessor) {
3        // 更新時,淡入或淡出
4        var shouldDisplay = valueAccessor();
5        shouldDisplay ? $(element).fadeIn() : $(element).fadeOut();
6    }
7};

update處理常式取得取得兩個元素的繫結,且回傳關聯資料的現值。基於現值,使用jQuery進行淡入或淡出效果。

下一步,修改View,使用自訂fadeVisible繫結:

1<h3 data-bind="fadeVisible: pointsUsed() > pointsBudget">你使用超過的點數,請刪減一些。</h3>

重新執行應用程式,測試分配超過的點數,你能看到提示訊息淡入和淡出的效果。

元素初始化

有一件事不是很正確,當頁面第一次載入時,提示訊息在載入瞬間是可見的並立即淡出(你可以快速按F5,可重現此問題),你需要使用init處理常式,確保該元素的初始狀態與初始ViewModel的資料相符合。

01ko.bindingHandlers.fadeVisible = {
02    init: function(element, valueAccessor) {
03        // 依初始值決定可見或不可見
04        var shouldDisplay = valueAccessor();
05        $(element).toggle(shouldDisplay);
06    },
07    update: function(element, valueAccessor) {
08        // // 更新時,淡入或淡出
09        var shouldDisplay = valueAccessor();
10        shouldDisplay ? $(element).fadeIn() : $(element).fadeOut();
11    }
12};

現在淡入或淡出動畫效果只會發生在ViewModel改變時。

現在fadeVisible自訂繫結是一個可以重覆使用的程式碼,你可以繼續建立其他的自訂繫結。以此範例而言,現在可以在應用程式的任何地方,把fadeVisible自訂繫結用來替代visible繫結,讓應用程式有更好更美觀的效果。

整合第三方元件

如果你想要在View裡使用其他JavaScript函式庫(例如,jQuery UI或YUI)且要繫結它們到ViewModel屬性,最簡單的方法就是建立自訂繫結。自訂繫結會介於ViewModel與第三方元件的中間。

這裡我們使用jQuery UI button widget(http://jqueryui.com/demos/button/)來改善【完成】按鈕。

在ViewModel最上方定義一個jqButton自訂繫結:

1ko.bindingHandlers.jqButton = {
2    init: function(element) {
3       $(element).button(); // 讓元素使用jQuery UI button樣式
4    }
5};

更新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的處理:

01ko.bindingHandlers.jqButton = {
02    init: function(element) {
03       $(element).button(); // 讓元素使用jQuery UI button樣式
04    },
05    update: function(element, valueAccessor) {
06        var currentValue = valueAccessor();
07        // 這裡我們只是更新"disabled"狀態,也可以更新其他屬性
08        $(element).button("option", "disabled", currentValue.enable === false);
09    }
10};

現在button在超過點數時會沒有作用。

再次提醒,jqButton繫結可以重覆使用在應用程式任何的button,讓你在ViewModel條件裡宣告button的enabled/disabled的狀態。

現在,你應該也可以開發其他自訂繫結來增強應用程式了。

開發自訂小工具(widgets)

最後來改善”重要性”的下拉式選單,改使用星星評分方式。你可以使用現有的外掛插件(例如,http://www.fyneworks.com/jquery/star-rating/)來替代,不過為了學習,讓我們從零開始。

在ViewModel上方自定義一個starRating繫結:

1ko.bindingHandlers.starRating = {
2    init: function(element, valueAccessor) {
3        $(element).addClass("starRating");
4        for (var i = 0; i < 5; i++)
5           $("<span>").appendTo(element);
6    }
7};

這段程式碼會安插數個<span>元素。以下是對應的CSS樣式:

1.starRating span {
2width: 24px;
3height: 24px;
5display: inline-block;
6cursor: pointer;
7background-position: -24px 0;
8}

更新View,將原始的<select>元素改為使用starRating繫結:

1<tbody data-bind="foreach: answers">
2    <tr>
3        <td data-bind="text: text"></td>
4        <td data-bind="starRating: points"></td>
5    </tr>   
6</tbody>

重新執行應用程式,現可以看到星星。

呈現評分

每當ViewModel變動時,星星評分要能自動更新狀態,我們可以使用update處理常式來處理合適的CSS類別給現有資料:

01function SurveyViewModel(question, pointsBudget, answers) {
02ko.bindingHandlers.starRating = {
03    init: function(element, valueAccessor) {
04        $(element).addClass("starRating");
05        for (var i = 0; i < 5; i++)
06           $("<span>").appendTo(element);
07    },
08    update: function(element, valueAccessor) {
09        // 給第一個 x 星級"chosen"類別,條件 x < = rating
10        var observable = valueAccessor();
11        $("span", element).each(function(index) {
12            $(this).toggleClass("chosen", index < observable());
13        });
14    }
15};

以下是對應的CSS樣式:

1.starRating span.chosen {
2 background-position: 0 0;
3}

重新執行應用程式,可以看到第一個星星預設的綠色。

滑行時高亮度提示

當訪客滑過星星時應該高亮度來突顯它們,這可以幫助他們選擇。這裡使用jQuery的hover函數來轉換星星的狀態:

01ko.bindingHandlers.starRating = {
02    init: function(element, valueAccessor) {
03        $(element).addClass("starRating");
04        for (var i = 0; i < 5; i++)
05           $("<span>").appendTo(element);
06 
07        // 處理滑鼠滑過星星
08        $("span", element).each(function(index) {
09            $(this).hover(
10                function() { $(this).prevAll().add(this).addClass("hoverChosen") },
11                function() { $(this).prevAll().add(this).removeClass("hoverChosen") }               
12            );
13        });
14    },
15    update: function(element, valueAccessor) {
16        // 給第一個 x 星級"chosen"類別,條件 x < = rating
17        var observable = valueAccessor();
18        $("span", element).each(function(index) {
19            $(this).toggleClass("chosen", index < observable());
20        });
21    }
22};

以下是對應的CSS樣式:

1.starRating:hover span.hoverChosen { background-position: 0 0; }

重新執行應用程式,現在可以看到滑鼠滑過的效果。

以下是完整的CSS:

1table { background-color: #cde; padding: 1em; border-radius: 0.5em; }
2table th { text-align:left; }
3table th:last-child { min-width: 130px; }
4 
5.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; }
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) {
2    $(this).hover(
3        function() { $(this).prevAll().add(this).addClass("hoverChosen") },
4        function() { $(this).prevAll().add(this).removeClass("hoverChosen") }               
5    ).click(function() {
6       var observable = valueAccessor();  // 取得關聯可觀察物件
7       observable(index+1);               // 寫入新評分
8     });
9});

重新執行應用程式,現在點擊的評分已經可以被儲存且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"


    回覆刪除

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