0

I'm writing a script that pulls data from a Twitch.tv API for a number of channels specified in an array, and then populates a table with the name of the channels and what's streaming. Here's the JS code followed by the HTML:

var channels = ["ESL_SC2", "freecodecamp", "OgamingSC2", "noobs2ninjas"];

$(document).ready(function() {
 $( window ).on("load", function() {
    for (var i = 0; i < channels.length; i++) {
      $.getJSON("https://wind-bow.gomix.me/twitch-api/streams/" + channels[i] + "?callback=?", function(json) {
      var json = JSON.stringify(json);
      var obj = JSON.parse(json);
      var streamStatus = obj.stream;
      if (streamStatus === null) {
        streamStatus = "Offline";
      }
      else {
        streamStatus = "Streaming " + obj.stream.channel.status;
      }
      var para = document.createElement("div");
      var node = document.createTextNode(streamStatus);
      para.appendChild(node);
      var element = document.getElementById("status");
      element.appendChild(para); 
      });
    }
    for (var i = 0; i < channels.length; i++) {
      var para = document.createElement("div");
      var node = document.createTextNode(channels[i]);
      para.appendChild(node);
      var element = document.getElementById("title");
      element.appendChild(para);
    }
  });
});
<head>
  <script src="http://code.jquery.com/jquery-latest.js"></script>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>

<div class="row">
  <div class="col-sm-3"></div>
  <div class="col-sm-6 center-column">
    <h3>Twitch.tv Viewer</h3>
    <div class="row">
      <div class="col-sm-2" id="title"></div>
      <a href="" id="link" target="_blank" style="text-decoration:none;"><div id="status"></div></a>
    </div>
  </div>
  <div class="col-sm-3"></div>
</div>

Here's my problem: every time the page loads, the order of the items in the second column (the 'status' div, letting you know what's streaming) ends up in a different order, which means they no longer line up correctly with the channels in the first column (the 'title' div).

I suspect it has something to do with the API call, and the for loop being asynchronic, as suggested here. But I can't figure out how to implement the solution proposed. Any advice would be greatly appreciated!

Community
  • 1
  • 1
kh_one
  • 257
  • 2
  • 9

5 Answers5

1

Well, yes, you are right - the problem you face is that every time you make an API call in for loop it takes a different amount of time to process, so some callbacks could strike earlier that another breaking your for loop order.

The best practice to work with is using Promises as described here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

