135

I'm trying to use KnockoutJS with jQuery UI. I have an input element with a datepicker attached. I'm currently running knockout.debug.1.2.1.js and it seems that the change event is never being caught by Knockout. The element looks like this:

<input type="text" class="date" data-bind="value: RedemptionExpiration"/>

I've even tried changing the valueUpdate event type but to no avail. It seems like Chrome causes a focus event just before it changes the value, but IE doesn't.

Is there some Knockout method that "rebinds all the bindings"? I technically only need the value changed before I send it back to the server. So I could live with that kind of workaround.

I think the problem's the datepicker's fault, but I can't figure out how to fix this.

Any ideas?

Jeroen
  • 60,696
  • 40
  • 206
  • 339
Jose
  • 10,891
  • 19
  • 67
  • 89

13 Answers13

253

I think that for the jQuery UI datepicker it is preferable to use a custom binding that will read/write with Date objects using the APIs provided by the datepicker.

The binding might look like (from my answer here):

ko.bindingHandlers.datepicker = {
    init: function(element, valueAccessor, allBindingsAccessor) {
        //initialize datepicker with some optional options
        var options = allBindingsAccessor().datepickerOptions || {},
            $el = $(element);

        $el.datepicker(options);

        //handle the field changing by registering datepicker's changeDate event
        ko.utils.registerEventHandler(element, "changeDate", function () {
            var observable = valueAccessor();
            observable($el.datepicker("getDate"));
        });

        //handle disposal (if KO removes by the template binding)
        ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
            $el.datepicker("destroy");
        });

    },
    update: function(element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor()),
            $el = $(element);

        //handle date data coming via json from Microsoft
        if (String(value).indexOf('/Date(') == 0) {
            value = new Date(parseInt(value.replace(/\/Date\((.*?)\)\//gi, "$1")));
        }

        var current = $el.datepicker("getDate");

        if (value - current !== 0) {
            $el.datepicker("setDate", value);
        }
    }
};

You would use it like:

<input data-bind="datepicker: myDate, datepickerOptions: { minDate: new Date() }" />

Sample in jsFiddle here: http://jsfiddle.net/rniemeyer/NAgNV/

Community
  • 1
  • 1
RP Niemeyer
  • 114,592
  • 18
  • 291
  • 211
  • 21
    What I love is that you didn't cut corners in this binder, like with the dispose callback. A sound example to follow on the road to KnockoutJS mastery! – Dav Mar 13 '12 at 01:50
  • 2
    And what about the datepicker binded to an element that is dinamically created... i mean, the datepicker with a live handler. – Phoenix_uy Sep 17 '12 at 19:34
  • I love this answer, however, I can't get this to work well with Eric Barnard's custom validator extenders. github.com/ericmbarnard/Knockout-Validation . Once you add the additional fix mentioned here: https://github.com/ericmbarnard/Knockout-Validation/issues/69 everything works great, except any invalid input will return today's date to the validators. Example, I enter 1/1/1/1/1, the date validator will log today's date as what is being validated. – Brad M Nov 07 '12 at 17:54
  • Have a version that is working with validation, will post it inline below. – Brad M Nov 07 '12 at 18:45
  • For me this seems to work everywhere, but not in IE.. http://stackoverflow.com/questions/13744270/knockout-js-with-jquery-ui-datepicker-works-everywhere-except-ie – mat Dec 06 '12 at 13:19
  • The fiddle was using an old version of jQuery. I updated it: http://jsfiddle.net/rniemeyer/NAgNV/ – RP Niemeyer Dec 06 '12 at 14:08
  • 6
    Phoenix_uy: For the datepicker to work with dynamically created objects, be sure not to set the ID or the Name of the input. – James Reategui Jan 17 '13 at 16:52
  • Warning: in the given 'update' method, the variable 'current' is global. Missing a 'var' keyword there ;) – jpatte Apr 06 '13 at 11:18
  • 1
    I am using this and it is working perfectly except for one small thing - If I set the minDate or maxDate equal to an observable it does not get updated if that observable is changed (ex. if I have two datepickers where the max date of the first is the value of the second, if I update the second it does not update the max date of the first) same as this question http://stackoverflow.com/questions/14732204/how-to-link-2-jquery-ui-datepickers-with-knockout?rq=1 – PW Kad Jun 11 '13 at 20:56
  • 1
    Thanks! Is there any change to dynamically change the datepicker options? i.e.: a selected dropdown value should change the minDate property of the datepicker? – chris vietor Jul 16 '13 at 16:37
  • I really have to study and practice custom bindings. This was driving me insane for a while. Dynamite code block. – Joel Etherton Jun 13 '14 at 17:33
  • 1
    how to set datepicker option dynamically using the above code.? everythig work fine except options set dynamically.. – Kuttan Sujith Jun 16 '15 at 05:40
  • 2
    looks like the event name is wrong, ko.utils.registerEventHandler(element, "changeDate", function () - should be ko.utils.registerEventHandler(element, "change", function () – Adam Bilinski Jan 14 '16 at 13:14
13

Here is a version of RP Niemeyer's answer that will work with the knockout validation scripts found here: http://github.com/ericmbarnard/Knockout-Validation

ko.bindingHandlers.datepicker = {
    init: function (element, valueAccessor, allBindingsAccessor) {
        //initialize datepicker with some optional options
        var options = allBindingsAccessor().datepickerOptions || {};
        $(element).datepicker(options);

        //handle the field changing
        ko.utils.registerEventHandler(element, "change", function () {
            var observable = valueAccessor();
            observable($(element).val());
            if (observable.isValid()) {
                observable($(element).datepicker("getDate"));

                $(element).blur();
            }
        });

        //handle disposal (if KO removes by the template binding)
        ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
            $(element).datepicker("destroy");
        });

        ko.bindingHandlers.validationCore.init(element, valueAccessor, allBindingsAccessor);

    },
    update: function (element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());

        //handle date data coming via json from Microsoft
        if (String(value).indexOf('/Date(') == 0) {
            value = new Date(parseInt(value.replace(/\/Date\((.*?)\)\//gi, "$1")));
        }

        current = $(element).datepicker("getDate");

        if (value - current !== 0) {
            $(element).datepicker("setDate", value);
        }
    }
};

Changes are to the change event handler to pass the entered value and not the date to the validation scripts first, then only setting the date to the observable if it is valid. I also added the validationCore.init that is needed for custom bindings discussed here:

http://github.com/ericmbarnard/Knockout-Validation/issues/69

I also added rpenrose's suggestion for a blur on change to eliminate some pesky datepicker scenarios getting in the way of things.

Brad M
  • 825
  • 2
  • 12
  • 14
  • 2
    Doesn't seem to work for me, I get TypeError: observable.isModified is not a function on line 313 of knockout.validation.js. Small example here: http://frikod.se/~capitol/fel/test.html – Alexander Kjäll Apr 18 '13 at 11:18
  • The important line to get it working with the validation library is: ko.bindingHandlers.validationCore.init(element, valueAccessor, allBindingsAccessor); – CRice Jan 15 '15 at 23:33
11

I've used a different approach. Since knockout.js doesn't seem to fire the event on change, I've forced the datepicker to call change() for its input once closed.

$(".date").datepicker({
    onClose: function() {
        $(this).change(); // Forces re-validation
    }
});
ThiagoPXP
  • 5,362
  • 3
  • 31
  • 44
  • 1
    $('.datepicker').datepicker({ onSelect: function(dateText) { $("#date_in").trigger("change"); } }); – elsadek Jul 20 '14 at 05:40
9

Although all of these answers saved me a lot of work, none of them fully worked for me. After selecting a date, the binded value would not update. I could only get it to update when changing the date value using the keyboard then clicking out of the input box. I fixed this by augmenting RP Niemeyer's code with syb's code to get:

ko.bindingHandlers.datepicker = {
        init: function (element, valueAccessor, allBindingsAccessor) {
            //initialize datepicker with some optional options
            var options = allBindingsAccessor().datepickerOptions || {};

            var funcOnSelectdate = function () {
                var observable = valueAccessor();
                observable($(element).datepicker("getDate"));
            }

            options.onSelect = funcOnSelectdate;

            $(element).datepicker(options);

            //handle the field changing
            ko.utils.registerEventHandler(element, "change", funcOnSelectdate);

            //handle disposal (if KO removes by the template binding)
            ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
                $(element).datepicker("destroy");
            });

        },
        update: function (element, valueAccessor) {

            var value = ko.utils.unwrapObservable(valueAccessor());
            if (typeof(value) === "string") { // JSON string from server
                value = value.split("T")[0]; // Removes time
            }

            var current = $(element).datepicker("getDate");

            if (value - current !== 0) {
                var parsedDate = $.datepicker.parseDate('yy-mm-dd', value);
                $(element).datepicker("setDate", parsedDate);
            }
        }
    };

