Nessa seção vamos aprender como criar uma Pokedex usando Angular
Publicado em 04 de maio de 2026
Esse desafio foi bem diferente dos anteriores, porque aqui eu saí daquele mundo mais “fechado” do componente e comecei a trabalhar com uma comunicação mais real de aplicação.
A ideia era criar uma Pokédex usando a PokéAPI, listando pokémons, fazendo paginação, buscando detalhes, usando service, trabalhando com formulário reativo, debounceTime, switchMap e também rota com parâmetro.
No começo parecia simples:
buscar pokémons na API
mostrar na tela
clicar e ver detalhes
Mas quando comecei a fazer, percebi que tinha bastante coisa acontecendo por trás.
A primeira coisa foi criar o service, porque eu não queria deixar a chamada HTTP direto dentro do componente.
O service ficou responsável por conversar com a API.
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { PokemonDetail, PokemonResponse } from '../models/pokemon-model';
@Injectable({
providedIn: 'root',
})
export class PokemonsService {
private readonly apiUrl = 'https://pokeapi.co/api/v2/pokemon';
constructor(private http: HttpClient) {}
getPokemons(offset: number, limit: number) {
return this.http.get<PokemonResponse>(
`${this.apiUrl}?offset=${offset}&limit=${limit}`
);
}
getPokemonByName(name: string) {
return this.http.get<PokemonDetail>(`${this.apiUrl}/${name}`);
}
}
Aqui eu entendi melhor o papel do service.
O componente não precisa saber exatamente qual é a URL da API, nem montar tudo manualmente toda hora.
Ele só chama:
this.pokemonsService.getPokemons(this.offset, this.limit)
ou:
this.pokemonsService.getPokemonByName(name)
E o service cuida da parte da requisição.
Isso deixa o código mais organizado, porque a responsabilidade fica separada:
Componente -> cuida da tela e dos eventos
Service -> cuida da comunicação com a API
Também criei alguns models para tipar melhor os dados que vinham da API.
export interface PokemonResponse {
count: number;
next: string | null;
previous: string | null;
results: PokemonItem[];
}
export interface PokemonItem {
name: string;
url: string;
}
export interface PokemonDetail {
id: number;
name: string;
height: number;
weight: number;
sprites: {
front_default: string;
};
types: {
type: {
name: string;
};
}[];
abilities: {
ability: {
name: string;
};
}[];
}
Eu não precisei mapear o JSON inteiro da PokéAPI.
A API retorna muita coisa, mas eu só tipiei o que eu realmente queria usar na tela.
Isso foi importante para eu não me perder.
A primeira parte do desafio foi listar os pokémons.
No componente principal, eu criei um array para guardar a lista:
pokemons: PokemonItem[] = [];
E no ngOnInit, chamei a função que carrega os dados:
ngOnInit(): void {
this.loadPokemons();
}
A função ficou assim:
loadPokemons() {
this.loading = true;
this.error = '';
this.pokemonsService.getPokemons(this.offset, this.limit).subscribe({
next: (pokemon) => {
this.pokemons = pokemon.results;
this.loading = false;
},
error: () => {
this.error = 'Erro ao carregar os pokémons.';
this.loading = false;
},
});
}
Aqui eu entendi melhor o fluxo do subscribe.
Quando a API responde com sucesso, cai no next.
Quando dá erro, cai no error.
Então ficou mais ou menos assim:
1. Começa carregando
2. Limpa erro antigo
3. Chama a API
4. Se der certo, salva os pokémons
5. Se der erro, mostra mensagem
6. Para o loading
A variável loading serve para mostrar algo na tela enquanto os dados ainda estão chegando.
loading = false;
E a variável error guarda a mensagem de erro:
error = '';
No HTML, usei assim:
@if (loading) {
<p class="status-message">Carregando...</p>
}
@if (error) {
<p class="status-message error">{{ error }}</p>
}
Isso deixou a tela mais amigável, porque o usuário não fica sem saber o que está acontecendo.
Depois disso, eu passei a lista de pokémons para um componente filho.
No componente pai:
<app-pokemon-card [data]="pokemons"></app-pokemon-card>
No componente filho:
import { Component, Input } from '@angular/core';
import { RouterLink } from '@angular/router';
import { PokemonItem } from '../../../models/pokemon-model';
@Component({
selector: 'app-pokemon-card',
imports: [RouterLink],
templateUrl: './pokemon-card.html',
styleUrl: './pokemon-card.scss',
})
export class PokemonCard {
@Input() data: PokemonItem[] = [];
}
Aqui o @Input recebe os dados que vêm do pai.
@Input() data: PokemonItem[] = [];
A ideia mental que ficou foi:
PokemonContainer tem os dados
PokemonCard só recebe e exibe
No HTML do card, percorri a lista com @for.
<div>
@for (pokemon of data; track pokemon.name) {
<div>
<span>{{ pokemon.name }}</span>
<a [routerLink]="['/pokemon', pokemon.name]">
Ver detalhes
</a>
</div>
}
</div>
No começo eu tinha tentado usar a URL que vem da API como se fosse um link comum.
Mas depois entendi que essa URL não é uma página visual.
Ela é um endpoint de JSON.
Exemplo:
https://pokeapi.co/api/v2/pokemon/bulbasaur
Essa URL retorna os detalhes do pokémon em formato JSON.
Então o mais certo não era abrir esse link direto como se fosse uma página bonita.
O mais certo era usar uma rota da minha aplicação.
Por isso usei:
<a [routerLink]="['/pokemon', pokemon.name]">
Ver detalhes
</a>
Se o pokémon for pikachu, o Angular monta a rota:
/pokemon/pikachu
Se for charizard, monta:
/pokemon/charizard
Essa parte foi importante porque o desafio pedia routing com parâmetros.
A rota ficou assim:
import { Routes } from '@angular/router';
import { PokemonDetailComponent } from './components/pokemon-container/pokemon-detail/pokemon-detail';
import { PokemonContainer } from './components/pokemon-container/pokemon-container';
export const routes: Routes = [
{
path: '',
redirectTo: 'pokemon',
pathMatch: 'full',
},
{
path: 'pokemon',
component: PokemonContainer,
pathMatch: 'full',
},
{
path: 'pokemon/:name',
component: PokemonDetailComponent,
},
];
Essa parte aqui é a mais importante:
{
path: 'pokemon/:name',
component: PokemonDetailComponent,
}
O :name é um parâmetro dinâmico.
Então quando entro em:
/pokemon/venusaur
O Angular entende que:
name = venusaur
No componente de detalhes, eu peguei esse parâmetro usando ActivatedRoute.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { PokemonsService } from '../../../services/pokemons-service';
import { PokemonDetail } from '../../../models/pokemon-model';
@Component({
selector: 'app-pokemon-detail',
imports: [RouterLink],
templateUrl: './pokemon-detail.html',
styleUrl: './pokemon-detail.scss',
})
export class PokemonDetailComponent implements OnInit {
pokemon?: PokemonDetail;
loading = false;
error = '';
constructor(
private route: ActivatedRoute,
private pokemonsService: PokemonsService
) {}
ngOnInit(): void {
const name = this.route.snapshot.paramMap.();
(!name) {
. = ;
;
}
.(name);
}
() {
. = ;
. = ;
..(name).({
: {
. = pokemon;
. = ;
},
: {
. = ;
. = ;
},
});
}
}
A linha que mais me ajudou a entender a rota foi essa:
const name = this.route.snapshot.paramMap.get('name');
Basicamente ela diz:
Angular, pega o valor do parâmetro name que veio na URL.
Então se a URL for:
/pokemon/pikachu
O valor de name será:
pikachu
Depois disso eu chamo o service:
this.pokemonsService.getPokemonByName(name)
E carrego os detalhes desse pokémon.
O HTML da página de detalhes ficou assim:
<a routerLink="/pokemon">Voltar para lista</a>
@if (loading) {
<p>Carregando...</p>
}
@if (error) {
<p>{{ error }}</p>
}
@if (pokemon) {
<section class="pokemon-detail-page">
<h1>{{ pokemon.name }}</h1>
<img
[src]="pokemon.sprites.front_default"
[alt]="pokemon.name"
/>
<div class="info">
<p>ID: {{ pokemon.id }}</p>
<p>Altura: {{ pokemon.height }}</p>
<p>Peso: {{ pokemon.weight }}</p>
</div>
<h2>Tipos</h2>
<div class="tags">
@for (item of pokemon.types; track item.type.name) {
<span>{{ item.type.name }}</span>
}
</>
Habilidades
@for (item of pokemon.abilities; track item.ability.name) {
{{ item.ability.name }}
}
}
Aqui a tela de detalhe ficou separada da tela de lista.
Antes eu estava mostrando o detalhe na mesma tela.
Funcionava, mas não cumpria tão bem a ideia de rota com parâmetro.
Agora o fluxo ficou mais real:
1. Lista pokémons
2. Usuário clica em Ver detalhes
3. Angular navega para /pokemon/nome
4. Componente de detalhe lê o nome da URL
5. Service busca os dados na API
6. Tela mostra o detalhe
Também precisei garantir que o projeto tivesse o router-outlet.
No app.html:
<router-outlet></router-outlet>
E no componente principal:
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
templateUrl: './app.html',
styleUrl: './app.scss',
})
export class App {}
Sem o router-outlet, a URL até pode mudar, mas o Angular não tem onde renderizar a página nova.
Essa parte foi uma pegadinha importante.
Depois veio a paginação.
Essa parte eu achei meio confusa no começo, porque eu ainda não tinha muita prática com offset e limit.
Na PokéAPI, a paginação funciona assim:
limit = quantos itens eu quero buscar
offset = de onde eu quero começar
Exemplo:
Página 1
offset = 0
limit = 20
Isso busca os primeiros 20 pokémons.
Página 2
offset = 20
limit = 20
Isso pula os primeiros 20 e busca os próximos 20.
Página 3
offset = 40
limit = 20
Isso pula os primeiros 40 e busca os próximos 20.
No componente, criei:
offset = 0;
limit = 20;
E na hora de buscar a lista:
this.pokemonsService.getPokemons(this.offset, this.limit)
O botão de próxima página ficou assim:
nextPage() {
this.offset = this.offset + this.limit;
this.selectedPokemon = undefined;
this.loadPokemons();
}
Ou seja:
offset atual + limit
Se o offset está em 0 e o limit é 20:
0 + 20 = 20
Vai para a próxima página.
Se já está em 20:
20 + 20 = 40
Vai para a próxima.
O botão de voltar ficou assim:
previousPage() {
if (this.offset === 0) return;
this.offset = this.offset - this.limit;
this.selectedPokemon = undefined;
this.loadPokemons();
}
Aqui eu precisei colocar essa proteção:
if (this.offset === 0) return;
Porque se eu estiver na primeira página, não faz sentido voltar mais.
Sem essa proteção, poderia virar:
offset = -20
E isso não faz sentido para a API.
No HTML, os botões ficaram assim:
<div class="pagination">
<button
type="button"
(click)="previousPage()"
[disabled]="offset === 0"
>
Anterior
</button>
<span>Página {{ offset / limit + 1 }}</span>
<button
type="button"
(click)="nextPage()"
>
Próxima
</button>
</div>
Essa parte aqui calcula a página atual:
Página {{ offset / limit + 1 }}
Se o offset for 0:
0 / 20 + 1 = 1
Página 1.
Se o offset for 20:
20 / 20 + 1 = 2
Página 2.
Se o offset for 40:
40 / 20 + 1 = 3
Página 3.
Foi aqui que a paginação começou a fazer mais sentido para mim.
Depois veio a busca com debounceTime e switchMap.
Essa parte foi mais avançada, porque envolve formulário reativo e RxJS.
Como eu tinha três campos na tela, achei melhor organizar tudo com FormGroup e FormBuilder.
Os campos eram:
search -> busca normal
pokemonA -> primeiro pokémon da comparação
pokemonB -> segundo pokémon da comparação
O formulário ficou assim:
form: FormGroup;
constructor(
private pokemonsService: PokemonsService,
private fb: FormBuilder
) {
this.form = this.fb.nonNullable.group({
search: '',
pokemonA: '',
pokemonB: '',
});
}
Eu tentei criar o form fora do constructor usando this.fb, mas deu erro:
Property 'fb' is used before its initialization.
Isso aconteceu porque o Angular ainda não tinha terminado de inicializar o FormBuilder.
Então deixei o form declarado como propriedade:
form: FormGroup;
E inicializei dentro do constructor:
this.form = this.fb.nonNullable.group({
search: '',
pokemonA: '',
pokemonB: '',
});
No HTML, envolvi os campos com:
<form [formGroup]="form">
E cada input usa formControlName.
<input
type="text"
placeholder="Buscar pokémon"
formControlName="search"
/>
Para comparar:
<input
type="text"
placeholder="Pokémon 1"
formControlName="pokemonA"
/>
<input
type="text"
placeholder="Pokémon 2"
formControlName="pokemonB"
/>
A busca principal ficou ouvindo as mudanças do campo search.
listenSearch() {
this.form.get('search')?.valueChanges
.pipe(
debounceTime(500),
filter((value) => value.trim().length > 0),
distinctUntilChanged(),
switchMap((value) => {
this.loading = true;
this.error = '';
return this.pokemonsService
.getPokemonByName(this.normalizePokemonName(value))
.pipe(
catchError(() => {
this.error = 'Pokémon não encontrado.';
return of(undefined);
})
);
})
)
.subscribe((pokemon) => {
this.loading = false;
this.selectedPokemon = pokemon;
});
}
Essa parte foi bem importante:
debounceTime(500)
Ela faz o Angular esperar 500ms depois que eu paro de digitar.
Sem isso, cada tecla faria uma chamada para a API.
Exemplo ruim:
p -> chama API
pi -> chama API
pik -> chama API
pika -> chama API
pikachu -> chama API
Com debounceTime, fica melhor:
usuário digita
Angular espera um pouco
só depois chama a API
Depois tem:
filter((value) => value.trim().length > 0)
Isso evita buscar quando o campo está vazio.
Sem isso, poderia tentar buscar:
/pokemon/
E isso não seria uma busca válida.
Também usei:
distinctUntilChanged()
Ele evita fazer uma busca repetida se o valor não mudou.
E a parte mais importante foi o switchMap.
switchMap((value) => {
return this.pokemonsService.getPokemonByName(
this.normalizePokemonName(value)
);
})
A imagem mental que ficou para mim foi:
switchMap cancela a busca anterior e usa a busca mais recente.
Exemplo:
digitei: p
começou uma busca
digitei: pi
a busca anterior deixa de importar
digitei: pika
a anterior deixa de importar de novo
resultado final:
só quero o resultado mais recente
Isso é muito útil para busca em tempo real.
Também tratei erro com catchError.
catchError(() => {
this.error = 'Pokémon não encontrado.';
return of(undefined);
})
Eu precisei disso porque, se uma busca der erro dentro do fluxo do RxJS, pode quebrar a escuta do campo.
Com catchError, eu trato o erro e devolvo um valor seguro:
of(undefined)
Assim a busca continua funcionando depois de um erro.
Também criei uma função para normalizar o nome:
private normalizePokemonName(name: string) {
return name.trim().toLowerCase();
}
Ela transforma coisas assim:
" Pikachu " -> "pikachu"
"CHARIZARD" -> "charizard"
" venusaur " -> "venusaur"
Isso deixa a busca mais confiável.
Depois veio o bônus de comparar dois pokémons lado a lado.
Para isso, eu criei dois estados diferentes:
pokemonA?: PokemonDetail;
pokemonB?: PokemonDetail;
A ideia é simples:
pokemonA guarda o primeiro pokémon
pokemonB guarda o segundo pokémon
Se eu usasse só uma variável, um pokémon substituiria o outro.
Então para comparar lado a lado eu precisava de dois espaços separados.
A escuta do primeiro campo ficou assim:
listenPokemonA() {
this.form.get('pokemonA')?.valueChanges
.pipe(
debounceTime(500),
filter((value) => value.trim().length > 0),
distinctUntilChanged(),
switchMap((value) =>
this.pokemonsService
.getPokemonByName(this.normalizePokemonName(value))
.pipe(catchError(() => of(undefined)))
)
)
.subscribe((pokemon) => {
this.pokemonA = pokemon;
});
}
A do segundo ficou quase igual:
listenPokemonB() {
this.form.get('pokemonB')?.valueChanges
.pipe(
debounceTime(500),
filter((value) => value.trim().length > 0),
distinctUntilChanged(),
switchMap((value) =>
this.pokemonsService
.getPokemonByName(this.normalizePokemonName(value))
.pipe(catchError(() => of(undefined)))
)
)
.subscribe((pokemon) => {
this.pokemonB = pokemon;
});
}
Aqui eu percebi que tinha repetição de código.
Daria para melhorar depois criando uma função reutilizável, mas nesse momento eu preferi deixar mais explícito para entender.
No HTML, renderizei os dois dentro da mesma área:
<section class="compare-area">
<h2>Comparar pokémons</h2>
<div class="compare-inputs">
<input
type="text"
placeholder="Pokémon 1"
formControlName="pokemonA"
/>
<input
type="text"
placeholder="Pokémon 2"
formControlName="pokemonB"
/>
</div>
<div class="compare">
@if (pokemonA) {
<div class="pokemon-detail">
<h2>{{ pokemonA.name }}</h2>
<img
[src]="pokemonA.sprites.front_default"
[alt]="pokemonA.name"
/>
<p>ID: {{ pokemonA.id }}</p>
<p>Altura: {{ pokemonA.height }}</p>
<p>Peso: {{ pokemonA.weight }}
}
@if (pokemonB) {
{{ pokemonB.name }}
ID: {{ pokemonB.id }}
Altura: {{ pokemonB.height }}
Peso: {{ pokemonB.weight }}
}
E no CSS, para ficar lado a lado:
.compare {
display: flex;
gap: 24px;
align-items: flex-start;
flex-wrap: wrap;
}
O display: flex coloca os cards lado a lado.
O gap dá espaço entre eles.
O flex-wrap: wrap ajuda no responsivo, porque se a tela for pequena, um card desce para baixo do outro.
Também criei um estilo melhor para a tela e para os cards.
:host {
display: block;
min-height: 100vh;
padding: 32px;
background: linear-gradient(135deg, #eef2ff, #f8fafc);
color: #1f2937;
font-family: Arial, Helvetica, sans-serif;
}
h1 {
margin: 0 0 24px;
font-size: 40px;
text-align: center;
color: #dc2626;
}
h2 {
margin: 0 0 16px;
font-size: 24px;
color: #111827;
}
section {
margin-bottom: 32px;
}
input {
width: 100%;
max-width: 320px;
padding: 12px 14px;
border: 1px solid #cbd5e1;
border-radius: 12px;
font-size: 16px;
outline: none;
background: #ffffff;
: border-color , box-shadow ;
}
{
: ;
: (, , , );
}
,
{
: inline-block;
: ;
: none;
: ;
: ;
: ;
: ;
: ;
-decoration: none;
: pointer;
: transform , background , opacity ;
}
(:disabled),
{
: ;
: (-);
}
{
: ;
: not-allowed;
: ;
}
hr {
: ;
: none;
: solid ;
}
{
: flex;
: center;
: center;
: ;
: ;
}
{
: ;
: ;
}
{
: ;
: auto;
: ;
: ;
: ;
-align: center;
: (, , , );
}
{
: ;
: ;
}
,
{
: ;
: auto ;
: ;
: ;
: ;
: (, , , );
}
{
: ;
: ;
: solid ;
: ;
: ;
-align: center;
: (, , , );
}
{
: ;
-: capitalize;
: ;
}
{
: ;
: ;
-fit: contain;
: ;
}
{
: ;
: ;
: ;
: ;
}
{
: flex;
: center;
: ;
}
{
: flex;
: ;
: wrap;
: ;
}
{
: flex;
: ;
: flex-start;
: wrap;
}
(: ) {
{
: ;
}
{
: ;
}
,
{
: column;
}
{
: ;
}
}
Também estilizei o componente de card da lista.
:host {
display: block;
max-width: 980px;
margin: 0 auto;
}
div {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
}
div > div {
display: flex;
flex-direction: column;
gap: 12px;
padding: 18px;
border-radius: 18px;
background: #ffffff;
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.08);
transition: transform 0.15s, box-shadow 0.15s;
}
div > div:hover {
transform: translateY(-3px);
box-shadow: 0 18px 36px rgba(15, 23, 42, 0.12);
}
{
: ;
: ;
-: capitalize;
: ;
}
{
: ;
: ;
: ;
: ;
: ;
-align: center;
-decoration: none;
: background , transform ;
}
{
: ;
: (-);
}
No final, meu componente principal ficou com bastante coisa.
import { Component, OnInit } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, FormGroup } from '@angular/forms';
import {
debounceTime,
distinctUntilChanged,
switchMap,
filter,
catchError,
of,
} from 'rxjs';
import { PokemonCard } from './pokemon-card/pokemon-card';
import { PokemonsService } from '../../services/pokemons-service';
import { PokemonDetail, PokemonItem } from '../../models/pokemon-model';
@Component({
selector: 'app-pokemon-container',
imports: [PokemonCard, ReactiveFormsModule],
templateUrl: './pokemon-container.html',
styleUrl: './pokemon-container.scss',
})
export class PokemonContainer implements OnInit {
pokemons: PokemonItem[] = [];
selectedPokemon?: PokemonDetail;
pokemonA?: PokemonDetail;
?: ;
loading = ;
error = ;
offset = ;
limit = ;
: ;
() {
. = ...({
: ,
: ,
: ,
});
}
(): {
.();
.();
.();
.();
}
() {
. = ;
. = ;
..(., .).({
: {
. = pokemon.;
. = ;
},
: {
. = ;
. = ;
},
});
}
() {
..()?.
.(
(),
( value.(). > ),
(),
( {
. = ;
. = ;
.
.(.(value))
.(
( {
. = ;
();
})
);
})
)
.( {
. = ;
. = pokemon;
});
}
() {
..()?.
.(
(),
( value.(). > ),
(),
(
.
.(.(value))
.(( ()))
)
)
.( {
. = pokemon;
});
}
() {
..()?.
.(
(),
( value.(). > ),
(),
(
.
.(.(value))
.(( ()))
)
)
.( {
. = pokemon;
});
}
() {
. = . + .;
. = ;
.();
}
() {
(. === ) ;
. = . - .;
. = ;
.();
}
() {
name.().();
}
}
O HTML principal ficou assim:
<h1>Pokédex</h1>
<form [formGroup]="form">
<section class="search-area">
<h2>Buscar pokémon</h2>
<input
type="text"
placeholder="Buscar pokémon"
formControlName="search"
/>
</section>
@if (loading) {
<p class="status-message">Carregando...</p>
}
@if (error) {
<p class="status-message error">{{ error }}</p>
}
<div class="pagination">
<button
type="button"
(click)="previousPage()"
[disabled]="offset === 0"
>
Anterior
</button>
<span>Página {{ offset / limit + 1 }}</span>
<button
type=
()=
>
Próxima
@if (selectedPokemon) {
{{ selectedPokemon.name }}
ID: {{ selectedPokemon.id }}
Altura: {{ selectedPokemon.height }}
Peso: {{ selectedPokemon.weight }}
}
Comparar pokémons
@if (pokemonA) {
{{ pokemonA.name }}
ID: {{ pokemonA.id }}
Altura: {{ pokemonA.height }}
Peso: {{ pokemonA.weight }}
}
@if (pokemonB) {
{{ pokemonB.name }}
ID: {{ pokemonB.id }}
Altura: {{ pokemonB.height }}
Peso: {{ pokemonB.weight }}
}
No fim, esse desafio juntou muita coisa:
HttpClient
Service
models
Input
RouterLink
ActivatedRoute
rotas com parâmetro
loading
error state
paginação
FormGroup
FormBuilder
valueChanges
debounceTime
switchMap
catchError
comparação lado a lado
CSS responsivo
Foi bastante coisa nova ao mesmo tempo.
Então eu não acho que esse seja um desafio para decorar tudo de primeira.
A imagem mental que ficou para mim foi essa:
A tela principal lista os pokémons.
O service conversa com a API.
A paginação muda o offset e busca uma nova lista.
A busca escuta o input em tempo real com valueChanges.
O debounceTime evita chamar a API a cada tecla.
O switchMap mantém só a busca mais recente.
A rota /pokemon/:name abre uma página separada de detalhes.
A comparação usa dois estados diferentes para renderizar dois pokémons lado a lado.
O mais importante foi entender que uma aplicação real não é só HTML e CSS.
Ela tem fluxo.
Usuário faz uma ação
Componente reage
Service busca dados
Estado muda
Tela atualiza
Rota muda quando precisa
Esse desafio foi difícil, mas foi bom porque parece muito mais próximo de uma tela real de produto.
Principalmente por ter lista, detalhe, busca, paginação e comparação.
No final, eu não só listei pokémons.
Eu consegui montar um fluxo mais completo de aplicação Angular.
Carregando comentários...