Are computed observables on prototypes feasible in KnockoutJS?
Asked Answered
C

3

6

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.

Crawly answered 24/4, 2012 at 14:22 Comment(0)
G
7

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.

Glazing answered 25/4, 2012 at 0:56 Comment(3)
Thanks. I think the purpose of .extend() just clicked.This solves my scoping / double function invocation problems nicely, and I'll take your word that Knockout won't let me keep the actual computed observable on the prototype.Crawly
eric, could you provide a more "sugarified" version? Something like childClass = BaseClass.extend(function(){/*...*/}) that already does the prototype chain setup? Having to do it manually for each class instance is a bit weird...Litigate
I don't see any reason why you couldn't put an 'extend' static function on the inherited Constructor, as long as it basically does: SubClass.extend = function(extender){ SubClass.prototype.extend(extender); };Glazing
D
0
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;
});

};

Dianetics answered 13/2, 2015 at 13:58 Comment(0)
D
0
Item.prototype.extend(function () {
var scope = this
this.Points = ko.computed(function () {
    return (this.PointsFromUser != null) ? 
           this.PointsFromUser : this.PointsCalculated;
}, scope);
};
Dianetics answered 13/2, 2015 at 14:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.