1

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];
    }
Jan R
  • 41
  • 5

1 Answers1

0

Make sure you initialize datetimepicker in "attached" function and make sure you expose "attached" function in your viewmodel.

Also, I managed to bind the value to it as follows (uses MomentJS to convert datetime format read from database - you don't need to use MomentJS if your observable is in right format):

function attached(view){
    $('#datetimepicker_id').datetimepicker({
        format: 'dd/MM/yyyy hh:mm',
        pickSeconds: false
    });

    // placing inital value, reading from observable and formatting it with momentjs
    $('#datetimepicker_id')[0].childNodes[1].value = moment(myObservable()).format('DD/MM/YYYY HH:mm'); 

    // and now to subscribe to change event and place new value to my observable
    $('#datetimepicker_id').on('changeDate', function(e){
        myObservable(moment(e.date).toDate());
    });
}
Dominictus
  • 721
  • 5
  • 12
  • 1
    Using a custom binding handler is more favorable than using jQuery in the attached function. – PW Kad Oct 16 '13 at 14:10
  • Well, this is a workaround that works since it is hardcoded around ko. But it actually does not answer the question. I would prefer to use the ko.bindinghandler since it is a cleaner way of binding data. – Jan R Oct 16 '13 at 19:44
  • I kinda wanted results fast so it's not the nicest solution out there. On the other hand, check if your view is unbound properly. Double DOM objects usually point to some mistake where you keep reapplying bindings. If you instantly get two DOM objects then something makes bindings trigger twice. – Dominictus Oct 17 '13 at 08:01