Search code examples
javascriptjqueryjsonajaxjquery-ajax

jQuery - How to show elements from GitHub API as desired in an unordered list?


I am trying to get some data from the GitHub API using jQuery(AJAX), and appending it in a static webpage. Below are my HTML and JS snippets.

        $(document).ready(function(){
            $.ajax({
                url: 'https://api.github.com/repos/FearlessPython/Basics-of-Python',
            }).done(function(repo){
                $('#heading').html(`${repo.name}<hr>`);
                $('#stats').html(`
                    <strong>Stars</strong>: ${repo.stargazers_count} <br>
                    <strong>Watchers</strong>: ${repo.watchers_count} <br>
                    <strong>Language</strong>: ${repo.language} <br>
                    <strong>Open Issues</strong>: ${repo.open_issues_count} <br>
                    <strong>License</strong>: ${repo.license.name}<br>
                `);
            });
            $.ajax({
                method: "GET",
                url: "https://api.github.com/repos/FearlessPython/Basics-of-Python/contributors",
                dataType: "json"
            }).done(function(data){
                $.each(data, function(index, url){
                    $.ajax({
                        url: data[index].url
                    }).done(function(individual_contributor){
                        // console.log(data[index].url);
                        $('ul#users').append(`
                            <li><strong>${individual_contributor.name}</strong></li>
                            <li><strong>Bio:</strong> ${individual_contributor.bio}</li>
                            <li><strong>Public Repos:</strong> ${individual_contributor.public_repos}</li>
                            <li><strong>Followers:</strong> ${individual_contributor.followers}</li>
                            <li><strong>Following:</strong> ${individual_contributor.following}</li>
                        `)
                    });
                });
                $.map(data, function(contributor, i){
                    // contrb.push(contributor);
                    $('ol#contributors').append(`
                    <li><a href="${contributor.html_url}" target="_blank">${contributor.login}</a></li>

                    <img src = "${contributor.avatar_url}" style = "float: right;" width=10%>   
                    <ul id="users"></ul>
                    `);
                });
            });
        });
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://code.jquery.com/jquery-3.5.1.js" integrity="sha256-QWo7LDvxbWT2tbbQ97B53yJnYU3WhH/C8ycbRAkjPDc=" crossorigin="anonymous"></script>
</head>
<body>Here, dynamic statistics are available regarding this repository in GitHub.

    <div id="statistics">
        <h3 id="heading"></h3>
        <ul id="stats"></ul>
        <h4>Contributors</h4><hr>
        <ol id="contributors">
            <!-- <ul id="users"></ul> -->
        </ol>
    </div>
</body>
</html>

You can see that under each user, the same thing iterates again and again. Using .html() instead of .append() overwrites the first iteration data and everything gets replaced by the latest iteration's data. The ideal result should be like this:

Desired View

I also tried to use for loop, but the same thing is happening. I am trying to solve it from the last few days, but unable to solve it. I have just started using jQuery and AJAX.

Note: In case if the GitHub API requests exceed for you, then you won't be able to see the result of the above code snippets. So I am also including an image of the current situation of the Webpage.

enter image description here


