June 3, 2013

解決Twitter Bootstrap Date Picker無法更新KnockoutJS ViewModel問題

某個表單需要使用者填入出生年月日,格式為為yyyy-mm-dd。研究了幾個套件如Datepicker for Bootstrapbootstrap-datepickerDate/Time Picker for Twitter Boostrap,最後選用了Datepicker for Bootstrap。bootstrap-datepicker及Date/Time Picker for Twitter Boostrap都是從Datepicker for Bootstrap衍生而來,bootstrap-datepicker提供較Datepicker for Bootstrap多的函式,而Date/Time Picker for Twitter Boostrap增加了時間的選擇。

使用上很簡單,在需要使用date picker的textbox裡先設定其id(或是CSS類別),再透過jQuery呼叫datepicker函式,如
<input id="dob" type="text" data-date-format="yyyy-mm-dd" data-bind="value: DateOfBirth" />

$('#dob').datepicker();

設定成功的話,點選textbox就可以看到date picker出現在textbox下方。點選日期後也會將符合我們設定格式(yyyy-mm-dd)的日期顯示在textbox裡。


然而在使用上,會出現兩個問題。

第一個問題其實也不算真正的問題,比較算是操作上的偏好。你會發現在點選了date picker上的日期後date picker仍會停留在畫面上,直到textbox失去了focus,例如滑鼠點選了頁面其它地方。我個人比較喜歡在選完日期後,就讓date picker自動關閉。透過changeDate事件hide參數,在使用者選完日期後可將date picker關閉,如
$('#dob').datepicker().on('changeDate', function(e) {
    $(this).datepicker('hide');
});

第二個問題則是當使用者選完日期後,KnockoutJS的ViewModel(DateOfBirth屬性)並沒有被即時更新。原因是因為KnockoutJS預設是在控制項有先取得focus,資料變更並失去focus後才會更新ViewModel,而以date picker直接更新了textbox之值並沒有觸發change事件讓ViewModel被更新。要解決此問題,可以透過自訂KnockoutJS的custom binding,在日期被選擇後更新ViewModel。
        ko.bindingHandlers.datepicker = {
            init: function(element, valueAccessor, allBindingsAccessor) {
                var options = allBindingsAccessor().datepickerOptions || {};
                $(element).datepicker(options).on('changeDate', function(e) {
                    var observable = valueAccessor();
                    observable($(element).val());
                    $(element).datepicker("hide");
                });

                var value = ko.utils.unwrapObservable(valueAccessor());

                if (typeof (value) == "undefined" || value == "") {
                    return;
                }
                
                $(element).val(value);
                $(element).datepicker("setValue", value);
            }
        };

<input id="dob" type="text" data-bind="datepicker: DateOfBirth,
                   datepickerOptions: { 'format' : 'yyyy-mm-dd' }" />
KnockoutJS的custom binding分為兩個部份,initupdate。init主要用在初始化binding,update用於當ViewModel有更新時。詳細的參數說明可參考custom binding。在這裡我們僅需使用到init。

第3行:取得textbox裡的datapickerOptions binding,這個binding裡設定了date picker的options

第4~8行:將取得的options代入date picker中,設定當changeDate事件觸發時(即使用者選擇日期完),將textbox裡的值取出來並更新至ViewModel,最後再將date picker關閉

第10~17行:將ViewModel裡的屬性(DateOfBirth)值取出來,如果值不為undefined或是空字串,則將該值指派給textbox及date picker,因為ViewModel在一建立時可能有預設值

設定成功的話,選擇完日期date picker會自動關閉,ViewModel也會同時更新。

No comments: