Ostatnio w projekcie potrzebowaliśmy wykorzystać kontrolkę umożliwiającą wybór daty jak i godziny danego zdarzenia. Jako, że całe UI oparte jest o Bootsratp – szukaliśmy czegoś co będzie zgodne z framework a przy okazji nie będzie miało tylu problemów co poprzednia kontrolka.

Udało nam się znaleźć dwie kontrolki, które spełniały nasze oczekiwania – DateTime Picker i Date/Time Picker (tak, wiem, nazewnictwo powala ;)).

Nie licząc różnicy w API (małej) jak i w UI (też małej), obie się dobrze sprawdzają, choć Date/Time Picker oparty jest na wersji kodu, który przyprawiał mnie o ból głowy – około 70 bugów (DateTime Picker na poprawionej wersji tej samej kontrolki). Kontrolka ta została nawet ostatnio zaktualizowana, jednak wciąż posiada stare bugi – ostatni został poprawiony 8 dni temu.

Tak czy siak, zdecydowaliśmy się na pójście w DateTime Picker.

By umilić sobie z nim pracę stworzyliśmy prosty binder, który umożliwia bindowanie wartości wybranej do knockout i na odwrót.

Jednak sprawa nie była tak prosta na jaką się wydawała, że będzie. Mianowicie kontrolka nie wywołuje eventu changeDate jeżeli datę się ręcznie poda – wpisze. Więc wykorzystanie przechwytywania daty na changeDate jest nie opłacalne – ale wciąż konieczne pod pewnym warunkiem.

Trzeba przechwycić event na zmianę pola tekstowego, ale tutaj następuje problem: jedne pole może być tekstowe drugie może być komponentem – czyli bez bezpośredniego dostępu do pola input.

Wystarczyło trochę małej magii i tak o to mamy gotowy binding:

ko.bindingHandlers.dtpicker = {
    init: function (element, valueAccessor, allBindingsAccessor, context) {
        var options = {
            weekStart: 1,
                format: 'dd/mm/yyyy',
                autoclose: true,
                startView: 2,
                minView: 2,
                todayBtn: 'linked',
                pickerPosition: 'bottom-left'
            }, 
            el = $(element), 
            opt = allBindingsAccessor().dtOptions,
            elData,
            eventName,
            bindingEl,
            isObservable = ko.isObservable(valueAccessor());

        if (opt){
            
            if (opt.max) {

                if (opt.max == 'today') {
                    options.endDate = new Date();
                } else if (opt.max instanceof Date) {
                    options.endDate = opt.max;
                }
            }
            
            if (opt.time) {
                options.minView = 0;
            }
        }

        el.datetimepicker(options);
        elData = el.data().datetimepicker;
        

        // we dont want to handle change twice: on input or on datepicker
        if(elData.isInput) {
            // we can bind to input
            bindingEl = elData.element[0];
            eventName = 'change';
        } else if (elData.component) {
            // or to compnent
            bindingEl = elData.element.find('input')[0];
            eventName = 'change';
        } else {
            // this means that we do not have input filed, but we want to handle it
            bindingEl = elData.element[0];
            eventName = 'changeDate';
        }
        
        if (bindingEl) {
            ko.utils.registerEventHandler(bindingEl, eventName, function() {
                var observable = valueAccessor(),
                    val = elData.date;
            
                if(val) {
                    isObservable ? observable(val) : observable = val;
                } else {
                    isObservable ? observable(null) : observable = null;
                }
            });
        }
        
        ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
            var data = el.data().datetimepicker;
            
            if (bindingEl) {
                $(bindingEl).off(eventName);
            }
            
            data.remove();
            el.removeData('datetimepicker');
            data = undefined;
        });
    },
    update: function (element, valueAccessor, allBindingsAccessor, context) {
        var el = $(element),
            observable = valueAccessor(),
            value, 
            dp = el.data().datetimepicker, 
            current = dp.date;
        
        if (ko.isObservable(observable)) {
            value = ko.utils.unwrapObservable(observable);
        } else {
            value = observable;
        }

        // convert to date
        if (value && !(value instanceof Date)) {
            value = el.datetimepicker.DPGlobal.parseDate(value, dp.format, dp.language, dp.formatType);
        }
        
        if (value - current !== 0) {
            if (value) {
                dp.update(value);
            } else {
                dp.reset();
            }   
        }
    }
};

Oczywiście, tutaj można wiele rzeczy dodać. My obsługujemy jedynie opcje maksymalnej daty oraz czy czas ma zostać dodany. Jak chcecie więcej opcji to nic nie stoi na przeszkodzie by je dodać. Zamiast custom możecie wykorzystać $.extend i podawać domyślne ustawienia kontrolki. Droga wolna :)

Może więc to się komuś przyda :)