Solution

  • After you get the contributor list you're performing two iterations. The $.each, in which you fire a request to get the contributor details, which will be appended to a container with id='users'. (That doesn't exist yet!)

    $.each(data, function(index, url){
       // request contributor details
    }
    

    and the $.map which appends the login and avatar to a list but also a container <ul id='users'>.

    $.map(data, function(contributor, i){
        $('ol#contributors').append(`
         <li><a href="${contributor.html_url}" target="_blank">${contributor.login}</a></li>
         <img src = "${contributor.avatar_url}" style = "float: right;" width=10%>   
         <ul id="users"></ul>`  // this will cause you trouble
    );
    });
    

    Now, ids should be unique within the document, so appending extra elements with the same id won't work as expected.

    See, you have a racing condition in which your contributor details handler depends on an element that you haven't created yet. Anyway, when it resolves, its contents will be appended to whatever element is found when looking for $('#users'), most probably the first one in the DOM.

    Rounding up:

    • perform just one iteration over the contributor list
    • append the contributor details to the corresponding user <ul>
    • don't use ids for a container you mean to repeat across a list (or at least compute its id instead of hardcoding it)

    When you get the list, I would iterate using for..of. then on each loop I'd declare the <ul> element (which becomes block scoped) and use that reference to append the user details when the child request resolves.

      for (let contributor of contributors) {
        // block scoped
        let userDetailsElement = $('<ul class="users"></ul>');
    
        $('ol#contributors')
          .append(userHeader(contributor))
          .append(userDetailsElement);
    
        $.getJSON(contributor.url)
         .then( individual_contributor => {
          // I know   which <ul> I'm appending to
            userDetailsElement.append(
                  contributorDetailsHtml(individual_contributor)
            );
        });
      }
    

    In the following snippet I'm declaring jQuery's callback as async, therefore I could also await the contributor details just like

    let individual_contributor = await $.getJSON(contributor.url);
    
    userDetailsElement.append(
                  contributorDetailsHtml(individual_contributor)
            );
    

    awaiting on each user's detail means that requests will be ran sequentially, whereas firing a thenable request will do it in parallel. If you were about to query a repo with hundreds of contributors the choice can make a difference not just because of performance but also because you will get throttled and a part of your dom won't render.

    const repoUrl = 'https://api.github.com/repos/FearlessPython/Basics-of-Python';
    
    function repoStatsHtml(repo) {
      return `
        <strong>Stars</strong>: ${repo.stargazers_count} <br>
        <strong>Watchers</strong>: ${repo.watchers_count} <br>
        <strong>Language</strong>: ${repo.language} <br>
        <strong>Open Issues</strong>: ${repo.open_issues_count} <br>
        <strong>License</strong>: ${repo.license.name}<br>
        `;
    }
    
    function contributorDetailsHtml(individual_contributor) {
      return `
        <li><strong>${individual_contributor.name}</strong></li>
        <li><strong>Bio:</strong> ${individual_contributor.bio}</li>
        <li><strong>Public Repos:</strong> ${individual_contributor.public_repos}</li>
        <li><strong>Followers:</strong> ${individual_contributor.followers}</li>
        <li><strong>Following:</strong> ${individual_contributor.following}</li>
                            `;
    }
    
    function userHeaderHtml(contributor) {
      return `
        <li><a href="${contributor.html_url}" class="login" target="_blank">${contributor.login}</a></li>
        <img src = "${contributor.avatar_url}" class="avatar">   
                        `;
    }
    $(document).ready(async function() {
      $.getJSON(repoUrl).done(function(repo) {
        $('#heading').html(`${repo.name}<hr>`);
        $('#stats').html(repoStatsHtml(repo));
      });
      let contributors = await $.getJSON(`${repoUrl}/contributors`);
    
      for (let contributor of contributors) {
        let userDetailsElement = $('<ul class="users"></ul>');
        $('ol#contributors')
          .append(userHeaderHtml(contributor))
          .append(userDetailsElement);
        $.getJSON(contributor.url).then(individual_contributor => {
          userDetailsElement.append(
            contributorDetailsHtml(individual_contributor)
          );
        });
      }
    
    });
    .users {
      min-width: 90px;
      min-height: 90px;
      margin-bottom: 0.5em;
    }
    
    .avatar {
      float: right;
      width: 10%;
    }
    
    .login {
      margin-top: 0.5em;
      font-size: 1.1em;
      text-decoration: none;
    }
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <script src="https://code.jquery.com/jquery-3.5.1.js" integrity="sha256-QWo7LDvxbWT2tbbQ97B53yJnYU3WhH/C8ycbRAkjPDc=" crossorigin="anonymous"></script>
    </head>
    
    <body>Here, dynamic statistics are available regarding this repository in GitHub.
    
      <div id="statistics">
        <h3 id="heading"></h3>
        <ul id="stats"></ul>
        <h4>Contributors</h4>
        <hr>
        <ol id="contributors">
        </ol>
      </div>
    </body>
    
    </html>