codeandcloud
codeandcloud

Reputation: 55210

Why does adding a computed to an observableArray fail?

I think something is very wrong here. I would like to add a ko.computed to a ko.observableArray like this.

Before that my model for clarity

//var job = @Html.Raw(Json.Encode(Model.job));
//var customer = @Html.Raw(Json.Encode(Model.customer));
//var estimateMaterials = @Html.Raw(Json.Encode(Model.estimateMaterials));
//var estimate = @Html.Raw(Json.Encode(Model.estimate));
var estimateTasks = @Html.Raw(Json.Encode(Model.Tasks));

var JobPostViewModel = function(){
    var self = this;
    //self.customer = ko.observable(customer);
    //self.job = ko.observable(job);
    //self.estimateMaterials = ko.observableArray(estimateMaterials);
    //self.estimate = ko.observable(estimate);
    self.estimateTasks = ko.observableArray(estimateTasks);
    self.estimateTasks().estLaborSubTotal = ko.computed(function () {
        return (isNaN(self.EstHr)? 0: +self.EstHr) * (isNaN(self.TaskPerHourCost)? 0: +self.TaskPerHourCost);
    });
};
var model = new JobPostViewModel();
ko.applyBindings(model,  document.getElementById("my_job_form"));

So this is what my model bind is. my_job_form is data-bind="with:jobs" and I am populating a table inside the form that is bound to estimateTasks. The markup is

<tbody data-bind="foreach: $root.estimateTasks">
    <tr>
        <td>
            <input type="text" data-bind="value: EstHr" />
            <input type="hidden" data-bind="value: TaskPerHourCost" />
        </td>
        <td>
            <input type="text" data-bind="value: estLaborSubTotal" disabled />
        </td>
    </tr>
</tbody>

On bind, I get the error

ReferenceError: estLaborSubTotal is not defined

What am I doing wrong here?

Upvotes: 0

Views: 475

Answers (3)

Damien
Damien

Reputation: 8987

You are adding an computed to an array. This array is just the result of the evaluation of estimateTasks observableArray();

If I understand what you are trying to do.

You better do it this way. This will add an computed named estLaborSubTotal to item.

var JobPostViewModel = function(){
    var self = this;
    ko.utils.arrayForEach(estimateTasks, function(et) {
        et.estLaborSubTotal  = ko.computed({
            read: function(){
                var estHr = this.EstHr();
                var taskPerHourCost =  this.TaskPerHourCost();
                if(estHr === null)
                    return 0;
                return estHr * taskPerHourCost; 
            },
            owner : et // specify the "this" during evaluation 
        });
    });

    self.estimateTasks = ko.observableArray(estimateTasks);
};

See fiddle

I hope it helps.

Upvotes: 3

PW Kad
PW Kad

Reputation: 14995

I think this is what you are trying to do -

function jobPostEstimateTaskModel (task) {
    var self = this;
    // Make some properties of this task observable
    self.name = ko.observable(task.name);
    // Make a computed for each task
    self.estLaborSubTotal = ko.computed(function () {
        return (isNaN(self.EstHr)? 0: +self.EstHr) * (isNaN(self.TaskPerHourCost)? 0: +self.TaskPerHourCost);
    });
}

var JobPostViewModel = function(){
    var self = this;
    self.estimateTasks = ko.observableArray();
    $.each(estimateTasks, function (index, item) {
        self.estimateTasks.push(new jobPostEstimateTaskModel(item));
    }
};

This will iterate through the estimateTasks plain old JavaScript array that you have created and add a new jobPostEstimateTaskModel object for each task in the array. It will create observable properties on the task such as name in this example, and then create a computed for each item in the array.

By calling it the way you were above you are simply creating a computed onto the observableArray, and not doing what you intended.

One of the confusion points is that your ViewModel has an object that is an observableArray called estimateTasks but you also have a variable at the context 'above' it named the exact same thing. Consider renaming your non-observable to something like estimateTasksJson to clarify.

Edit

Another way to approach this is to use prototyping - you would still need to instantiate an object like jobPostEstimateTaskModel or whatever you want to call it but you could extend an observable onto each instance without having to iterate through that code each time and add it in the model.

Upvotes: 2

Vladimir
Vladimir

Reputation: 7475

Your problem is that inside foreach current binding content is current array element.
And this element does not have estLaborSubTotal property.
You can use <input type="text" data-bind="value: $root.estimateTasks().estLaborSubTotal" disabled /> instead.

Or you can change your viewmodel:

var JobPostViewModel = function(){
    var self = this;
    //self.customer = ko.observable(customer);
    //self.job = ko.observable(job);
    //self.estimateMaterials = ko.observableArray(estimateMaterials);
    //self.estimate = ko.observable(estimate);
    self.estimateTasks = ko.observableArray(estimateTasks);
    self.estLaborSubTotal = ko.computed(function () {
        return (isNaN(self.EstHr)? 0: +self.EstHr) * (isNaN(self.TaskPerHourCost)? 0: +self.TaskPerHourCost);
    });
};

And use <input type="text" data-bind="value: $root.estLaborSubTotal" disabled />.

Upvotes: 1

Related Questions