Search code examples
javascripthtmlknockout.jsindexofko.observablearray

Knockout JS: indexOf always returns -1?


Background

I'm trying to build a gradebook app, mostly as a learning exercise. Currently, I have two models, a student, and an assignment. I decided to store all score-related information inside the student rather than with each assignment. Perhaps there's a better approach.

Regardless, I already have the average score for each student, i.e., her grade in the class. I'm now at the point where I want to calculate the average score on each assignment. This is where I run into trouble, as it's slightly trickier. I'm currently using the following method:

JS Bin (entire project): http://jsbin.com/fehoq/84/edit

JS

  var _this = this;
  ...

  // get index i of current assignment; 
  // then, for each student, grab her grade for assignment i;
  // add each grade at i, then divide by # of students;
  // return this value (the mean);

  this.workMean = ko.computed(function (work) {
        var i = parseFloat(_this.assignments.indexOf(work)); 
        var m = 0;
        var count = 0;
        ko.utils.arrayForEach(_this.students(), function (student) {
            if (!isNaN(parseFloat(student.scores()[i]))) {
                m += parseFloat(student.scores()[i]);
            } else {
                count += 1;
            }
        });
        m = m / (_this.students().length - count); 
        return m.toFixed(2);   
    });

And I'm binding this to the HTML in the following way:

HTML

<tbody>
  <!-- ko foreach: students -->
    <tr>
        <td><input data-bind="value: fullName + ' ' + ($index()+1)"/></td>  
        <!-- ko foreach: scores -->  
        <td><input data-bind="value: $rawData"/></td>
        <!-- /ko --> 
        <td data-bind="text: mean" />
        <td><input type="button" value="remove" data-bind="click: $root.removeStudent.bind($root)". /></td>
    </tr> 
  <!-- /ko -->
    <tr>
      <td>Class Work Average</td> 
      <!-- ko foreach: assignments -->
      <td data-bind="text: $root.workMean"></td>
      <!-- /ko -->
    </tr>  
</tbody>    

The problem is that something I'm doing here - and I think it's the workMean() method - is completely breaking my app. While trying to debug, I noticed that if I simply comment out the entire method save i, then return i and bind it to the lower foreach: assignments, it consistently returns -1 (for each assignment).

The Knockout docs tell me that means there's no match when I call indexOf, but I'm lost as to why. Guidance appreciated.


Solution

  • Besides the issue that DCoder identified — observables not taking parameters -, you had another bug here:

    score = parseFloat(student.scores()[i]);
    

    should have been

    score = parseFloat(student.scores()[i]());
    

    The nth (or i-th) element of the observable array you access there is itself an observable, so before, you where passing a function to parseFloat, which always yields NaN.

    Here is a working version: http://jsbin.com/lejezuhe/3/edit

    By the way: after DCoders changes,

    <td data-bind="text: $root.workMean($data, $index())"></td>
    

    binds against a normal function, not an observable. So why does this still work?

    RP Niemeyer, one of the Knockout core members writes:

    In Knockout, bindings are implemented internally using dependentObservables, so you can actually use a plain function in place of a dependentObservable in your bindings. The binding will run your function inside of a dependentObservable, so any observables that have their value accessed will create a dependency (your binding will fire again when it changes).

    (computed observables used to be called "dependentObservables" in earlier versions of Knockout)

    For these type of issues, it really helps to be familiar with a debugger such as the one in the Chrome Developer Tools. Being able to step through your code line by line and seeing what arguments and variables actually contain is immensely helpful.

    The Chrome Knockout context debugger is worthwhile to have when debugging bindings, because you can click on any DOM element and see the binding context:

    Screenshot depicting the Knockout context debugger in action

    Lastly, using ko.dataFor() in the console allows you to poke around any of your existing Knockout models and viewmodels bound to the DOM:

    Screenshot depicting the usage of ko.dataFor() in the dev tools console

    In the Chrome console, $0 is always a reference to the DOM element you have currently selected in the Elements panel - here, a <td>.