Pokedex using Angular and Bootstrap with PokeAPI

Zaki Mohammed Zaki Mohammed
February 28, 2021 | 9 min read | 444 Views

Squirtle, without a doubt, is my favorite Pokemon and I am open for a debate. You can check the Pokedex on your own or create a Pokedex on your own. Let us talk about the second option in this article, and with the help of PokeAPI, Angular and Bootstrap such crazy demands can be achieved.

You don’t have to create a Pokemon dictionary on your own just use it from PokeAPI, an amazing open source API just for crazy Pokemon lovers. The PokeAPI has everything; Pokemons, their attacks, types, evolutions, spices and everything on a simple HTTP’s GET. Fun fact it was originally a weekend project of Paul Hallett the guy behind this amazing tool, so keep a watch on your weekend projects.

The PokeAPI usage is straight forward and comes with nicely written documentation. For an instance one can simply hit following endpoint to get the list of Pokemons:

https://pokeapi.co/api/v2/pokemon

This will simply return the first 20 list of Pokemon names and their individual url. PokeAPI by default supports pagination, by simply passing offset and limit you can ask for next 20 Pokemons:

https://pokeapi.co/api/v2/pokemon?offset=20&limit=20

FYI, in this article we will going to make use of following endpoints:

https://pokeapi.co/api/v2/pokemon
https://pokeapi.co/api/v2/pokemon/{id or name}
https://pokeapi.co/api/v2/pokemon/evolution-chain/{id}
https://pokeapi.co/api/v2/pokemon/pokemon-species/{id or name}

Coming back to this article, we will take a challenge to create a stunning Pokedex web app with the simplicity of Bootstrap and the power of Angular. Let us define some targets to achieve to have a Pokedex in our pocket, the Pokedex will consist of following pages:

  1. List page - to list down the pokemons and their types
  2. View page - to show a detailed view for each Pokemon

Quite simple, so let us begin with setting up our Angular-Bootstrap project and checkout PokeAPI in action. The final Pokedex UI will look like below:

Pokedex App UI | CodeOmelet

Initializing Angular and Bootstrap 5

Let us start with creating a new Angular application and install the Bootstrap 5, a nicely written article about setting up Angular and Bootstrap 5 can be found here Exploring Bootstrap 5 with Angular. Will execute the following command to create an Angular app and install Bootstrap 5.

ng new pagination-app
npm i bootstrap@next bootstrap-icons

Adding Bootstrap 5 dependencies to angular.json file:

{
    ...
    "projects": {
        "pagination-app": {
            ...
            "architect": {
                "build": {
                    "options": {
						"assets": [
                            {
                               "glob": "*.svg",
                        	   "input": "./node_modules/bootstrap-icons/icons/",
                        	   "output": "/assets/icons/"
                            },
                            "src/favicon.ico",
                            "src/assets"
                        ],
                        "styles": [
                            "./node_modules/bootstrap/dist/css/bootstrap.min.css",
                            "src/styles.scss"
                        ],
                        "scripts": [
                            "./node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
                        ]
                    }
                },
                ...
            }
        }
    }
}

Setting up Components and Services

The default setup of any Angular application remains the same. We will add 2 routed components named: list and view and the app component will hold only the router outlet.

ng g c components/list
ng g c components/view

Later needs to add them to the routes as follows:

const routes: Routes = [
  { path: '', component: ListComponent },
  { path: 'view/:name', component: ViewComponent },
  { path: '**', component: ListComponent }
];

We will require a service to make calls to PokeAPI:

ng g s services/pokemon

Our project structure will be looking like below:

app
    |-- components
        |-- list
            |-- list.component.html
            |-- list.component.scss
            |-- list.component.spec.ts
            |-- list.component.ts
        |-- view
            |-- view.component.html
            |-- view.component.scss
            |-- view.component.spec.ts
            |-- view.component.ts
    |-- services
        |-- pokemon.service.spec.ts
        |-- pokemon.service.ts
    |-- app-routing.module.ts
    |-- app.component.html
    |-- app.component.scss
    |-- app.component.spec.ts
    |-- app.component.ts
    |-- app.module.ts

Pokemon Service

Let us have a closer look at the pokemon.service file.

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';

@Injectable({
  providedIn: 'root'
})
export class PokemonService {

  private url: string = environment.apiUrl + 'pokemon/';
  private _pokemons: any[] = [];
  private _next: string = '';

  constructor(private http: HttpClient) {
  }

  get pokemons(): any[] {
    return this._pokemons;
  }
  
  get next(): string {
    return this._next;
  }

  set next(next: string) {
    this._next = next;
  }

