1

I have this very simple knockoutjs script. My view model contains a property called 'modules' which is an array of strings. If I have a foreach list like this it prints a list of modules for each item:

<tbody data-bind="foreach: items">
    <tr>
        <td data-bind="text: modules"></td>
    </tr>
</tbody>

But if I want to print the number of modules instead, by adding a computed observable:

<tbody data-bind="foreach: items">
    <tr>
        <td data-bind="text: numModules"></td>
    </tr>
</tbody>

I get into problems. 'undefined' is not a function it says on the first line of my computed function. My js code looks like this:

function AppViewModel(data) {
    var self = this;
    ko.mapping.fromJS(data, {}, this);
    this.numModules = ko.computed(function() {
        return self.modules().length;
    });
};

$.getJSON("/api/items", function(data) {
    var viewModel = new AppViewModel(data);
    ko.applyBindings(viewModel);
});
Markus Johansson
  • 3,733
  • 8
  • 36
  • 55

4 Answers4

7

In cases like this, it is possible that the computed is attempting to calculate before the property actually exists. One parameter that is not set by default on ko.computed is the deferEvaluation parameter...once this parameters is set, the computed won't attempt to calculate on the initialization of your AppViewModel.

this.numModules = ko.computed({ 
    read: function() {
        return self.modules().length;
    },
    deferEvaluation: true
);

On a more picky note, if you define 'self = this' then in the very next line set up your computed with the context of 'this', why did you ever define 'self'?

beauXjames
  • 8,222
  • 3
  • 49
  • 66
1

The problem is that you're defining numModules on the root of your ViewModel while the calculation you're attempting to perform is on the modules property of each item.

Hence, self.modules is indeed undefined and cannot be invoked since self refers to the root object, while modules is a property of each item.

Try this instead:

function AppViewModel(data)
{
    var self = this;
    ko.mapping.fromJS(data, {}, this);

    // defining the computed function on each 'item'
    for (var i in self.items())
    {
        self.items()[i].numModules = ko.computed(function()
        {
            return this.modules().length;
        }, self.items()[i]);
    }
};

Or this way:

// defined on $root
self.numModules = function(item) {
    return item.modules().length;
}

// but passing 'item' upon invocation
<td data-bind="text: $root.numModules($data)"></td>
haim770
  • 48,394
  • 7
  • 105
  • 133
  • In your first example, I want to avoid defining a function on each item. I'd rather define it on the model somehow. In the last example, is the function really the same as a ko.computed? – Markus Johansson May 15 '14 at 07:27
  • It's very common to use functions over `ko.computed`, and it'll work just fine in your case as well. See http://stackoverflow.com/a/11528135 for more info. – haim770 May 15 '14 at 07:40
  • Of the answers so far, your last one is the only one which works. The $root and $data syntax in the html doesn't look as nice as simply "numModules" though. – Markus Johansson May 15 '14 at 07:44
  • If you're looking for nice and clean code, you best bet is to actually *define* your item ViewModel (`function item() { this.modules = ... }`) and not to rely on `ko.mapping` to create the objects on the fly from arbitrary data. – haim770 May 15 '14 at 08:02
0

I am not sure but what if you do it like this:

function AppViewModel(data) {
    var self = this;

    this.modules = ko.obeservableArray();

    ko.mapping.fromJS(data, {}, this.modules());

    this.numModules = ko.computed(function() {
        return self.modules().length;
  });
};
Mason
  • 1,007
  • 1
  • 13
  • 31
0

I found that the easiest way to solve the problem, without adding any special js code at all is to simply do like this in the html:

<td data-bind="text: modules().length"></td>
Markus Johansson
  • 3,733
  • 8
  • 36
  • 55