Making API Calls (part 2)

In the first part of this tutorial, we used an API call to create a list of pokemon. This tutorial will pick up where we left off and work through to the final result.

Pagination

Our API call returned an array with 20 objects, but it indicated there were a total of 811 objects available. To see the remaining pokemon we will need to retrieve each page of results. Fortunately, we can use our existing code to do so, we just need to make some minor adjustments.

const container = document.querySelector('.pokedex');
const pageOneUrl = "https://pokeapi.co/api/v2/pokemon";

const getPokemonList = (url) => {
  axios.get(url)
       .then( (response) => {
          const data = response.data.results;
     
          data.map( (pokemon) => {
            const div = document.createElement('div');
            const text = document.createTextNode(pokemon.name);
             
            div.appendChild(text);
            div.className += 'pokeList';
            container.appendChild(div);
          });      
       })
       .catch( (error) => {
          throw error;
       });
}

getPokemonList(pageOneUrl);

I’ve moved our variable holding the api url outside of the function and renamed it pageOneUrl. I’ve added url as a parameter to our getPokemonList function so we can pass in other urls as needed. Now any url provided to this function will be used for the API call, making it possible to reuse this function for any data we may receive in the same format as our pageOneUrl returns.

Next we need to make sure that our parent container, the div with the class pokedex is empty before we add anything to it. We can do this with just three lines of code:

const container = document.querySelector('.pokedex');
const pageOneUrl = "https://pokeapi.co/api/v2/pokemon";

const getPokemonList = (url) => {
  while (container.firstChild) {
    container.removeChild(container.firstChild);
  }

  axios.get(url)
       .then( (response) => {
          const data = response.data.results;
     
          data.map( (pokemon) => {
            const div = document.createElement('div');
            const text = document.createTextNode(pokemon.name);
             
            div.appendChild(text);
            div.className += 'pokeList';
            container.appendChild(div);
          });      
       })
       .catch( (error) => {
          throw error;
       });
}

getPokemonList(pageOneUrl);

This while loop looks in our container and checks for a child element, if one is found it removes that child. This loop continues until no more children are found. Piece of cake!

Next, we need some way to tell the app to load the next page. For this we will create buttons and add them to either side of our header. Let’s update the header html from this:

<div class='header'>Pokeapi Pokedex</div>

To this:

<div class='header'>
  <div class='nav' id='prev'>&lsaquo; Prev</div>
  Pokeapi Pokedex
  <div class='nav' id='next'>Next &rsaquo;</div>
</div>

In our css we’ll change the header to:

.header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  
  margin: 10px;

  font-size: 36px;
  font-weight: bold;
  font-family: arial;
  text-align: center;
}

And we’ll add our new nav class to the css:

.nav {
  font-size: 18px;
  font-weight: bold;
  color: #e5a5a5;
  
  cursor: pointer;
}

Our app now looks like this:

A view of the pokedex app after adding nav buttons

All that is left is adding the onclick handlers to make the buttons work. The data returned in our API call has keys named "previous" and "next" which are populated with urls to the next page of results. We can add some onclick handlers to our new buttons with this information.

const getPokemonList = (url) => {
  while (container.firstChild) {
    container.removeChild(container.firstChild);
  }
  
  axios.get(url)
       .then( (response) => {
          const data = response.data.results;
          const prev = response.data.previous;
          const next = response.data.next;
    
          data.map( (pokemon) => {
            const div = document.createElement('div');
            const text = document.createTextNode(pokemon.name);
            
            div.appendChild(text);
            div.className += 'pokeList';
            container.appendChild(div);
          });
    
          if (prev) {
            document.getElementById('prev').onclick = () => {
              getPokemonList(prev);
            }
          }
    
          if (next) {
            document.getElementById('next').onclick = () => {
              getPokemonList(next);
            }
          }
       })
       .catch( (error) => {
          throw error;
       });
}

Now, if our API call returns a url for "previous" or "next" we will add an event listener to our new buttons which when clicked will call our getPokemonList function with the url to the appropriate page of results.

Creating Cards For Selected Pokemon

Looking at lists of pokemon names is fun. Super fun. With 20 results per page, and 811 total results, we can look at 41 pages of pokemon names. For some, this may be enough but I think we can take this a step further and display information about each pokemon on demand.

To accomplish this, we need to have some way of selecting which pokemon we want to look at. Each list item already looks like a button, let’s make it behave like one as well.

data.map( (pokemon) => {
  const div = document.createElement('div');
  const text = document.createTextNode(pokemon.name);
            
  div.appendChild(text);
  div.className += 'pokeList';
  div.onclick = () => { getPokemonDetails(pokemon.url); };
  container.appendChild(div);
});

In our Array.map() method we’ve added an onclick event handler to each list item. This event handler will take the url we received in each object and pass it to a new function which looks like this:

