2

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.

Community
  • 1
  • 1
Jefftopia
  • 2,105
  • 1
  • 26
  • 45
  • 1
    Hello again :) As I'm already familiar with your code, I'll take a look as soon as I am home at my desk. – janfoeh Apr 25 '14 at 17:40
  • 1
    You misunderstand the use of `ko.computed`. It does *not* take arguments, it is used to compute a value when observables its value depends on are changed. Edit: See [this updated bin](http://jsbin.com/wirirotu/1/edit) - I converted `workMean` to an ordinary function that receives the looped item and its index as arguments, and made some other changes to certain places that seemed iffy. – DCoder Apr 25 '14 at 17:44
  • I guess it's unclear to me what exactly is meant by "its value depends on". Either way, your edits do seem to improve things and it's less verbose now. – Jefftopia Apr 25 '14 at 17:59
  • 2
    When a `ko.computed` is created, knockout calls it to get its initial value and does some magic to track which observables were used during that call. Later, when any of those observables changes its value, knockout re-runs the computed function to get its new value. That's what I meant by "its value depends on". Like everything else in knockout, a value of a `computed` should depend only on observables, so it can be updated automatically. – DCoder Apr 25 '14 at 18:04
  • 1
    "a value of a computed should depend **only** on observables" (emphasis added) - that part helped especially. Thanks. Do you happen to know why the `workMean` method returns N/A (outside the obvious `else {return 'N/A'}`)? – Jefftopia Apr 25 '14 at 18:22
  • `'N/A'` is returned when no student has a grade for that assignment - in that case using plain math would produce a `0/0`, also known as `NaN`. I added the check for that case and returned `N/A` specifically to avoid this. – DCoder Apr 25 '14 at 18:32
  • My thought, though it seems I am wrong, was that without `workMean` being a `ko.computed`, it won't update when new values are pushed to the `scores` array, and that's why `workMean` stays at `N/A`. If I am wrong, then why won't `workMean` update? – Jefftopia Apr 25 '14 at 18:35
  • Notice that `workMean` stays at 'N/A' even as the scores change... – Jefftopia Apr 25 '14 at 18:39

1 Answers1

3

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>.

Community
  • 1
  • 1
janfoeh
  • 10,243
  • 2
  • 31
  • 56
  • Answered my question, solved my problem, provided tools for future debugging. It's too bad I can't +1 this twice, Thanks! – Jefftopia Apr 25 '14 at 19:58