1

Here is some code that replicates the issue

places = ["London","Paris","Brussels"];
placeWikis = ko.observableArray([]);

var viewModel = function(){

for (i in places) {
    function wikiInit(callback){
        this.wikiMarkup = "";

        var wikiTitle = places[i].replace(" ", "_");
        var wikiURL = "https://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro=&explaintext=&titles="+places[i];

        $.ajax({
            type:"GET",
            dataType:"jsonp",
            async: false,
            url: wikiURL,

            success: function(result){
                callback(result.query.pages[Object.keys(result.query.pages)[0]].extract);
            },
            error: function(){

            }
        });
    };

    wikiInit(function (wikiString) {
        var wikiMarkup = "<h4>Wikipedia:</h4>" + "<p>" + wikiString + "</p>";

        placeWikis.push(wikiMarkup);

        console.log(placeWikis());
    });
}

ko.applyBindings(new viewModel(), view);

The issue is every time this code runs and returns data from the source, Wikipedia in this case, the results for placesWikis() are ordered randomly. I am adding the API results separately on a different <div>, so I need the indexes for the results to be the same as the indexes for their respective entries in places[]

Brushline
  • 47
  • 5
  • 1
    I think you might have a well-disguised duplicate of this question: [Elements order in a “for (… in …)” loop](http://stackoverflow.com/a/280861/419956)? If order is important, you should sort yourself. (Note that, if you need a *stable* sort you should have an explicit tiebreaker too, as array sorting in Javascript isn't guaranteed to be a stable sort - AFAIK.) – Jeroen Feb 12 '16 at 06:30
  • So I guess that means there is no easy solution. Great. – Brushline Feb 12 '16 at 06:43
  • Why not? If you change from `for..in` to a regular `for` loop things should work out, right? – Jeroen Feb 12 '16 at 06:59
  • Hmm, I was wrong. That *might* be an issue, though probably not the one you're seeing. Instead, it's the fact that the Ajax success handlers return in non-deterministic order as they're handled asycnhronously. I've posted an answer with some two possible solutions. (In addition, I suggest replacing `for..in` with `for` anyways.) – Jeroen Feb 12 '16 at 07:55

1 Answers1

1

You have two issues with order:

  • for.in does not guarantee order;
  • you fill the observableArray asynchronously in callbacks, but there is no guarantee that they will arrive in order you fire them off!

The latter is probably the most prominent issue. You currently push results into your observable array, but you do so in the order by which the browser returns results.

Here's a slightly more minimal repro of your problem, with the differences callback timings exagerated:

// Mock Ajax stuff:
var $ = {
  ajax: function(options) {
    // Simulate unknown return moment with setTimeout:
    setTimeout(function() {
      options.success(options.url + " is a great city...");
    }, Math.random() * 1500 + 50);
  }
}

function ViewModel() {
  var self = this;
  self.items = ko.observableArray([]);
  
  var places = ["london", "brussels", "paris"];
  
  function wikiInit(place, callback) {
    $.ajax({
      url: "http://en.wikipedia.org/w/" + place,
      success: function(result) {
        callback(result);
      }
    });
  }
  
  for (var i = 0; i < places.length; i++) {
    wikiInit(places[i], function(wikiString) {
      self.items.push(wikiString);
      console.log(self.items());
    });
  }
}

ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<ul data-bind="foreach: items">
  <li data-bind="text: $data"></li>
</ul>

As you can see, every time your run this you have a chance the results are out of order.

Option 1 is to splice new items into the array in the position you want them in (instead of using push). This is IMO a bit convoluted approach, and I'll leave the code to do so to the reader.

Option 2, and a probably easier approach is to prepare the array beforehand, and on the callbacks fill the array. E.g. like this:

// Mock Ajax stuff:
var $ = {
  ajax: function(options) {
    // Simulate unknown return moment with setTimeout:
    setTimeout(function() {
      options.success(options.url + " is a great city...");
    }, Math.random() * 1500 + 50);
  }
}

function Place(data) {
  this.place = data.place;
  this.wikistuff = ko.observable("");
}

function ViewModel() {
  var self = this;
  self.items = ko.observableArray([]);
  
  var places = ["london", "brussels", "paris"];
  
  function wikiInit(place, callback) {
    var placeVm = new Place({ place: place });
    self.items.push(placeVm);
    $.ajax({
      url: "http://en.wikipedia.org/w/" + place,
      success: function(result) {
        callback(result, placeVm);
      }
    });
  }
  
  for (var i = 0; i < places.length; i++) {
    wikiInit(places[i], function(wikiString, placeVm) {
      placeVm.wikistuff(wikiString);
    });
  }
}

ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<ul data-bind="foreach: items">
  <li data-bind="text: wikistuff"></li>
</ul>

This result is always in order of the original array.

It currently always shows the bullets, even before the callback completed. I only did so to demonstrate how the solution works, but you could easily do this...

<li data-bind="text: wikistuff, visible: !!wikistuff()"></li>

...if needed.

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