In your current code you can wait for all callbacks to finish and only then render the data to the DOM (but it's not a best practice):

var channels = ["ESL_SC2", "freecodecamp", "OgamingSC2", "noobs2ninjas"];

$(document).ready(function() {
 $( window ).on("load", function() {
      var getData = function(i){
        $.getJSON("https://wind-bow.gomix.me/twitch-api/streams/" + channels[i] + "?callback=?", function(json) {
        var json = JSON.stringify(json);
        var obj = JSON.parse(json);
        var streamStatus = obj.stream;
        if (streamStatus === null) {
          streamStatus = "Offline";
        }
        else {
          streamStatus = "Streaming " + obj.stream.channel.status;
        }
        channels[i].streamStatus = streamStatus;
        var para = document.createElement("div");
        var node = document.createTextNode(streamStatus);
        para.appendChild(node);
        var element = document.getElementById("status");
        element.appendChild(para); 
        if(i == channels.length -1){
          getData(++i);
        }
      });
    }
    getData(0);
    for (var i = 0; i < channels.length; i++) {
      var para = document.createElement("div");
      var node = document.createTextNode(channels[i]);
      para.appendChild(node);
      var element = document.getElementById("title");
      element.appendChild(para);
    }
  });
});

In this example script uses a looped function and you load the next iteration only when the previous one was finished.

Note, that this will affect the performance - because it actually kills the perfection of asynchronous API calls; Still this way would be much easier for you to implement and manage it.

lut
  • 130
  • 1
  • 9
0

Just receive next portion of data at end of your getJSON callback:

function updateNextStream(){
if (i>=channels.length)return;
$.getJSON("https://wind-bow.gomix.me/twitch-api/streams/" + channels[i] + "?   callback=?", function(json) {
  var json = JSON.stringify(json);
  var obj = JSON.parse(json);
  var streamStatus = obj.stream;
  if (streamStatus === null) {
    streamStatus = "Offline";
  }
  else {
    streamStatus = "Streaming " + obj.stream.channel.status;
  }
  var para = document.createElement("div");
  var node = document.createTextNode(streamStatus);
  para.appendChild(node);
  var element = document.getElementById("status");
  element.appendChild(para);
  i++;
  updateNextStream();
  });
  }
roma2341
  • 111
  • 2
  • 10
0

One way you could solve this is to make a recursive function. This allows you to "wait" for each AJAX request and when the request is handled, it then goes to fetch the next channel.

var channels = ["ESL_SC2", "freecodecamp", "OgamingSC2", "noobs2ninjas"];

var ci = 0; // Channel index
(function getChannels(){
    if(ci >= channels.length)
        return; // Stop the recursion if we've iterated all channels
    $.getJSON("https://wind-bow.gomix.me/twitch-api/streams/" + channels[ci] + "?callback=?", function(json) {
        var json = JSON.stringify(json);
        var obj = JSON.parse(json);
        var streamStatus = obj.stream;
        if (streamStatus === null) {
            streamStatus = "Offline";
        }
        else {
            streamStatus = "Streaming " + obj.stream.channel.status;
        }
        var para = document.createElement("div");
        var node = document.createTextNode(streamStatus);
        para.appendChild(node);
        var element = document.getElementById("status");
        element.appendChild(para);
        ci++;
        getChannels();
    });
    var para = document.createElement("div");
    var node = document.createTextNode(channels[ci]);
    para.appendChild(node);
    var element = document.getElementById("title");
    element.appendChild(para);
})();

https://jsfiddle.net/mnw2ojwg/

Mathias W
  • 1,433
  • 12
  • 16
0

That's because you're creating the "title" elements independently of the "status" elements, which get created in the API callback. The order in which the API requests will complete varies.

Just create them both at the same time and you shouldn't have this problem.

var channels = ["ESL_SC2", "freecodecamp", "OgamingSC2", "noobs2ninjas"];

$(document).ready(function() {
  for (var i = 0; i < channels.length; i++) {
    $.getJSON("https://wind-bow.gomix.me/twitch-api/streams/" + channels[i] + "?callback=?", dataCallback(channels[i]));
  }
  
  function dataCallback(channel) {
    return function(data) {
      var streamStatus = data.stream;
      
      if (streamStatus === null) {
        streamStatus = "Offline";
      } else {
        streamStatus = "Streaming " + data.stream.channel.status;
      }
      
      var para = document.createElement("div");
      var node = document.createTextNode(streamStatus);
      para.appendChild(node);
      var element = document.getElementById("status");
      element.appendChild(para);

      var para = document.createElement("div");
      var node = document.createTextNode(channel);
      para.appendChild(node);
      var element = document.getElementById("title");
      element.appendChild(para);
    }
  }
});
<head>
  <script src="http://code.jquery.com/jquery-latest.js"></script>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>

<div class="row">
  <div class="col-sm-3"></div>
  <div class="col-sm-6 center-column">
    <h3>Twitch.tv Viewer</h3>
    <div class="row">
      <div class="col-sm-2" id="title"></div>
      <a href="" id="link" target="_blank" style="text-decoration:none;">
        <div id="status"></div>
      </a>
    </div>
  </div>
  <div class="col-sm-3"></div>
</div>
Brother Woodrow
  • 6,092
  • 3
  • 18
  • 20
0

As the others said, the problem is that the results are returned asynchronously, but your logic relies on them returning in the same order they were called.

Here's an example solution that uses an ID from the returned JSON to match the returned status to the relevant element.

The status elements are created along with the title elements and display a status of 'Pending' until the real status is returned from the API.

var channels = ["ESL_SC2", "freecodecamp", "OgamingSC2", "noobs2ninjas"];

$(document).ready(function() {
  $(window).on("load", function() {
    for (var i = 0; i < channels.length; i++) {
      $.getJSON("https://wind-bow.gomix.me/twitch-api/streams/" + channels[i] + "?callback=?", function(json) {
        var json = JSON.stringify(json);
        var obj = JSON.parse(json);
        var streamStatus = obj.stream;
        if (streamStatus === null) {
          streamStatus = "Offline";
        } else {
          streamStatus = "Streaming " + obj.stream.channel.status;
        }

        //Update status
        var channelId = '';
        if (obj._links && obj._links.channel) {
          const parts = obj._links.channel.split('/');
          if (parts.length >= 6) {
            channelId = parts[5];
          } 
        }

        var statusEl = document.getElementById("channel_"+channelId);
        if (statusEl) {
          statusEl.innerHTML = streamStatus;
        }
      });
    }
    
    //cache relevant elements
    var titleEl = document.getElementById("title");
    var statusEl = document.getElementById("status");
    for (var i = 0; i < channels.length; i++) {
      var para = document.createElement("div");
      var node = document.createTextNode(channels[i]);
      para.appendChild(node);
      titleEl.appendChild(para);
      
      //create status elements
      para = document.createElement("div");
      para.id = "channel_"+channels[i];
      node = document.createTextNode('pending...');
      para.appendChild(node);
      statusEl.appendChild(para);
    }
  });
});
<head>
  <script src="http://code.jquery.com/jquery-latest.js"></script>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>

<div class="row">
  <div class="col-sm-3"></div>
  <div class="col-sm-6 center-column">
    <h3>Twitch.tv Viewer</h3>
    <div class="row">
      <div class="col-sm-2" id="title"></div>
      <a href="" id="link" target="_blank" style="text-decoration:none;">
        <div id="status"></div>
      </a>
    </div>
  </div>
  <div class="col-sm-3"></div>
</div>
Giladd
  • 1,351
  • 8
  • 9