Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

KnockoutJS Binding Issue - Cannot read property

I have what is likely a simple Knockout question but I'm a complete beginner with it. I was tossed this page of code that someone else has worked on but never finished.

When this page first loads, the data is retrieved and the main grid loads properly. The problem comes in when I attempt to auto-select the first record in the results so that a detail list gets filled out below the grid.

When that happens, I receive the following message.

Uncaught TypeError: Unable to process binding "text: function (){return selected.RequestLog.Timestamp }" , Message: Cannot read property 'Timestamp' of undefined

Here is the code snippets with which I'm working. The data coming back is from Entity Framework.

var siteLogModel = function () {
            var self = this;

            self.errorList = ko.observableArray([]);
            self.selected = ko.observable();

            self.updateErrorList = function (page) {
                jQuery.ajax({
                    type: "POST",
                    url: "/Admin/ErrorPage",
                    data: { pageNum: page },
                    success: function (result) {
                        self.errorList(result);
                        self.selected(result[0]);

                        // Since we have success, add the click handler so we can get the details about a row by id.
                        //addRowHandlers();
                    },
                    error: function (result) {
                        jQuery("#status").text = result;
                    }
                });
            };
        };

This is the actual binding that tries to happen after the data is loaded. RequestLog does not seem to exist at binding time, even though it does seem to be ok if I set a breakpoint in the above function on the line self.selected(result[0]).

I think this is a scope problem but I can't for the life of me think of how best to fix it. Any help would be appreciated.

    <div class="param">
       <span>Time</span>
       <label data-bind="text: selected.RequestLog.Timestamp"></label>
    </div>

UPDATE: Here is the document ready portion.

jQuery(document).ready(function () {

            var vm = new siteLogModel();
            vm.updateErrorList(0);
            ko.applyBindings(vm);
        });
like image 407
Rob Horton Avatar asked Dec 11 '22 08:12

Rob Horton


1 Answers

Your selected observable does not have a .RequestLog property at the time ko is evaluating the binding expression. That error is coming from javascript, not ko (though ko wraps the exception in the error message you see). When running, selected.RequestLog === undefined is true, and you can't invoke anything on undefined. It's like a null reference exception.

It makes sense if you are calling applyBindings before the ajax call finishes.

One way to fix this by doing a computed instead:

<div class="param">
   <span>Time</span>
   <label data-bind="text: selectedRequestLogTimestamp"></label>
</div>

self.selectedRequestLogTimestamp = ko.computed(function() {
    var selected = self.selected();
    return selected && selected.RequestLog
        ? selected.RequestLog.TimeStamp
        : 'Still waiting on data...';
});

With the above, nothing is ever being invoked on an undefined variable. Your label will display "Still waiting on data" until the ajax call finishes, then it will populate with the timestamp as soon as you invoke self.selected(result[0]).

Another way to solve it is by keeping your binding the same, but by giving the selected observable an initial value. You can leave all of your html as-is, and just to this:

self.selected = ko.observable({
    RequestLog: {
        TimeStamp: 'Still waiting on data'
    }
});

... and you will end up with the same result.

Why?

Any time you initialize an observable by doing something like self.prop = ko.observable(), the actual value of the observable is undefined. Try it out:

self.prop1 = ko.observable();
var prop1Value = self.prop1();
if (typeof prop1Value === 'undefined') alert('It is undefined');
else alert('this alert will not pop up unless you initialize the observable');

So to summarize what is happening:

  1. You initialize your selected observable with a value equal to undefined in your viewmodel.
  2. You call ko.applyBindings against the viewmodel.
  3. ko parses the data-bind attributes, and tries to bind.
  4. ko gets to the text: selected.RequestLog.Timestamp binding.
  5. ko invokes selected(), which returns undefined.
  6. ko tries to invoke .RequestLog on undefined.
  7. ko throws an error, because undefined does not have a .RequestLog property.

All of this happens before your ajax call returns.

Reply to comment #1

Yes, you can call applyBindings after your ajax success event. However, that's typically not always what you should do. If you want to, here's one example of how it could be done:

self.updateErrorList = function (page) {
    self.updateErrorPromise = jQuery.ajax({
        type: "POST",
        url: "/Admin/ErrorPage",
        data: { pageNum: page },
        success: function (result) {
            self.errorList(result);
            self.selected(result[0]);
        },
        error: function (result) {
            jQuery("#status").text = result;
        }
    });
};

jQuery(document).ready(function () {
    var vm = new siteLogModel();
    vm.updateErrorList(0);
    vm.updateErrorPromise.done(function() {
        ko.applyBindings(vm);
    });
});

Yet another way would be to go ahead and eager-bind (applyBindings before the ajax call finishes), but wrap your markup in an if binding like so:

<div class="param" data-bind="if: selected">
   <span>Time</span>
   <label data-bind="text: selected.RequestLog.Timestamp"></label>
</div>
like image 75
danludwig Avatar answered Dec 27 '22 02:12

danludwig