I have an application built on Durandal JS 2.0 and using inspired parts from the CodeCamper SPA by John Papa. In one of the forms I want to have a date and time picker so I found http://www.malot.fr/bootstrap-datetimepicker which looks nice and meets my needs. It's connected to the data layer using Knockout JS with a custom bindinghandler and also initialized as such.
Please note that the control visually and data works just fine and it also looks nice. It updates the datamodel and the underlying database just fine. All other standard controls work fine except this datetimepicker.
My problem occurs when the durandal form is composed, the datetimepicker is inserted into the DOM double. And every time I open the form the picker is again inserted, double. After a while the whole application starts slowing because I have a humongous amount of datetimepickers inserted in the DOM.
I have tried to trace where this happens and it seems to occur every time I initialize or reinitialize the observable. This occurs when the form is activated and when data is retreived. This triggers the bindinghandler to run and another instance of the picker is inserted.
I have experimented with different ways of keeping the picker from beeing created but nothing seems to work. I have tried to HTML-initialize the picker but then the bindinghandler has problems finding the control.
So, I'm stuck with the thought that the problem is either in the way I initialize the underlying knockout observable or that the picker cannot be initalized using knockout bindinghandler.
Any ideas on this would be most welcome.
Here is some selected code from my app:
<div class="row-fluid ui-row">
<div class="span2 ui-form-label">
Start:
</div>
<div class="span3">
<form class="form-inline">
<div id="actystartdate" class="controls input-append date">
<input class="ui-input ui-edit input-small" type="text" data-bind='datetimepicker: startTime, datetimepickerOptions: { language: "sv", pickerPosition: "bottom-left", format: "yyyy-mm-dd", weekStart: 1, todayBtn: 1, autoclose: 1, todayHighlight: 1, startView: 2, minView: 2 }' disabled readonly><span class="add-on ui-input ui-edit ui-nonedit"><i class="icon-calendar"></i></span>
</div>
<div id="actystarttime" class="controls input-append date">
<input class="ui-input ui-edit input-mini" type="text" data-bind='datetimepicker: startTime, datetimepickerOptions: { language: "sv", pickerPosition: "bottom-left", format: "hh:ii", autoclose: 1, startView: 1, minView: 0, maxView: 1, minuteStep: 15 }' disabled readonly><span class="add-on ui-input ui-edit ui-nonedit"><i class="icon-time"></i></span>
</div>
</form>
</div>
</div>
The knockout bindinghandler:
ko.bindingHandlers.datetimepicker = {
init: function (element, valueAccessor, allBindingsAccessor) {
var options = allBindingsAccessor().datetimepickerOptions || {};
$('#' + element.parentNode.id).datetimepicker(options);
//when a user changes the date, update the view model
ko.utils.registerEventHandler(element, "change", function () {
var value = valueAccessor();
if (ko.isObservable(value)) {
// Separate and merge date and time portions
if (Date.parse(element.value)) {
// We have an incoming date, merge with stored time and update
var incDate = new Date(element.value);
var dateTimeString = new Date(
incDate.getFullYear() + '-' + (incDate.getMonth() + 1 ) + '-' + incDate.getDate() + ' ' +
(value().getHours() < 10 ? "0" + value().getHours() : value().getHours()).toString() + ':' +
(value().getMinutes() < 10 ? "0" + value().getMinutes() : value().getMinutes()).toString() + ':00'
);
value(dateTimeString);
} else {
// We have an incoming time, merge with stored date and update
var incTime = new Date('1970-01-01 '+element.value);
var timeDateString = new Date(
value().getFullYear() + '-' + (value().getMonth() + 1 )+ '-' + value().getDate() + ' ' +
(incTime.getHours() < 10 ? "0" + incTime.getHours() : incTime.getHours()).toString() + ':' +
(incTime.getMinutes() < 10 ? "0" + incTime.getMinutes() : incTime.getMinutes()).toString() + ':00'
);
value(timeDateString);
}
}
});
},
update: function (element, valueAccessor) {
var widget = $('#' + element.parentNode.id).data("datetimepicker");
if (widget) {
widget.update(ko.utils.unwrapObservable(valueAccessor()));
widget.setValue();
}
}
};
Part taken from the datalayer that maps data retreived from Amplify JS and mapped to the datamodel:
mapToContext = function (dtoList, items, results, mapper, filter, sortFunction) {
var id, existingItem;
id = mapper.getDtoId(dtoList);
existingItem = items[id];
items[id] = mapper.fromDto(dtoList, existingItem);
results(items[id]); <---------- Here the bindingHandler is again triggered
return items[id];
}