Trabajando con Poke Api para hacer una Pokedex con Angular Material

Poke api es un servicio abierto al publico el cual aprovechamos para hacer el modelo inicial de una pokedex con diferentes componentes de angular material como mat-table, mat-paginator, mat-filter, mat-snackbar, mat-cards, mat-chips.

Overview

resultado
Resultado del proyecto

Si quieres mas información de pokeapi, en el siguiente link.

Requerimientos

  • Conocimientos básicos de Angular (crear un proyecto, crear componentes)
  • Conocimientos muy básicos de Angular Material
  • Creación del proyecto en Angular con hoja de estilos SCSS
  • Proyecto con dependencia de Angular material añadida

Creación inicial de componentes

ng g c components/header
ng g c components/footer
ng g c components/poke-detail
ng g c components/poke-table
  • poke-detail : componente de detalle del pokemon seleccionado
  • poke-table: componente de tabla del pokemon

Creación inicial del servicio

ng g s services/pokemon

Servicio en el cual se consume el api de pokeapi

Creación de archivo compartido styles

En la carpeta de assets creamos dos carpetas y en styles creamos un archivo para almacenar los colores que vamos a utilizar en la aplicación:

  • assets/images
  • assets/styles/variables.scss
$red: #bd3736;
$black: black;
$white: #fcfcfc;
$yellow: #FFD924;

Deberíamos tener la siguiente estructura

Estructura base del proyecto

Modulo compartido componentes material

En shared creamos un modulo que va a ser compartido y carga los componentes necesarios, este modulo se debe colocar en los imports de app.module.ts

import { NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon';
import {MatGridListModule} from '@angular/material/grid-list';
import {MatTableModule} from '@angular/material/table';
import {MatPaginatorModule} from '@angular/material/paginator';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {MatChipsModule} from '@angular/material/chips';

@NgModule({
  imports: [
    MatButtonModule,
    MatMenuModule,
    MatToolbarModule,
    MatIconModule,
    MatCardModule,
    MatGridListModule,
    MatTableModule,
    MatPaginatorModule,
    MatFormFieldModule,
    MatInputModule, 
    MatChipsModule
  ],
  exports: [
    MatButtonModule,
    MatMenuModule,
    MatToolbarModule,
    MatIconModule,
    MatCardModule,
    MatGridListModule,
    MatTableModule,
    MatPaginatorModule,
    MatFormFieldModule,
    MatInputModule,
    MatChipsModule
  ]
})
export class MaterialModule {}

app.module.ts

Importamos HttpClientModule para poder realizar peticiones HTTP

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from 'src/shared/material.module';
import { HeaderComponent } from './components/header/header.component';
import { PokeTableComponent } from './components/poke-table/poke-table.component';
import { PokeDetailComponent } from './components/poke-detail/poke-detail.component';
import { HttpClientModule } from '@angular/common/http';
import { FooterComponent } from './components/footer/footer.component';

@NgModule({
  declarations: [
    AppComponent,
    HeaderComponent,
    PokeTableComponent,
    PokeDetailComponent,
    FooterComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    HttpClientModule,
    MaterialModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

app.component.html

<app-header></app-header>

<router-outlet></router-outlet>

<app-footer></app-footer>

app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { PokeDetailComponent } from './components/poke-detail/poke-detail.component';
import { PokeTableComponent } from './components/poke-table/poke-table.component';


const routes: Routes = [
  {path: 'home', component: PokeTableComponent},
  {path: 'pokeDetail/:id', component: PokeDetailComponent},
  {path: '', pathMatch: 'full', redirectTo: 'home' },
  {path: '**', pathMatch: 'full', redirectTo: 'home'},
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

environment.ts

Creamos la url base de pokeapi en enviroment.ts

export const environment = {
  production: false,
  baseUrl: 'https://pokeapi.co/api/v2'
};

pokemon.service.ts

Si llamamos el servicio unicamente con /pokemon, trae los nombres y una url de la información del pokemon, nosotros utilizaremos /pokemon/${index} donde index va a ser el pokemon que deseemos, cabe resaltar que vamos a traer los 150 primer pokemon de la primera generación

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

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

  baseUrl: string = environment.baseUrl;

  constructor(private http: HttpClient) { }

  //Obtiene pokemon
  getPokemons(index){
    return this.http.get<any>(`${this.baseUrl}/pokemon/${index}`);
  }
  
}

Poke-table

En el componente poke-table vamos a listar los pokemons de la siguiente manera

import { Component, OnInit, ViewChild } from '@angular/core';
import { PokemonService } from 'src/app/services/pokemon.service';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';

@Component({
  selector: 'app-poke-table',
  templateUrl: './poke-table.component.html',
  styleUrls: ['./poke-table.component.scss']
})
export class PokeTableComponent implements OnInit {
  //Columnas que se muestran de la tabla de angular material
  displayedColumns: string[] = ['position', 'image', 'name'];
  data: any[] = [];
  dataSource = new MatTableDataSource<any>(this.data);

  @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;

  pokemons = [];

  constructor(private pokemonService: PokemonService, private router: Router) { }

  ngOnInit(): void {
    this.getPokemons();
  }

  getPokemons() {
    let pokemonData;
    
    for (let i = 1; i <= 150; i++) {
      this.pokemonService.getPokemons(i).subscribe(
        res => {
          pokemonData = {
            position: i,
            image: res.sprites.front_default,
            name: res.name
          };
          //ponemos la data que viene del servicio en un arreglo
          this.data.push(pokemonData);
          this.dataSource = new MatTableDataSource<any>(this.data);
          this.dataSource.paginator = this.paginator;
        },
        err => {
          console.log(err);
        }
      );
    }
  }

  //Filtro para el paginador
  applyFilter(event: Event) {
    const filterValue = (event.target as HTMLInputElement).value;
    this.dataSource.filter = filterValue.trim().toLowerCase();

    if (this.dataSource.paginator) {
      this.dataSource.paginator.firstPage();
    }
  }

 //Obtiene elemento seleccionado
  getRow(row){
    //console.log(row);
    this.router.navigateByUrl(`/pokeDetail/${row.position}`)
  }

}
<div class="container">

    <mat-form-field>
        <mat-label>Filter</mat-label>
        <input matInput (keyup)="applyFilter($event)" #input>
    </mat-form-field>

    <table mat-table [dataSource]="dataSource">
      
        <ng-container matColumnDef="position">
            <th mat-header-cell *matHeaderCellDef> No. </th>
            <td mat-cell *matCellDef="let element"> {{element.position}} </td>
        </ng-container>

        <ng-container matColumnDef="image">
            <th mat-header-cell *matHeaderCellDef> Image </th>
            <td mat-cell *matCellDef="let element">
                <img src="{{element.image}}" alt="pokemon" width="100" height="100">
            </td>
        </ng-container>

        <ng-container matColumnDef="name">
            <th mat-header-cell *matHeaderCellDef> Name </th>
            <td mat-cell *matCellDef="let element"> {{element.name}} </td>
        </ng-container>

        <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
        <tr mat-row *matRowDef="let row; columns: displayedColumns;" (click)="getRow(row)"></tr>

        <!-- Row shown when there is no matching data. -->
        <tr class="mat-row" *matNoDataRow>
            <td class="mat-cell" colspan="4">No data matching the filter "{{input.value}}"</td>
        </tr>

    </table>

    <mat-paginator [pageSizeOptions]="[5, 10, 20]" showFirstLastButtons></mat-paginator>
 </div>
@import "../../../assets/styles/variables.scss";

.container{
    width: 100%;
    margin-top: 20px;
    text-align: center;
}

table {
    min-width: 80%;
    margin: auto;
    text-align: center;
}

.mat-header-cell {
    color: black;
    font-weight: bold;
    text-align: center;
    font-size: 15px;
}

//even
//Representa elementos cuya posición numérica en una serie de hermanos es par: 2, 4, 6, etc.
.mat-row:nth-child(even) {
    background-color: $white;
}

//odd
//Representa elementos cuya posición numérica en una serie de hermanos es impar: 1, 3, 5, etc.
.mat-row:nth-child(odd) {
    background-color: $red;
    & .mat-cell {
        color: $white;
    }
}

.mat-cell:first-letter {
    text-transform: uppercase;
}

.mat-row:hover {
    cursor: pointer;
    box-shadow: inset 0 0 0 5px $yellow;
}

::ng-deep.mat-paginator {
    width: 80%;
    margin: auto;
}

.mat-form-field {
    font-size: 13px;
    width: 20%;

    margin-bottom: 5px;
    padding: 10px;
}

::ng-deep.mat-form-field-label {
    color: $white !important;
}
::ng-deep.mat-form-field-underline {
    background-color: $white !important;
}

input {
    color: $white !important;
}

Poke-detail

import { Component, OnInit } from '@angular/core';
import { PokemonService } from 'src/app/services/pokemon.service';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-poke-detail',
  templateUrl: './poke-detail.component.html',
  styleUrls: ['./poke-detail.component.scss']
})
export class PokeDetailComponent implements OnInit {

  pokemon: any = '';
  pokemonImg = '';
  pokemonType = [];

  constructor(private activatedRouter: ActivatedRoute,
    private pokemonService: PokemonService) {
    //obtiene parametro de la url
    this.activatedRouter.params.subscribe(
      params => {
        this.getPokemon(params['id']);
      }
    )
  }

  ngOnInit(): void {
  }

  getPokemon(id) {
    this.pokemonService.getPokemons(id).subscribe(
      res => {
        console.log(res);
        this.pokemon = res;
        this.pokemonImg = this.pokemon.sprites.front_default;
        this.pokemonType = res.types[0].type.name;
      },
      err => {
        console.log(err);
      }
    )
  }

}
<mat-card class="card">
  
  <mat-card-header>
    <img mat-card-avatar class="header-image" src="{{pokemonImg}}">
    <mat-card-title>{{pokemon.name}}</mat-card-title>
    <mat-card-subtitle>Nº {{pokemon.id}}</mat-card-subtitle>
  </mat-card-header>

  <img mat-card-image src="{{pokemonImg}}" alt="pokemon">

  <mat-card-content>
    <mat-chip-list>
      <mat-chip color="primary">Type: {{pokemonType}}</mat-chip>
      <mat-chip color="primary">Height: {{pokemon.height}}</mat-chip>
      <mat-chip color="primary">Weight: {{pokemon.weight}}</mat-chip>
    </mat-chip-list>
  </mat-card-content>

</mat-card>
@import "../../../assets/styles/variables.scss";

.card {
    width: 300px;
    margin: auto;
    margin-top: 20px;
    background-color: $red;
}

.header-image {
    background-size: cover;
}

.mat-card-header{
    background-color: $white;
    padding-top: 10px;
    border-radius: 10px;
}

.mat-card-title{
    text-transform: uppercase;
    color: $black;
}

.mat-card-subtitle{
    color: $black;
}

Header

<mat-toolbar color="primary">
    <mat-toolbar-row>
      <!-- Imagen que quieres en el header-->
       <img src="../../../assets/images/pokedex.png" alt="pokedex" width="40" height="40" (click)="home()">
    </mat-toolbar-row>
</mat-toolbar>
img:hover{
    cursor: pointer;
}

Footer

footer{
    display: flex;
    flex: 0;
    justify-content: center;
    align-items: center;
    height: 80px;
    color: white;
}

Resultado

pokedex-web
pokedex-filter-angular
pokemon-detail-angular

Gracias a que se utiliza Angular Material y los componentes al tener flex la aplicación actual responsive

Conclusión

Pokeapi tiene una estructura un poco complejo de entender, en este ejercicio se trabajo sin orientación a objetos, pero lo mas recomendado es trabajar con este paradigma para así evitar muchos inconvenientes en el trabajo con esta API, en cuanto a Angular material se observa que gracias a ::ng-deep es muy sencillo cambiar los estilos de cualquier componente que deseemos.

Repositorios

Da clic en el siguiente enlace o el icono que se encuentra abajo

Contacto

One Reply to “Trabajando con Poke Api para hacer una Pokedex con Angular Material”

Leave a Reply

Your email address will not be published.