Question

I’m using knockout to create a basic AJAX shopping cart for a project and the cost of the individual products that are contained in an observable collection inside the view model need to update when a property on the view model is updated. I’ve been trying various solutions for many hours and I hope somebody can point me in the right direction. I’ve included a jsfiddle.

http://jsfiddle.net/g8BLj/3/

var product = function (title, operationName, description, parent) {
  this.title = title;
  this.operationName = operationName;
  this.description = description;
  this.cost = 9;
  this.count = ko.observable(parent.recordCount);
  this.subtotal = ko.computed(function () {
    return this.count * this.cost;
  }).bind(this);
};

order = function () {
 var listName = ko.observable("not defined"),
     listId = ko.observable("not defined"),
     recordCount = ko.observable("not defined"),
     products = [
     new product('Product1', 'EMAIL_VER_DELIVERABLE', 'Description.', this),
     new product('Product2', 'EMAIL_BASIC_NO_SUPRESSION_W_VERIFICATION', 'Description.', this),
     new product('Product3', 'PHONE_PREM', 'Description.', this)],
     total = function () {
        var total = 0;
        $.each(this.products(), function () {
            total += this.subtotal();
        });
        return total;
     };

 // anything in the return block is considered public, anything above is private
 return {
    listName: listName,
    listId: listId,
    recordCount: recordCount,
    products: products,
    total: total
 };
}();

ko.applyBindings(order);

// when this value changes the product Cost needs to be updated
order.listName('test list')
order.listId(1)
order.recordCount(100)

Thanks, Chris

Was it helpful?

Solution

There are a couple of issues with the "this" scope which are causing problems.

this.subtotal = ko.computed(function () {
  return this.count * this.cost;
}).bind(this);

The correct way to get the ambient this scope correct is to pass "this" into the ko.computed as a second argument. Also, count is an observable to you need to evaluate it.

this.subtotal = ko.computed(function () {
  return this.count() * this.cost;
}, this);

The products variable is passing "this" into the constructor of product. The ambient this at this point is window, as are you are using anonymous function/revealing module pattern.

So when

 this.count = ko.observable(parent.recordCount);

evaluates, parent is window, so recordCount == undefined.

If you want to continue to use the revealing module pattern you need to tweek the order function to declare your return object ("obj") and then create the products.

You should also declare the total property as a ko.computed. I have used map/reduce instead of $.each but that is personal preference.

When this is done, it reveals a further issues with the count property on the product class. The parent.recordCount is an observable, so you are creating an observable where its value is an observable, not the value of the observable. Just assign the observable to a count property.


var product = function (title, operationName, description, parent) {
    this.title = title;
    this.operationName = operationName;
    this.description = description;
    this.cost = 9;
    this.count = parent.recordCount;
    this.subtotal = ko.computed(function () {
        return this.count() * this.cost;
    }, this);
};

order = function () {
    var
    listName = ko.observable("not defined"),
        listId = ko.observable("not defined"),
        recordCount = ko.observable("not defined"),
        products = [];

    var obj = {
        listName: listName,
        listId: listId,
        recordCount: recordCount,
        products: products
    }

    // now we have an object to push into the product class
    products.push(
        new product('Product1', 'EMAIL_VER_DELIVERABLE', 'Description.', obj),
        new product('Product2', 'EMAIL_BASIC_NO_SUPRESSION_W_VERIFICATION', 'Description.', obj),
        new product('Product3', 'PHONE_PREM', 'Description.', obj)
        );

    obj.total = ko.computed( function() {
        return this.products
            .map(function(item) { return item.subtotal(); })
            .reduce( function(runningTotal, subtotal) { return runningTotal + subtotal;
        }, 0);
    }, obj);

    // anything in the return block is considered public, anything above is private
    return obj;
}();

ko.applyBindings(order);

// when this value changes the product Cost needs to be updated
order.listId(1);
order.listName('test list');
order.recordCount(100);

OTHER TIPS

I think, the most important thing to solve this problem, is to use subscription to order.recordCount changes instead of sending order as parameter to product.
You can write something like this:

recordCount.subscribe(function (newValue) {
    ko.utils.arrayForEach(products, function (product) {
        product.count(newValue);
    })
});

Also, you should change product.subtotal calculation:

this.subtotal = ko.computed(function () {
    return this.count() * this.cost;
}, this);
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top