const getPokemonDetails = (url) => {
  while (container.firstChild) {
    container.removeChild(container.firstChild);
  }
  
  axios.get(url)
       .then( (response) => {
          
       })
       .catch( (error) => {
          throw error;
       });
}

Look familiar? The only thing that’s changed is the name! If we were following the D.R.Y. (Don’t Repeat Yourself) principle we’d just modify our existing function to handle our new data format we’ll receive, but for the sake of simplicity we will just create a new function to handle the api calls for pokemon details. They call this W.E.T. instead of DRY (We Enjoy Typing!)

If you want you can log the result of this call to your console, I will again go to Postman to see what we can expect.

A view of the results of our API call in Postman

Aaaaaaand it’s huge. Really huge. 9018 lines, in Postman. If you are more industrious than I am, you can go through all the information we have available here, but I am going to choose some data and we’ll ignore the rest.

Since we’re only dealing with one data set instead of an array of data, we don’t need to map through anything so this will be pretty simple. Into our call response we will add:

.then( (response) => {
  const data = response.data;
    
  const nameDiv = document.createElement('div');
          
  nameDiv.appendChild(
    document.createTextNode(data.species.name)
  );
  nameDiv.className = 'pokeName';
  container.appendChild(nameDiv);
    
  const img = document.createElement('img');
    
  img.src = data.sprites.front_default;
  img.className = 'pokePic';
  container.appendChild(img);
    
  const statsDiv = document.createElement('div');
    
  statsDiv.className = 'stats';
  statsDiv.appendChild(
    document.createTextNode("Height: " + data.height + " || Weight: " + data.weight)
  );
  container.appendChild(statsDiv);
})

This may seem a little complicated, but basically we’re just creating new html elements, assigning them some attributes like class or src and then adding them into our app.

Since we’re adding some classes here let’s go ahead and define them in our css by adding:

.pokeName {
  margin: 20px;
  
  font-size: 28px;
  font-weight: bold;
  text-align: center;
}

.pokePic {
  width: 250px;
}

.stats {
  font-size: 18px;
  font-weight: 600;
}

Now, when we click a name in our list we should see:

A view of the pokemon card

Not bad! As I said earlier, there is a lot of information returned for each pokemon so if you wanted to improve on the card you could add all kinds of cool information. For the purposes of this tutorial, I just want to add one more thing and make one small change.

While viewing a card in the pokedex it would be nice to be able to return to the list, so let’s add a button at the bottom of the card allowing us to do just that.

To make this work, we need to know where we’ve been, so we will add a second parameter to the getPokemonDetails function, which will allow us to pass the url of the list we were just viewing. Then we can set an onclick event handler which will reload the previous list. Our getPokemonDetails function should now look like this:

const getPokemonDetails = (url, listUrl) => {
  while (container.firstChild) {
    container.removeChild(container.firstChild);
  }
  
  axios.get(url)
       .then( (response) => {
          const data = response.data;
    
          const nameDiv = document.createElement('div');
          
          nameDiv.appendChild(
            document.createTextNode(data.species.name)
          );
          nameDiv.className = 'pokeName';
          container.appendChild(nameDiv);
    
          const img = document.createElement('img');
    
          img.src = data.sprites.front_default;
          img.className = 'pokePic';
          container.appendChild(img);
    
          const statsDiv = document.createElement('div');
    
          statsDiv.className = 'stats';
          statsDiv.appendChild(
            document.createTextNode("Height: " + data.height + " || Weight: " + data.weight)
          );
          container.appendChild(statsDiv);
    
          const button = document.createElement('div');
    
          button.className = 'back';
          button.appendChild(
            document.createTextNode("Back")
          );
          button.onclick = () => { getPokemonList(listUrl) };
          container.appendChild(button);
       })
       .catch( (error) => {
          throw error;
       });
}

Since this function is now expecting us to pass two parameters when it’s called, we also need to update the onclick event in our getPokemonList function so it sends everything we need.

div.onclick = () => { getPokemonDetails(pokemon.url, url); };

Now we can return to our list and browse other pokemon! Our finished card looks like this:

A view of the finalized card

The last thing I’d like to do is just clean up one little bit of UI/UX that is bugging me. Currently while viewing a card if we click the Prev or Next buttons it will load the previous or next list. What I’d like to do is just disable those buttons.

In our getPokemonDetails function let’s add something to do just that:

const getPokemonDetails = (url, listUrl) => {
  while (container.firstChild) {
    container.removeChild(container.firstChild);
  }
  
  document.getElementById('prev').onclick = () => {
    return false;
  }
  
  document.getElementById('next').onclick = () => {
    return false;
  }
  
  axios.get(url)

And there you have it. We’ve successfully made a whole lot of API calls and used the data to build an app, and in the process learned about promises and async tasks.

You can view the finished project here.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s