jQuery UI datepicker change event not caught by KnockoutJS
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?
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/
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.
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
}
});
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?
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();
});
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>
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/
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.
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);
}
}
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"));
}
}
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>
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" />
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 input
s 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.
精彩评论