I have an array of items coming back from the service. I'm trying to define a computed observable for every Item instance, so my instinct tells me to put it on the prototype.
One case for the computed observable: the system calculates points, but the user can choose to override the calculated value. I need to keep the calculated value available in case the user removes the override. I also need to coalesce user-assigned and calculated points, and add up the totals.
I'm using mapping to do the following:
var itemsViewModel;
var items = [
{ 'PointsCalculated' : 5.1 },
{ 'PointsCalculated' : 2.37, 'PointsFromUser' : 3 }
];
var mapping = {
'Items' : {
create : function(options) {
return new Item(options.data);
}
}
};
var Item = function(data) {
var item = this;
ko.mapping.fromJS(data, mapping, item);
};
Item.prototype.Points = function () {
var item = this;
return ko.computed(function () {
// PointsFromUser may be 0, so only ignore it if the value is undefined/null
return (item.PointsFromUser != null) ? item.PointsFromUser : item.PointsCalculated;
});
};
ko.mapping.fromJS(items, mapping, itemsViewModel);
The way it works now, I have to call the anonymous function to return the computed observable. That appears to create a new instance of the computed observable for each binding, which defeats most of the point of putting it on the prototype. And it's a little annoying having to decipher how many parentheses to use each time I access an observable.
It's also somewhat fragile. If I attempt to access Points() in code, I can't do
var points = 0;
var p = item.Points;
if (p && typeof p === 'function') {
points += p();
}
because that changes to context of Points() to DOMWindow, instead of item.
If I put the computed in create() in the mapping, I could capture the context, but then there's a copy of the method on each object instance.
I've found Michael Best's Google Groups post (http://groups.google.com/group/knockoutjs/browse_thread/thread/8de9013fb7635b13). The prototype returns a new computed observable on "activate". I haven't figured out what calls "activate" (maybe Objs?), but I'm guessing it still happens once per object, and I haven't a clue what scope 'this' will get.
At this point, I believe I'm past what's available in published docs, but I'm still working up to deciphering what's going on from the source.
You mention that you don't want to have an instance of the ko.computed
function on each instance of your javascript class, however, that won't really work with how ko's functionality has been built. When you use ko.computed
or ko.observable
they create specific memory pointers to private variables inside that you would not normally want to be shared across class instances (although in rare cases you might).
I do something like this:
var Base = function(){
var extenders = [];
this.extend = function(extender){
extenders.push(extender);
};
this.init = function(){
var self = this; // capture the class that inherits off of the 'Base' class
ko.utils.arrayForEach(extenders, function(extender){
// call each extender with the correct context to ensure all
// inheriting classes have the same functionality added by the extender
extender.call( self );
});
};
};
var MyInheritedClass = function(){
// whatever functionality you need
this.init(); // make sure this gets called
};
// add the custom base class
MyInheritedClass.prototype = new Base();
then for the computed observables (which HAVE to be instance functions on each instance of your MyInheritedClass
) I just declare them in an extender
like so:
MyInheritedClass.prototype.extend(function(){
// custom functionality that i want for each class
this.something = ko.computed(function() {
return 'test';
});
});
Given your example and the Base
class defined above, you could easily do:
var Item = function(data) {
var item = this;
ko.mapping.fromJS(data, mapping, item);
this.init(); // make sure this gets called
};
Item.prototype = new Base();
Item.prototype.extend(function () {
var self = this;
this.Points = ko.computed(function () {
// PointsFromUser may be 0, so only ignore it if the value is undefined/null
return (self.PointsFromUser != null) ?
self.PointsFromUser : self.PointsCalculated;
});
};
Then all instances of your Item
class will have a Points
property, and it will correctly handle the ko.computed
logic per instance.