ko.Computed() is not updating with observableArray
Asked Answered
S

2

13

I have the following code:

// First we define our gift class, which has 2 properties:
// a Title and a Price.
// We use knockout js validation to ensure that the values input are suitable/ 
function Gift(item)
{
    var self = this;
    self.Title = ko.observable(item.Title);

    // attach some validation to the Title property courtesy of knockout js validation
    self.Title.extend({
        required: true,
        minLength: 3,
        pattern: {
            message: 'At least ',
            params: '^[a-zA-Z]+\s?[a-zA-Z]*'
        }
    });
    self.Price = ko.observable(item.Price);
    self.Price.extend({required:true,number:true,min:0.1,max:1000});
};


var viewModelForTemplated =
{
    gifts: ko.observableArray(),        // gifts will be an array of Gift classes

    addGift: function ()
    {
        this.gifts.push(new Gift({ Title: "", Price: "" }));
    },
    removeGift: function (gift)
    {
        this.gifts.remove(gift);
    },

    totalCost: ko.computed(function () {
        if (typeof gifts == 'undefined')
            return 0;

        var total = 0;

        for (var i = 0; i < gifts().length; i++)
        {
            total += parseFloat(gifts()[i].Price());
        };
        return total;
    })
}


$(document).ready(function ()
{
    // load in the data from our MVC controller 
    $.getJSON("gift/getdata", function (allGifts)
    {
        var mappedgifts = $.map(allGifts, function (gift)
        {
            return new Gift(gift);
        });

        viewModelForTemplated.gifts(mappedgifts);
    });

    ko.applyBindings(viewModelForTemplated, $('#templated')[0]);
}

and then (above the script)

<div id="templated">

<table >
    <tbody data-bind="template: { name: 'giftRowTemplate', foreach: gifts }"></tbody>
</table>

<script type="text/html" id="giftRowTemplate">
    <tr>
        <td>Gift name: <input data-bind="value: Title"/></td>
        <td>Price: \$ <input data-bind="value: Price"/></td>           
        <td><a href="#" data-bind="click: function() { viewModelForTemplated.removeGift($data) }">Delete</a></td>
    </tr>
</script>

<p>Total Cost <span data-bind="text: totalCost"></span> </p>    

<button data-bind="click: addGift">Add Gift</button> 

<button data-bind="click: save">Save</button>

</div>

The totalCost method only runs once, when the gifts array is empty, and I can push or remove items onto the observableArray() no problem but nothing fires .

How do I get the span referring to totalCost to update? I bet it's something simple :)

Thanks for your help.

Smalto answered 28/10, 2013 at 16:28 Comment(2)
I notice during debugging that the code in ko.computed(function () executes before the $(document).ready() code does. Adding to the gifts array in the viewmodel does not cause the computed observable to fire the function though.Smalto
Hi Scott. Any movement on this issue? I'm actually struggling with the same issue at the moment.Koziel
R
12

You need to unwrap your observable:

totalCost: ko.computed(function () {
    //also, you forgot typeof below
    if (typeof gifts == 'undefined')
       return 0;

    var total = 0;  //here \/
    for (var i=0; i < gifts().length; i++)
    {                //and here  \/
        total += parseFloat(gifts()[i].Price());
    };
    return total;
})

The reason it's not updating, is because

gifts.length

is always evaluating to 0, and never entering the loop. And even if it did,

gifts[i].Price()

would not work for the same reason; you need to unwrap the observable.


Note that the reason why length evaluates to zero when you don't unwrap it is because you're getting the length of the actual observable array function. All observables in Knockout are implemented as regular functions; when you don't unwrap it, you're hitting the actual function itself, not the underlying array.


Edit,

Also, you need to reference gifts with this.gifts, since it's an object property. That's why this wasn't working; gifts is always undefined.

That said, you also need to do some more work to get ko computeds to work from an object literal. Read here for more info:

http://dpruna.blogspot.com/2013/09/how-to-use-kocomputed-in-javascript.html

Here's how I would make your view model:

function Vm{
    this.gifts = ko.observableArray();        // gifts will be an array of Gift classes

    this.addGift = function () {
        this.gifts.push(new Gift({ Title: "", Price: "" }));
    };

    this.removeGift = function (gift)
    {
        this.gifts.remove(gift);
    };

    this.totalCost = ko.computed(function () {
        var total = 0;

        for (var i = 0; i < this.gifts().length; i++)
        {
            total += parseFloat(this.gifts()[i].Price());
        };
        return total;
    }, this);
}

var viewModelForTemplated = new Vm();
Rhabdomancy answered 28/10, 2013 at 16:30 Comment(5)
Hi, I tried your suggestion but it didn't work. I have now included my viewmodel and the HTML markup in case that's at fault. Thanks for your help.Smalto
Hi Adam, no errors reported by Firefox or IE10. One thing to note is that the code in ko.computed() fires before the document.ready() does, and from checking the ko docs the first time that happens is that Knockout evaluates dependencies and then adds them to a list. Could it be because the computed function returns when typeof(gifts) is undefined that ko thinks there are no dependencies?Smalto
@Smalto - ah, there it is. typeof gifts is always undefined. You can remove that check completely; more importantly, replace all references to gifts with this.giftsRhabdomancy
@Smalto - gah, also, an object literal won't work like this. Might want to use a constructor function instead, per my hopefully last edit.Rhabdomancy
Hi Adam, thanks for your feedback. Yes, I was kind of thinking I'd need the "class definition" approach instead of "singleton" I used, as per the knockout tutorials, but hoping there would be another way. I'll try what you suggested and get back to you ASAP. Again, thanks for your help.Smalto
F
0

You have to unwrap observableArray using () when applying indexer to it. Update totalCost as follow:

totalCost: ko.computed(function () {
    if (this.gifts == 'undefined')
       return 0;

    var total = 0;
    for (var i=0; i<this.gifts().length;i++)
    {
        total += parseFloat(this.gifts()[i].Price());
    };
    return total;
})
Filipino answered 28/10, 2013 at 16:30 Comment(1)
Hi, I took on board your suggestions re unwrapping the observable but it didn't work. I have updated my OP to show the full code. Thanks for your assistance.Smalto

© 2022 - 2024 — McMap. All rights reserved.