40

I'm using a jQuery UI datepicker. The HTML input field behind it is currently hooked up to KnockoutJS as a dependentObservable, but when its value is set in the viewmodel, the datepicker loses its format.

How should I do this and not lose the format? I would like the viewModel not to know that it is a jQuery datepicker.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Rasmus Christensen
  • 8,321
  • 12
  • 51
  • 78

7 Answers7

82

You could write a custom binding that sets the date in the field using the datepicker APIs and also sets the value of your observable by reading the date properly.

The custom binding might look like:

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

        //initialize datepicker with some optional options
        $el.datepicker(options);

        //handle the field changing
        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),
            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() }" />

The datepickeroptions would be optional and could include anything that you want to pass into the datepicker() call.

Also, this assumes that you are using an observable for the date. The binding has to do a little more work if you want to do a one-way binding with a non-observable, but that is unlikely.

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

RP Niemeyer
  • 114,592
  • 18
  • 291
  • 211
  • What about this custom binding needs to change to better cooperate between datepicker and "with: submodelObject" style binding? In my use, it caused the datepicker's div to wind up rendered visible until it could be attached to a visible ; using "template: {afterRender: jqueryDecoratingMethod, data: submodelObject}" worked fine, though. – Tetsujin no Oni Jan 13 '12 at 14:50
  • @TetsujinnoOni do you have a sample of where you are seeing a problem or could your fork my fiddle? Would be happy to take a look. – RP Niemeyer Jan 13 '12 at 15:03
  • Ryan - thanks for the offer; I'm not able to reproduce in the fiddle. I suspect (due to other ugliness on the page) that I've either forgotten to theme jqueryui or have something similarly ugly going on in my CSS.... if I can narrow it to a specific combination of factors, I'll post a link or a fiddle. – Tetsujin no Oni Jan 18 '12 at 15:34
  • I realize this is very old, but when I try to use this for the date time picker, in IE7, the pop-up won't go away fully. When I select a value, it hides the pop-up briefly, and then it activates again without any way to close it. Any thoughts on this bug? It seems fine in Chrome. – arb Feb 02 '12 at 20:45
  • 2
    @Zero21xxx I updated the answer and fiddle with a tweak that prevents this issue on IE7. This came up in another question at some point. – RP Niemeyer Feb 02 '12 at 23:15
  • So, I fall into the situation where my date is NOT an observable and "have a little more work to do a one-way binding". What I do have is an observableArray of dates. The UI is effectively rendering the INPUT fields and the datepickers to my satisfaction.. but.. when the date is selected, while it shows in the INPUT, I cannot get the value round-tripped back into the model with the current code demonstrated above. – bkwdesign Mar 04 '14 at 20:47
  • So an input textbox is not supposed to ever be null. However, if you set a date, then delete the date, the value becomes null. Turns out developers code other utilities with this expectation that textbox is never null. So at the end of the update section, I added this: if (!value) value = ""; – Rhyous Apr 10 '14 at 17:31
6

I had to make a slight edit to RP Niemeyer's code to work in my code using the dateFormat option, replacing

$(element).datepicker("getDate")

With

$(element).val()

So the formatted version of the date was passed around correctly under the hood.

OddEssay
  • 1,334
  • 11
  • 19
6

I've been using RP Niemeyer's code marked as the answer above, but since I've been using it I've made a few small changes to it. I thought I would post here. Maybe it will help others. It's pretty much the same, the only difference is that if the element has a value when the page loads then it will retain its value. Also, I made $elem a variable so that there will be less processing of $(element) that jQuery will have to do.

ko.bindingHandlers['jqDatePicker'] = {
    'init': function(element, valueAccessor, allBindingsAccessor) {
        /* Initialize datepicker with some optional options */
        var options = allBindingsAccessor().jqDatePickerOptions || {},
            prop = valueAccessor(),
            $elem = $(element);

        prop($elem.val());

        $elem.datepicker(options);

        /* Handle the field changing */
        ko.utils.registerEventHandler(element, "change", function () {
            prop($elem.datepicker("getDate"));
        });

        /* Handle disposal (if KO removes by the template binding) */
        ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
            $elem.datepicker("destroy");
        });
    },
    'update': function(element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor()),
            $elem = $(element),
            current = $elem.datepicker("getDate");

        if (value - current !== 0) {
            $elem.datepicker("setDate", value);
        }
    }
};
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Evan Larsen
  • 9,935
  • 4
  • 46
  • 60
  • I didn't dig into it, but the initial value of the date-picker was not set to the value of the bound observable. – dhochee Dec 31 '15 at 20:34