I suspect putting the observable($(element).datepicker("getDate")); statement in its own function and registering that with options.onSelect did the trick?

brudert
  • 537
  • 8
  • 21
  • 2
    Thanks a million! I tried every example and then found this one at the bottom of the page and it finally works. I just have a small tweak in mine so that the bound value stays in the same "server friendly" format that it came down in. In your funcOnSelectdate function use this: observable($.datepicker.formatDate('yy-mm-dd', $(element).datepicker('getDate'))); – BrutalDev Jun 01 '13 at 11:08
  • I think if you override the `onSelect` function it won't raise the `change` event... – NickL Mar 05 '14 at 13:30
6

Thanks for this article I found it very useful.

If you want the DatePicker to behave exactly like the JQuery UI default behaviour I recommend adding a blur on the element in the change event handler:

i.e.

    //handle the field changing
    ko.utils.registerEventHandler(element, "change", function () {
        var observable = valueAccessor();
        observable($(element).datepicker("getDate"));

        $(element).blur();

    });
rpenrose
  • 61
  • 1
  • 2
  • This answer doesn't look complete? Is this a comment on @RPNiemeyer's answer, or someone else's? – rjmunro Jun 12 '12 at 13:03
3

I solved this problem by changing the order of my included script files:

<script src="@Url.Content("~/Scripts/jquery-ui-1.10.2.custom.js")"></script>
<script src="@Url.Content("~/Scripts/knockout-2.2.1.js")"></script>
Susanna
  • 31
  • 3
  • Had similar issues with model not being updated even though the input rendered the correctly chosen date from the datepicker. Started down the list of suggestions.. but.. this was definitely my problem. Hmmm.. my MVC project has had the KO script ahead of the jquery and jquery UI scripts for a long time - will have to test thoroughly. – bkwdesign Mar 14 '14 at 13:53
