Étape 13 — Filtre par type et tri alphabétique
Objectifs
- Filtrer les Pokémon par type avec un
<v-select> - Trier les Pokémon par nom (A-Z / Z-A) avec
Array.sort()etlocaleCompare() - Chaîner plusieurs
computedpour combiner recherche, filtre et tri
Résultat attendu

Contexte
On a déjà la recherche par nom (étape 12). On veut maintenant pouvoir filtrer par type (Feu, Eau, Plante...) et trier par ordre alphabétique. Ces trois transformations doivent fonctionner ensemble.
Encart oral — Chaîner les computed
Chaque computed prend le résultat du précédent comme source de données. On construit un pipeline de transformation :
pokemonStore.pokemons (données brutes)
↓
filteredByType (filtre par type)
↓
filteredBySearch (filtre par nom)
↓
sortedPokemons (tri alphabétique)Avantages :
- Chaque étape a une responsabilité unique (un filtre ou un tri)
- On peut ajouter ou retirer une étape sans toucher aux autres
- Vue.js recalcule uniquement les étapes impactées par un changement
Tâches
1. Ajouter le filtre par type
Dans le <script setup>, ajoutez une variable réactive pour le type sélectionné :
const selectedType = ref(null)Dans le <template>, ajoutez un <v-select> dans la <v-row> de recherche, à côté du champ de texte :
| Composant | Rôle | Documentation |
|---|---|---|
<v-select> | Menu déroulant avec liste de choix | Selects |
<v-col cols="12" sm="6" md="4">
<v-select
v-model="selectedType"
:items="pokemonStore.types"
item-title="name"
item-value="id"
label="Filtrer par type"
prepend-inner-icon="mdi-filter"
clearable
hide-details
variant="outlined"
density="compact"
/>
</v-col>:items="pokemonStore.types": les types chargés depuis l'API (Feu, Eau, Plante...)item-title="name": propriété affichée dans le menu (le nom du type)item-value="id": propriété utilisée comme valeur (l'ID du type)clearable: permet de désélectionner le filtre
2. Créer le computed de filtre par type
Ajoutez un computed avant filteredBySearch (l'ordre est important pour le chaînage) :
const filteredByType = computed(() => {
if (!selectedType.value) return pokemonStore.pokemons
return pokemonStore.pokemons.filter(pokemon =>
pokemon.types.includes(selectedType.value),
)
})- Si aucun type n'est sélectionné (
null) → on retourne tous les Pokémon pokemon.typesest un tableau d'IDs (ex:[1, 3]) — on vérifie si le type sélectionné est dedans
3. Chaîner la recherche sur le filtre par type
Modifiez filteredBySearch pour qu'il utilise filteredByType comme source (au lieu de pokemonStore.pokemons) :
const filteredBySearch = computed(() => {
if (!searchQuery.value) return filteredByType.value
const query = searchQuery.value.toLowerCase()
return filteredByType.value.filter(pokemon =>
pokemon.name.toLowerCase().includes(query),
)
})Le chaînage est maintenant en place : pokemons → filteredByType → filteredBySearch.
4. Ajouter le tri alphabétique
Dans le <script setup>, ajoutez une variable réactive pour l'ordre de tri :
const sortOrder = ref('asc')Ajoutez la fonction pour inverser l'ordre :
function toggleSort() {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
}Créez le computed de tri (le dernier de la chaîne) :
const sortedPokemons = computed(() => {
return [...filteredBySearch.value].sort((a, b) => {
const comparison = a.name.localeCompare(b.name, 'fr')
return sortOrder.value === 'asc' ? comparison : -comparison
})
})Points importants :
[...filteredBySearch.value]crée une copie du tableau (ne pas trier l'original)localeCompare('fr')gère correctement les accents et caractères spéciaux françaislocaleCompareretourne : négatif si a < b, 0 si a = b, positif si a > b- On multiplie par
-1(en inversant le signe) pour le tri descendant
5. Ajouter le bouton de tri dans le template
Ajoutez un troisième <v-col> dans la <v-row> de contrôles :
<v-col cols="12" md="4" class="d-flex align-center">
<v-btn
variant="outlined"
:prepend-icon="sortOrder === 'asc'
? 'mdi-sort-alphabetical-ascending'
: 'mdi-sort-alphabetical-descending'"
@click="toggleSort"
>
Tri {{ sortOrder === 'asc' ? 'A → Z' : 'Z → A' }}
</v-btn>
</v-col>6. Utiliser sortedPokemons dans le template
Remplacez filteredBySearch par sortedPokemons dans le v-for et dans le v-if du message "aucun résultat" :
<v-alert
v-if="sortedPokemons.length === 0"
type="info"
variant="tonal"
class="mb-6"
>
Aucun Pokémon ne correspond à votre recherche.
</v-alert>
<v-row v-else>
<v-col
v-for="pokemon in sortedPokemons"
:key="pokemon.id"
cols="12"
sm="6"
md="4"
lg="3"
>
<pokemon-card :pokemon="pokemon" />
</v-col>
</v-row>Schéma — Flux des computed chaînés
Projet perso — À faire en parallèle
Dans votre projet individuel :
- Ajoutez un filtre pertinent pour vos données (catégorie, statut, etc.)
- Ajoutez un tri alphabétique avec un bouton toggle
- Chaînez vos computed : données → filtre → recherche → tri
Références utiles
- Vue.js — computed()
- MDN — Array.filter()
- MDN — Array.sort()
- MDN — String.localeCompare()
- Vuetify — Selects
Tests
- Le
<v-select>affiche la liste des types (Feu, Eau, Plante...) - Sélectionner "Feu" n'affiche que les Pokémon de type Feu
- Vider le filtre type réaffiche tous les Pokémon
- Le filtre type et la recherche fonctionnent ensemble (ex: type Feu + recherche "sal")
- Le bouton de tri alterne entre A-Z et Z-A
- Le tri fonctionne avec la recherche et le filtre actifs
- 0 erreurs dans la console
Solution
src/pages/index.vue
<template>
<!-- Conteneur principal — centre le contenu et limite la largeur -->
<v-container>
<h1 class="text-h3 text-center my-6">
Pokédex
<span class="text-subtitle-1">({{ pokemonStore.totalPokemons }})</span>
</h1>
<!--
Barre de recherche, filtre et tri
* v-row avec 3 colonnes pour les contrôles
-->
<v-row class="mb-4">
<!--
Champ de recherche par nom
* v-model lie la valeur au ref searchQuery
* clearable ajoute un bouton pour vider le champ
-->
<v-col cols="12" sm="6" md="4">
<v-text-field
v-model="searchQuery"
label="Rechercher un Pokémon"
prepend-inner-icon="mdi-magnify"
clearable
hide-details
variant="outlined"
density="compact"
/>
</v-col>
<!--
Filtre par type
* v-select affiche un menu déroulant avec les types disponibles
* :items utilise les types chargés depuis l'API
* item-title et item-value définissent quelles propriétés afficher/utiliser
-->
<v-col cols="12" sm="6" md="4">
<v-select
v-model="selectedType"
:items="pokemonStore.types"
item-title="name"
item-value="id"
label="Filtrer par type"
prepend-inner-icon="mdi-filter"
clearable
hide-details
variant="outlined"
density="compact"
/>
</v-col>
<!--
Bouton de tri
* Alterne entre tri ascendant (A→Z) et descendant (Z→A)
* L'icône change selon l'ordre actuel
-->
<v-col cols="12" md="4" class="d-flex align-center">
<v-btn
variant="outlined"
:prepend-icon="sortOrder === 'asc'
? 'mdi-sort-alphabetical-ascending'
: 'mdi-sort-alphabetical-descending'"
@click="toggleSort"
>
Tri {{ sortOrder === 'asc' ? 'A → Z' : 'Z → A' }}
</v-btn>
</v-col>
</v-row>
<!--
Message si aucun résultat
* Affiché si la recherche ou le filtre ne donne aucun résultat
-->
<v-alert
v-if="sortedPokemons.length === 0"
type="info"
variant="tonal"
class="mb-6"
>
Aucun Pokémon ne correspond à votre recherche.
</v-alert>
<!--
Grille de cartes Pokémon
* On itère sur sortedPokemons (le dernier computed de la chaîne)
-->
<v-row v-else>
<v-col
v-for="pokemon in sortedPokemons"
:key="pokemon.id"
cols="12"
sm="6"
md="4"
lg="3"
>
<pokemon-card :pokemon="pokemon" />
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { usePokemonStore } from '@/stores/pokemonStore'
import PokemonCard from '@/components/PokemonCard.vue'
// Récupération du store Pokémon
const pokemonStore = usePokemonStore()
/**
* Variables réactives pour la recherche, le filtre et le tri
* ref() est auto-importé grâce à unplugin-auto-import
*/
const searchQuery = ref('')
const selectedType = ref(null)
const sortOrder = ref('asc')
/**
* COMPUTED CHAÎNÉS : Chaque étape filtre ou transforme le résultat de la précédente
* pokemons → filteredByType → filteredBySearch → sortedPokemons
*/
/**
* Étape 1 : Filtrer par type
* Si un type est sélectionné, ne garder que les Pokémon qui possèdent ce type
*/
const filteredByType = computed(() => {
if (!selectedType.value) return pokemonStore.pokemons
return pokemonStore.pokemons.filter(pokemon =>
pokemon.types.includes(selectedType.value),
)
})
/**
* Étape 2 : Filtrer par recherche
* Utilise filteredByType comme source (chaînage)
* La recherche est insensible à la casse (toLowerCase)
*/
const filteredBySearch = computed(() => {
if (!searchQuery.value) return filteredByType.value
const query = searchQuery.value.toLowerCase()
return filteredByType.value.filter(pokemon =>
pokemon.name.toLowerCase().includes(query),
)
})
/**
* Étape 3 : Trier par nom
* [...] crée une copie pour ne pas modifier le tableau filtré
* localeCompare('fr') gère les accents français
*/
const sortedPokemons = computed(() => {
return [...filteredBySearch.value].sort((a, b) => {
const comparison = a.name.localeCompare(b.name, 'fr')
return sortOrder.value === 'asc' ? comparison : -comparison
})
})
/**
* Inverse l'ordre de tri entre ascendant et descendant
*/
function toggleSort() {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
}
</script>Commit
git add -A
git commit -m "feat: filtre par type et tri alphabétique avec computed chaînés"