1
votes

My sample is working, but I am confused about the location of the ko.applyBindings() statement. I used this approach to populate my ViewModel from a single getJSON request. But suppose I need 2 getJSON requests. I moved the "var viewModel = new MyViewModel();" outside of the getJSON, but the ko.applyBinding() was in both getJSON methods, and I understand you should NOT have 2 bindings to the same VM. I tried moving the ko.applyBinding() below the getJSON, but nothing worked. So I left the ko.applyBinding() inside one of the getJSON methods, and called VM method to set the variable from the other JSON call. It seems to work, but I am concerned if there is a timing issue that may cause issues if the JSON requests return at different times.

var MyViewModel = function() {
    var self = this;
    self.types = ko.observableArray();
    self.states = ko.observableArray();
    self.loadStates = function (states){
        self.states = states;
    }
}
var viewModel = new MyViewModel();
$(function () {
    $.getJSON('json/typeArray.json', function(jTypes){
        viewModel.types = jTypes;
        ko.applyBindings(viewModel);
    });
    $.getJSON('json/stateArray.json', function(jStates){
        viewModel.loadStates(jStates);
        //ko.applyBindings(viewModel);
    });
});

I could use nested JSON requests, but I would prefer them to execute at the same time.

Why can't the ko.applyBindings(viewModel) be moved to the bottom of this script? I tried, but neither of my arrays get populated.

Update: Yes, there is a timing problem. Sometimes the 2nd "states" array gets updated in the UI, and sometimes it does not. It evidently depends on which getJSON returns first. So I do need to find a solution to this problem.

Here is the attempt to move the applyBindings after viewModel creation, which did not work (see comment):

var MyViewModel = function() {
    var self = this;
    self.name = "myViewModel";
    self.states = ko.observableArray();
    self.types = ko.observableArray();
    self.loadStates = function (states){
        self.states = states;
        console.log("Set states in viewModel: " + self.states);
    }
}

var viewModel = new MyViewModel();
ko.applyBindings(viewModel);

$(function () {
    $.getJSON('json/typeArray.json', function(jTypes){
        console.log("Setting types in viewModel: " + viewModel.name);
        viewModel.types = jTypes;
        //ko.applyBindings(viewModel);
    });
    $.getJSON('json/stateArray.json', function(jStates){
        console.log("Setting states in viewModel: " + viewModel.name);
        viewModel.loadStates(jStates);
        //ko.applyBindings(viewModel);
    });
});
2
applyBindings should be called once, e.g. immediately after viewModel creation. You should update observable properties in get json done function: viewModel.types(jTypes) - i suppose jTypes is an array. - TSV
I tried putting the applyBindings after the viewModel creation, but the arrays never get update on the HTML page. When I uncomment the applyBindings which are inside the getJSON, it works fine. Note that the console.log entries indicate that the viewModel is properly getting updated in all cases. - Scott Forbes
In addition to what @TSV says you can use Promise.all() to ensure all your json is loaded before your callback is invoked: see example jsfiddle.net/axLokczs/2 - Michael
Here is a ko example jsfiddle.net/axLokczs/3 - Michael
The race-problem you mention is solved if you use my suggestion (Promise.all()) - Michael

2 Answers

1
votes

The thing is that you set new values to observable arrays, and do not assign new object to bound properties.

Instead of assigning:

viewModel.types = jTypes;

I propose to use updating:

//viewModel.types(jTypes);
viewModel.types(["type a", 'type b', 'type c']);

I've created a sample (request is emulated via setTimeout), on startup arrays are empty, "times" are updated in 1 second, "states" are updated in 2 seconds:

var MyViewModel = function() {
    var self = this;
    self.name = "myViewModel";
    self.states = ko.observableArray();
    self.types = ko.observableArray();
}

var viewModel = new MyViewModel();
ko.applyBindings(viewModel);

//$(function () {
//    $.getJSON('json/typeArray.json', function(jTypes){
//        viewModel.types(jTypes);
//    });
//    $.getJSON('json/stateArray.json', function(jStates){
//        viewModel.states(jStates);
//    });
//});

//$.getJSON('json/typeArray.json', function(jTypes){
setTimeout(function() {
    viewModel.types(["type a", 'type b', 'type c'])
}, 1000);

//$.getJSON('json/stateArray.json', function(jStates){
setTimeout(function() {
    viewModel.states(["state d", 'state e', 'state f'])
}, 2000);

// ever more - update types again in 5 sec
//$.getJSON('json/typeArray.json', function(jTypes){
setTimeout(function() {
    viewModel.types(["type g", 'type h', 'type i', 'type j'])
}, 5000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<div>States:</div>
<!-- ko if: states().length === 0 -->
<div>There are no states for a while...</div>
<!-- /ko -->
<!-- ko foreach: states -->
<div data-bind="text: $data"></div>
<!-- /ko -->
  
<div>Types:</div>
<!-- ko if: types().length === 0 -->
<div>There are no types for a while...</div>
<!-- /ko -->
<!-- ko foreach: types -->
<div data-bind="text: $data"></div>
<!-- /ko -->
0
votes

Note that in this answer I'm explicitly reinventing the wheel to show a basic concept for how this could work. There are libraries, including the one you seem to already be using (jQuery), that make this task a lot easier and fluent.

Basically, you want to run ko.applyBindings, but only after two seperate asynchronous requests are completed. Here's a pattern to do that:

var viewModel = new MyViewModel();

function tryApplyBindings() {
  if (viewModel.types().length > 0 &&
      viewModel.states().length > 0) {
    ko.applyBindings(viewModel);
  }
}

$(function () {
    $.getJSON('json/typeArray.json', function(jTypes){
        console.log("Setting types in viewModel: " + viewModel.name);
        viewModel.types = jTypes;
        tryApplyBindings();
    });
    $.getJSON('json/stateArray.json', function(jStates){
        console.log("Setting states in viewModel: " + viewModel.name);
        viewModel.loadStates(jStates);
        tryApplyBindings();
    });
});

However, again: note that this reinvents the wheel. You can use the promise features of jQuery (e.g. with this approach, perhaps) to make a more elegant and DRY combination of promises.


As a footnote, you could also consider running applyBindings directly, and create a view that looks nice with empty arrays as well. Then, when the promises return, the UI gets updated automatically.