2

Here is what worked for my particular set of circumstances. I'm running a new enough version of MVC, that the default datetime serializer renders at ISO 8601 (see On the nightmare that is JSON Dates. Plus, JSON.NET and ASP.NET Web API). My bindings do not write directly to the date picker, but instead to an input tag's "value" attribute.

Also, of note, I'm using date.js

ko.bindingHandlers.dateValue = {
    update: function(element, valueAccessor, allBindingsAccessor, viewModel) {
        var value = valueAccessor(),
            allBindings = allBindingsAccessor();
        var valueUnwrapped = ko.utils.unwrapObservable(value);
        var pattern = allBindings.datePattern || 'MM/dd/yyyy';
        var date = Date.parse(valueUnwrapped)
        $(element).val(date.toString(pattern));
    },

    init: function(element, valueAccessor, allBindingsAccessor) {
        //handle the field changing
        ko.utils.registerEventHandler(element, "change", function () {
            var observable = valueAccessor();
            var date = Date.parse($(element).val());
            observable(date.toString("yyyy-MM-ddThh:mm:ss"));
        });
    }
}

Binding looks like this:

<input class="date" type="text" data-bind="dateValue: SomeViewModelDate" />

And the JavaScript code to turn on the datepicker:

$(document).ready(function () {
    $('.date').datepicker({ dateFormat: "mm/dd/yy" });
});
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
viggity
  • 15,039
  • 7
  • 88
  • 96
  • I have a question where should I put the code before apply bindings? Now I'm getting the error: Uncaught TypeError: Unable to process binding "foreach: function (){return policies }" Message: Unable to process binding "dateValue: function (){return startDate }" Message: Cannot read property 'toString' of null – Rafal_Koscinski Oct 16 '14 at 07:31
1

The datepicker samples above change the format of the date in the viewmodel from WCF format to the JavaScript date format when the user selects a new date from the datepicker control.

In my case, I was passing the date back to a WCF service, and it would not accept a deserialized JavaScript date, it needed the date in the WCF format. I modified the above script so that it stores the date in the viewmodel in WCF format so that it can be sent back to the server in that format.

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).datepicker("getDate"));
            // store the date in 'WCF String format"
            var tempdate=$(element).datepicker("getDate");
            var tempdatestr="/Date("+tempdate.getTime()+")/";
            observable(tempdatestr);
        });
        //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);
        }
    }
};
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
RussGove
  • 322
  • 5
  • 18
0

Formatting the date (to mm/dd/yyyy) inside the dependentObservable is exactly what I was wondering how to do. I'll post a little of my code if you can help, Peter Mortensen and/or nEEBz.

    <div data-bind="with: technology">  
        <div class="titleblock">
            <label><b>End of Life Date</b></label> 
            <Input  type="text" class="ui-datepicker" id="datepicker" data-bind="value: END_OF_LIFE_DATE"/>
        </div>       
    </div>

in ViewModel - technologydetail.js:

var technology = ko.observable();

in Activate:

return dataContext.getTechnologyById(currentTechnologyId, technology);

This displays a date that looks like this in the textbox: Wed Jan 29 19:00:00 EST 2014 but I want it to just show: 01/29/2014. I am using this datepicker option - dateFormat: "mm/dd/yy" but it has no effect on the initial value displayed.

I was able to format it using moment and it works nicely for displaying the current database value, but it seems to be breaking the binding back to the observable because it will no longer update on a SAVE.

<Input type="text" class="ui-datepicker" id="datepicker" data-bind="value: moment(END_OF_LIFE_DATE()).format('MM/DD/YYYY')" />
Debbie A
  • 404
  • 3
  • 13
0

One solution is to format the date yourself inside the dependentObservable function. So you must be returning something like return viewModel.someOtherObservable() in the function. Format the return value.

If you include your code, I can explain more.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
neebz
  • 11,465
  • 7
  • 47
  • 64