Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to tell knockout to wait to recalculate computed values until after the view model is defined?

I have a complex view model which is a couple hundred lines of javascript code with a good amount of observable properties, computed observable properties, writable computed observable properties and functions. So managing this is quite a bit of a challenge.

An annoying problem that I've had to deal with is that computed observables are calculated immediately right when you define it. So using variables that haven't been defined yet in the view model at the point of defining the observable lead to errors stating that the variable hasn't been defined. It is... just later in the file.

Here's a contrived example:

function ViewModel1​(args) {
    var self = this;

    self.firstName = ko.observable(args.firstName);
    self.lastName = ko.observable(args.lastName);
    self.fullName = ko.computed(function () {
        return self.firstName() + ' ' + self.lastName();
    });
}

function ViewModel2​(args) {
    var self = this;

    self.fullName = ko.computed(function () {
        // uh oh, where's firstName?
        return self.firstName() + ' ' + self.lastName();
    });
    self.firstName = ko.observable(args.firstName);
    self.lastName = ko.observable(args.lastName);
}

Using ViewModel1 will work with no problems. At the point fullName is defined, firstName and lastName is defined so it works as expected. Using ViewModel2 will not work. There would be an Error in the computed function stating firstName is not defined.

What I've been doing until now was to ensure that all computed observables are defined after all dependent variables have been defined. The problem with this is that in doing so, things are defined in seemingly random places when I would rather keep related variables defined close together.

One nice solution that I've come up with is to define an "initializing" observable set to true and make all computed observables test if it's still initializing and calculate and return the value when it isn't. That way, the attempts to access the currently undefined variable will not be made.

function ViewModel3(args) {
    var self = this;
    var initializing = ko.observable(true);

    self.fullName = ko.computed(function () {
        if (!initializing()) {
            return self.firstName() + ' ' + self.lastName();
        }
    });
    self.firstName = ko.observable(args.firstName);
    self.lastName = ko.observable(args.lastName);

    initializing(false);
}

But this won't be very practical in my case however. I have many computed observables, so doing this in all of them will make it very bloated, remember I have a lot of these. Throttling it doesn't seem to make a difference.

Is there a way to tell knockout to wait before trying to calculate the values of computed observables? Or is there a better way for me to structure my code to deal with this?

I could probably make some helper functions to manage the initialization logic, but I'd still have to alter all of the computed observable definitions. I suppose I can monkey patch knockout to add this initializing logic as I'm not aware knockout has such options which I might just do. I've looked at the source for computed observables before but I don't know if there's already a setting elsewhere.

jsfiddle demo

like image 751
Jeff Mercado Avatar asked Jul 19 '12 21:07

Jeff Mercado


1 Answers

Computed observables accept a deferEvaluation option that prevents the initial evaluation from happening until something actually tries to retrieve the computed's value.

You would define it like:

self.fullName = ko.computed({
   read: function() {
       return self.firstName() + " " + self.lastName();
   },
   deferEvaluation: true
});

Just for completeness, you could also specify it like:

this.fullName = ko.computed(function() {
       return this.firstName() + " " + this.lastName();
 }, this, { deferEvaluation: true });

Or you could wrap it like:

ko.deferredComputed = function(evaluatorOrOptions, target, options) {
   options = options || {};

   if (typeof evaluatorOrOptions == "object") {
       evaluatorOrOptions.deferEvaluation = true;   
   } else {
       options.deferEvaluation = true;
   }

   return ko.computed(evaluatorOrOptions, target, options);
};
like image 123
RP Niemeyer Avatar answered Nov 03 '22 20:11

RP Niemeyer