  getType(pokemon: any): string {
    return pokemon && pokemon.types.length > 0 ? pokemon.types[0].type.name : '';
  }

  get(name: string): Observable {
    const url = `${this.url}${name}`;
    return this.http.get(url);
  }

  getNext(): Observable {
    const url = this.next === '' ? `${this.url}?limit=100` : this.next;
    return this.http.get(url);
  }

  getEvolution(id: number): Observable {
    const url = `${environment.apiUrl}evolution-chain/${id}`;
    return this.http.get(url);
  }

  getSpecies(name: string): Observable {
    const url = `${environment.apiUrl}pokemon-species/${name}`;
    return this.http.get(url);
  }
}

We have created pokemons array which will hold up the loaded data so to reduce the API call and the next variable will hold up the next URL to fetch data from. We have created a setter and getter for these variables. The getType() method will simply return the type of pokemon passed to it. After that we have all the API endpoint get call methods, including get single and next pokemon, their species and evolution.

List Component

On the list component we are showing the list of Pokemons using the Bootstrap cards component. So we are making the use of service methods that are getNext and get to fetch all pokemon and their details and pushing pokemons to array. For showing Pokemon images we are making use of Pokeres where we simply need to supply Pokemon id at the end of the URL. We will decompose our HTML and TS of list component to focus on important logics:

list.component.html

<div *ngFor="let pokemon of pokemons; let i = index" class="col-md-6">
	<div class="card mb-3 bg-{{getType(pokemon)}}" [routerLink]="['/view', pokemon.name]">
		<div class="card-body">
			<div class="row">
				<div class="col-7 col-sm-8">
					<h3 class="card-title">{{pokemon.name | titlecase}}</h3>
					<span *ngFor="let type of pokemon.types"
						class="badge border rounded-pill me-1 bg-{{getType(pokemon)}}">
						{{type.type.name | titlecase}}
					</span>
				</div>
				<div class="col-5 col-sm-4">
					<img src="https://pokeres.bastionbot.org/images/pokemon/{{pokemon.id}}.png"
						class="img-fluid" alt="...">
				</div>
			</div>
		</div>
	</div>
</div>

Life is simple, we are using ngFor to iterate an array of pokemons and doing the usual stuff to create the elements. We have created different CSS classes to define background color for each type of Pokemons, and for that we are making use of the getType method of pokemon service.

list.component.html

loadMore(): void {
  this.loading = true;
  this.subscription = this.pokemonService.getNext().subscribe(response => {
    this.pokemonService.next = response.next;
    const details = response.results.map((i: any) => this.pokemonService.get(i.name));
    this.subscription = concat(...details).subscribe((response: any) => {
      this.pokemonService.pokemons.push(response);
    });
  }, error => console.log('Error Occurred:', error), () => this.loading = false);
}

Narrowing down our focus to an important method which does everything, is the loadMore method. It makes a call to the getNext method of pokemon service, once response loaded it arranges the results to make another call to get each of the pokemon details using concat method of RxJS in order to maintain the sequence of the call. Finally we add each of the pokemon to pokemon service’s pokemons array.

View Component

The view consists of major 2 sections, the banner and the details area. The banner shows the name of the Pokemon with their cute image and basic details like type, height, weight and base experience. The details area has further sub sections like evolution, stats, abilities and moves.

view.component.html > banner

<div class="row">
    <div class="col-sm-6">
        <div class="mt-2">
            <h1>{{pokemon.name | titlecase}}</h1>
            <span *ngFor="let type of pokemon.types"
                class="badge border rounded-pill me-1 bg-{{getType(pokemon)}}">
                {{type.type.name | titlecase}}
            </span>
        </div>

        <ul class="list-group list-group-flush mt-4" style="width: 20rem;">
            <li class="list-group-item bg-transparent">Experience <b
                class="float-end">{{pokemon.base_experience}}</b></li>
            <li class="list-group-item bg-transparent">Height <b
                class="float-end">{{pokemon.height}}</b></li>
            <li class="list-group-item bg-transparent">Weight <b
                class="float-end">{{pokemon.weight}}</b></li>
        </ul>
    </div>
    <div class="col-sm-6">
        <img src="https://pokeres.bastionbot.org/images/pokemon/{{pokemon.id}}.png" alt=""
            class="img-fluid d-block mx-auto" style="width: 300px;">
    </div>
</div>

In the banner we are showing Pokemon picture and name, types and a table for experience, height and weight.

Neat, let us checkout the other sections:

view.component.html > details > evolution

