1

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);
    });
});
Community
  • 1
  • 1
  • 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 Mar 26 '16 at 20:53
  • 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 Mar 26 '16 at 21:45
  • 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 https://jsfiddle.net/axLokczs/2/ – Michael Mar 27 '16 at 02:48
  • Here is a ko example https://jsfiddle.net/axLokczs/3/ – Michael Mar 27 '16 at 02:56
  • The race-problem you mention is solved if you use my suggestion (`Promise.all()`) – Michael Mar 28 '16 at 11:45

2 Answers2

1

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 -->
TSV
  • 7,538
  • 1
  • 29
  • 37
  • Works perfect. The async getJSON() shouldn't have caused a problem with the observableArray- which is why I was confused. But I was resetting the OBJECT, and the new object (jTypes) wasn't observable. I needed to set the VALUES of the current object, and I just didn't know how to do that. Replacing the = with viewModel.types(jTypes) did the job. The "Promise" technique is great when you need to wait for the requests to complete, but that was not the usecase here. Thanks for the correct solution, and the running code snippet. – Scott Forbes Mar 28 '16 at 23:45
0

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.

Community
  • 1
  • 1
Jeroen
  • 60,696
  • 40
  • 206
  • 339