2

Same as RP Niemeyer, but better support of WCF DateTime, Timezones and Using the DatePicker onSelect JQuery property.

        ko.bindingHandlers.datepicker = {
        init: function (element, valueAccessor, allBindingsAccessor) {
            //initialize datepicker with some optional options
            var options = allBindingsAccessor().datepickerOptions || {};

            var funcOnSelectdate = function () {
                var observable = valueAccessor();
                var d = $(element).datepicker("getDate");
                var timeInTicks = d.getTime() + (-1 * (d.getTimezoneOffset() * 60 * 1000));

                observable("/Date(" + timeInTicks + ")/");
            }
            options.onSelect = funcOnSelectdate;

            $(element).datepicker(options);

            //handle the field changing
            ko.utils.registerEventHandler(element, "change", funcOnSelectdate);

            //handle disposal (if KO removes by the template binding)
            ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
                $(element).datepicker("destroy");
            });

        },
        update: function (element, valueAccessor) {
            var value = ko.utils.unwrapObservable(valueAccessor());

            //handle date data coming via json from Microsoft
            if (String(value).indexOf('/Date(') == 0) {
                value = new Date(parseInt(value.replace(/\/Date\((.*?)\)\//gi, "$1")));
            }

            current = $(element).datepicker("getDate");

            if (value - current !== 0) {
                $(element).datepicker("setDate", value);
            }
        }
    };

Enjoy :)

http://jsfiddle.net/yechezkelbr/nUdYH/

syb
  • 369
  • 2
  • 5
1

I think it can be done much easier: <input data-bind="value: myDate, datepicker: myDate, datepickerOptions: {}" />

So you do not need manual change handling in init function.

But in this case, your 'myDate' variable will get only visible value, not Date object.

mot
  • 115
  • 1
  • 7
1

Alternatively, you can specify this in binding:

Update:

 function (element, valueAccessor) {
    var value = ko.utils.unwrapObservable(valueAccessor()),
        current = $(element).datepicker("getDate");

    if (typeof value === "string") {            
       var dateValue = new Date(value);
       if (dateValue - current !== 0)
           $(element).datepicker("setDate", dateValue);
    }               
}
Martin
  • 31
  • 3
  • 2
    This fixes an issue when the returned date value is in string format ie. "2013-01-20T05:00:00" instead of date object. I ran into this when loading data from Web API. – James Reategui Jan 20 '13 at 17:15
0

Based on Ryan's solution, myDate returns the standard date string, which is not the ideal one in my case. I used date.js to parse the value so it will always return the date format you want. Take a look at this example fiddle Example.

update: function(element, valueAccessor) {
    var value = ko.utils.unwrapObservable(valueAccessor()),
        current = $(element).datepicker("getDate");
    var d = Date.parse(value);
    if (value - current !== 0) {
        $(element).datepicker("setDate", d.toString("MM/dd/yyyy"));   
    }
}
Princa
  • 447
  • 2
  • 9
  • 19
0

I needed to repeatedly update my data from the server came across this but didn't quite finish the job for my needs sharing below(my date format /Date(1224043200000)/):

//Object Model
function Document(data) {
        if (String(data.RedemptionExpiration).indexOf('/Date(') == 0) {
            var newDate = new Date(parseInt(data.BDate.replace(/\/Date\((.*?)\)\//gi, "$1")));
            data.RedemptionExpiration = (newDate.getMonth()+1) +
                "/" + newDate.getDate() +
                "/" + newDate.getFullYear();
        }
        this.RedemptionExpiration = ko.observable(data.RedemptionExpiration);
}
//View Model
function DocumentViewModel(){
    ///additional code removed
    self.afterRenderLogic = function (elements) {
        $("#documentsContainer .datepicker").each(function () {
            $(this).datepicker();                   
        });
    };
}

After the model is formatted correctly for the output I added a template with the documentation knockoutjs:

<div id="documentsContainer">
    <div data-bind="template: { name: 'document-template', foreach: documents, afterRender: afterRenderLogic }, visible: documents().length > 0"></div>
</div>

//Inline template
<script type="text/html" id="document-template">
  <input data-bind="value: RedemptionExpiration" class="datepicker" />
</script>
Ken
  • 36
  • 3
0

Few people asked for dynamic datepicker options. In my case I needed a dynamic date range - so the first input defines the min value of the second and the second sets the max value for the first. I solved it by extending the RP Niemeyer's handler. So to his original:

ko.bindingHandlers.datepicker = {
    init: function(element, valueAccessor, allBindingsAccessor) {
        //initialize datepicker with some optional options
        var options = allBindingsAccessor().datepickerOptions || {},
            $el = $(element);

        $el.datepicker(options);

        //handle the field changing by registering datepicker's changeDate event
        ko.utils.registerEventHandler(element, "change", function() {
            var observable = valueAccessor();
            observable($el.datepicker("getDate"));
        });

        //handle disposal (if KO removes by the template binding)
        ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
            $el.datepicker("destroy");
        });

    },
    update: function(element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor()),
            $el = $(element);

        //handle date data coming via json from Microsoft
        if (String(value).indexOf('/Date(') == 0) {
            value = new Date(parseInt(value.replace(/\/Date\((.*?)\)\//gi, "$1")));
        }

        var current = $el.datepicker("getDate");

        if (value - current !== 0) {
            $el.datepicker("setDate", value);
        }
    }
};

I've added two more handlers corresponding to the options I wanted to modify:

ko.bindingHandlers.minDate = {
    update: function(element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor()),
            current = $(element).datepicker("option", "minDate", value);
    }
};

ko.bindingHandlers.maxDate = {
    update: function(element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor()),
            current = $(element).datepicker("option", "maxDate", value);
    }
};

And used them like that in my template:

<input data-bind="datepicker: selectedLowerValue, datepickerOptions: { minDate: minValue()}, maxDate: selectedUpperValue" />       
<input data-bind="datepicker: selectedUpperValue, datepickerOptions: { maxDate: maxValue()}, minDate: selectedLowerValue" />
Adam Bilinski
  • 1,188
  • 1
  • 18
  • 28
0

Using custom bindings provided in previous answers is not always possible. Calling $(element).datepicker(...) takes some considerable time, and if you have, for example, a few dozens, or even hundreds of elements to call this method with, you have to do it "lazy", i.e. on demand.

For example, the view model may be initialized, the inputs being inserted into the DOM, but the corresponding datepickers will only be initialized when a user clicks them.

So, here is my solution:

Add a custom binding that allows to attach arbitrary data to a node:

KO.bindingHandlers.boundData = {
  init: function(element, __, allBindings) {
    element.boundData = allBindings.get('boundData');
  }
};

Use the binding to attcah the observable used for the input's value:

<input type='text' class='my-date-input'
       data-bind='textInput: myObservable, boundData: myObservable' />

And finally, when initializing the datepicker, use it's onSelect option:

$('.my-date-input').datepicker({
  onSelect: function(dateText) {
    this.myObservable(dateText);
  }
  //Other options
});

This way, every time a user changes the date with the datepicker, the corresponding Knockout observable is also updated.

ololoepepe
  • 979
  • 1
  • 10
  • 18