<!-- evolution -->
<div class="row">
    <div *ngFor="let evolution of pokemon.evolutions" class="col-sm mb-3">
        <div class="card" [routerLink]="['/view', evolution.name]">
            <div class="card-body">
                <h4 class="card-title text-center mb-3">{{evolution.name | titlecase}}</h4>
                <img src="https://pokeres.bastionbot.org/images/pokemon/{{evolution.id}}.png" alt=""
                class="img-fluid d-block mx-auto" style="width: 150px;">
            </div>
        </div>
    </div>
</div>

In the details section we first have evolution where we are showing Pokemon’s evolution, this evolution data is not available from Pokemon endpoint instead we get it from the evolution endpoint.

view.component.html > details > stats, abilities, moves

<!-- stats -->
<div class="card mb-3">
   <div class="card-body">
      <h4 class="card-title">Stats</h4>
      <div *ngFor="let stat of pokemon.stats" class="row mt-3">
         <div class="col-sm-2">
            <h6 class="text-sm-end mt-1">{{stat.stat.name | titlecase}}</h6>
         </div>
         <div class="col-sm-10">
            <div class="progress" style="height: 30px">
               <div class="progress-bar progress-bar-striped bg-{{getType(pokemon)}}"
                  role="progressbar" style="height: 30px;" 
                  [style.width]="stat.base_stat + '%'"
                  aria-valuemin="0" aria-valuemax="100">
                  <b>{{stat.base_stat}}</b>
               </div>
            </div>
         </div>
      </div>
   </div>
</div>

<!-- abilities -->
<div class="card mb-3">
   <div class="card-body">
      <h4 class="card-title">Abilities</h4>
   </div>
   <ul class="list-group list-group-flush">
      <li *ngFor="let ability of pokemon.abilities" class="list-group-item">
         {{ability.ability.name | titlecase}}
      </li>
   </ul>
</div>

<!-- moves -->
<div class="card mb-3">
   <div class="card-body">
      <h4 class="card-title">Moves</h4>
   </div>
   <ul class="list-group list-group-flush">
      <li *ngFor="let move of pokemon.moves" class="list-group-item">
         {{move.move.name | titlecase}}
      </li>
   </ul>
</div>

Simply showing the stats, abilities and moves in the Bootstrap card list which we obtained from Pokemon details endpoint.

view.component.ts > ngOnInit()

ngOnInit(): void {
    this.subscription = this.route.params.subscribe(params => {

        if (this.pokemonService.pokemons.length) {
            this.pokemon = this.pokemonService.pokemons.find(i => i.name === params.name);
            if (this.pokemon) {
                this.getEvolution();
		  return;
            }
        }

        this.subscription = this.pokemonService.get(params.name).subscribe(response => {
            this.pokemon = response;
            this.getEvolution();
        }, error => console.log('Error Occurred:', error));
    });
}

On init if the pokemons array has the Pokemon we are looking for will simply get the evolution details for that Pokemon, otherwise we will call a single get service and then followed by a call to evolution method.

view.component.ts > getEvolution()

getEvolution() {
    if (!this.pokemon.evolutions || !this.pokemon.evolutions.length) {
        this.pokemon.evolutions = [];
        this.subscription = this.pokemonService.getSpecies(this.pokemon.name)
            .subscribe(response => {
                const id = this.getId(response.evolution_chain.url);
                this.subscription = this.pokemonService.getEvolution(id)
                    .subscribe(response => this.getEvolves(response.chain));
            });
    }
}

If the Pokemon don't have evolutions we will make a call to get evolution detail. But the evolution API needs the id of the base Pokemon, for example in order to get the evolution chain of Charmeleon we need to provide the evolution endpoint id of Charmander. Instead, we can directly get the evolution chain URL from species endpoint, so will make a call to species endpoint later will call the evolution endpoint. Let us check out these methods.

view.component.ts > getEvolves() and getId()

getEvolves(chain: any) {
    this.pokemon.evolutions.push({
        id: this.getId(chain.species.url),
        name: chain.species.name
    });

    if (chain.evolves_to.length) {
        this.getEvolves(chain.evolves_to[0]);
    }
}

// e.g. url: https://pokeapi.co/api/v2/evolution-chain/1/
getId(url: string): number {
    const splitUrl = url.split('/')
    return +splitUrl[splitUrl.length - 2];
}

For getting Pokemon id from the evolution chain URL we have created the getId method. The chain property of evolution response has a nested object which will have the name and ids of Pokemon, to handle that we have created a recursive function getEvolves which will return the evolution array of objects which holds id and name of each evolution details.

The project repository link and the live link is provided below.


Zaki Mohammed
Zaki Mohammed
Learner, developer, coder and an exceptional omelet lover. Knows how to flip arrays or omelet or arrays of omelet.