Zoltán Tamási
Zoltán Tamási

Reputation: 12754

A computed which "sleeps" until some initialization completes

I have a complex viewmodel, which holds several observable properties, arrays, etc. I have a computed observable which has several dependencies. The logic inside the computed mustn't be executed at definition time, but only after the model is fully initialized.

Example:

Image a form where users can select one of more continents, countries or cities. Each time some continents get selected, the list of countries should contain only those which are located on the selected continents, and the same for cities, etc.

For optimizing HTTP traffic, the initial lists are populated as part of the page, so that no initial JSON request is needed.

var viewModel = function(data) {
  this.Continents = ko.observableArray(data.Continents);
  this.Countries = ko.observableArray(data.Countries);
  this.Cities = ko.observableArray(data.Cities);

  this.SelectedContinents = ko.observableArray(data.SelectedContinents);
  this.SelectedCountries = ko.observableArray(data.SelectedCountries);
  this.SelectedCities = ko.observableArray(data.SelectedCities);

  this.LoadFromServer = function() {
    $.post({
      url: '/reloadLists',
      data: { continents: this.SelectedContinents(), countries: this.SelectedCountries(), cities: this.SelectedCities() },
      success: function(result) {
        this.Continents(result.Continents);     
        this.Countries(result.Countries);
        this.Cities(result.Cities);
      } 
    });
  };

  ko.computed(function() {
    this.LoadFromServer();
  }, this);

}

...

var data = ... // initial data rendered on server-side 
var model = new viewModel(data);
ko.applyBindings(model);

With this approach, the problem is that the LoadFromServer logic gets executed also at the model initialization when the computed initially executes the rad function. This way there is a redundant round-trip, because the initial lists are already in the model.

The only solution I can think of right now is to introduce a flag to block the concrete logic until it's needed. This flag shouldn't be an observable because then when I set it to true at the end of the constructor, the computed will get re-evaluated, and the redundant round-trip goes again. However, if the flag is not an observable, then I have to make sure that the dependencies are caught at initialization time, so that it can react to changes afterwards. Putting all this together, the currenct result looks something like this.

var viewModel = function(data) {

  var initialized = false;

  this.Continents = ko.observableArray(data.Continents);
  this.Countries = ko.observableArray(data.Countries);
  this.Cities = ko.observableArray(data.Cities);

  this.SelectedContinents = ko.observableArray(data.SelectedContinents);
  this.SelectedCountries = ko.observableArray(data.SelectedCountries);
  this.SelectedCities = ko.observableArray(data.SelectedCities);

  this.LoadFromServer = function() {
    $.post({
      url: '/reloadLists',
      data: { continents: this.SelectedContinents(), countries: this.SelectedCountries(), cities: this.SelectedCities() },
      success: function(result) {
        this.Continents(result.Continents);     
        this.Countries(result.Countries);
        this.Cities(result.Cities);
      } 
    });
  };

  ko.computed(function() {
    var catchDependencies = [this.SelectedContinents(), this.SelectedCountries(), this.SelectedCities()];
    if (!initialized) return;
    this.LoadFromServer();
  }, this);  

  initialized = true;
}

This is technically a good solution but I don't quite like it because it has some smell for me.

Is there any nicer solution for these scenarios? Or I just shouldn't try to optimize things and let the initial AJAX load instead of the server-side initialo data rendering?

Upvotes: 0

Views: 75

Answers (4)

JotaBe
JotaBe

Reputation: 39004

What you need to use is the computed observable deferEvaluation option:

Optional. If this option is true, then the value of the computed observable will not be evaluated until something actually attempts to access its value or manually subscribes to it. By default, a computed observable has its value determined immediately during creation.

Te idea is setting this option, and manually accessing the observable when the initialization has finished, so that it "becomes active". Of course, you have to store it in a "private" variable to be able to access it.

Please, see also this: Lazy Loading an Observable in KnockoutJS

Upvotes: 0

Roy J
Roy J

Reputation: 43881

To me, a computed that doesn't return a value is a code smell, because it's a tool with one purpose that you're using as a sort of multi-subscribe. It would be clearer to set up explicit subscriptions:

  this.SelectedContinents.subscribe(this.LoadFromServer);
  this.SelectedCountries.subscribe(this.LoadFromServer);
  this.SelectedCities.subscribe(this.LoadFromServer);

It is not clear to me whether LoadFromServer needs to be externally visible; you could do it as a private function.

Upvotes: 2

Dandy
Dandy

Reputation: 2177

Try

ko.computed(function() {
  if(ko.computedContext.isInitial()) return;
  this.LoadFromServer();
}, this);

Read more Here.

Upvotes: 2

Rajesh
Rajesh

Reputation: 24915

Hi I'm not sure if this is the best way but it works.

So my approach is, I have created a flag called isComplete and if this is true then show computed value else show default value. This is for HTML so that bindings does not fail.

Also I have added a button where I'm registering computed value. it can also do it on subscribe of certain value.

var ViewModel = function(first, last) {
    var self = this;
    self.firstName = ko.observable(first);
    self.lastName = ko.observable(last);
 	self.isComplete = ko.observable(false);
  
    // Computed registered section
    self.registerComputed = function(){
        if(!self.hasOwnProperty("fullName")){
            self.fullName = ko.computed(function() {
                return this.firstName() + " " + this.lastName();
            }, this);
        }
        self.isComplete(true);
    }
};
 
ko.applyBindings(new ViewModel("Planet", "Earth"));
body { font-family: arial; font-size: 14px; }
.liveExample { padding: 1em; background-color: #EEEEDD; border: 1px solid #CCC; max-width: 655px; }
.liveExample input { font-family: Arial; }
.liveExample b { font-weight: bold; }
.liveExample p { margin-top: 0.9em; margin-bottom: 0.9em; }
.liveExample select[multiple] { width: 100%; height: 8em; }
.liveExample h2 { margin-top: 0.4em; font-weight: bold; font-size: 1.2em; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class='liveExample'>   
    <p>First name: <input data-bind='value: firstName' /></p> 
    <p>Last name: <input data-bind='value: lastName' /></p> 
    <h2>Hello, <span data-bind='text: isComplete()?fullName():""'> </span>!</h2>  
    <button data-bind="click:registerComputed, text:'Register Full Name'"></button>
</div>

Upvotes: 0

Related Questions