Les prédicats de recherche et de filtre

Un projet reste visible si son titre correspond à la recherche et si sa technologie correspond au choix, sauf lorsque ce choix vaut « toutes ». Décrire cette règle permet de choisir les opérateurs avant le code.

const recherche = saisie.trim().toLowerCase();
const visibles = projets.filter((projet) =>
  projet.title.toLowerCase().includes(recherche)
);

filter produit un nouveau tableau contenant les éléments pour lesquels la fonction renvoie vrai. Il ne supprime pas les projets de la base. includes recherche ici une sous-chaîne ; toLowerCase rend la comparaison insensible à la casse dans ce cas simple. trim retire les espaces de bord, pas les espaces au milieu du titre.

OpérationDonnée concernéeRésultat
Recherche.Tableau complet reçu.Sous-ensemble visible.
Filtre de technologie.Champ technology.Projets de la catégorie ou tous.
Réinitialisation.Contrôles et vue.Collection complète affichée à nouveau.

Le tableau source et la vue filtrée

Si vous remplacez le tableau source à chaque recherche, effacer le champ peut ne pas restaurer les projets déjà éliminés. Conservez une source stable et recalculez la vue à partir de cette source. Une suppression véritable relève d’une commande différente vers l’API.

input, change et compteur de résultats

input convient à une recherche pendant la saisie ; change convient à un select. Affichez le nombre de projets visibles et un état sans résultat. La recherche locale est adaptée à la petite collection du TP ; une très grande collection demanderait une recherche côté serveur, qui n’est pas ajoutée ici sans besoin.

Array.filter et combinaison des filtres

Le visiteur veut trouver un projet. Une recherche locale part du tableau complet reçu et produit un sous-ensemble avec filter. Elle ne supprime rien dans MySQL. Gardez le tableau source pour pouvoir réinitialiser la recherche.

function selectionner(projects, texte, technologie) {
  const recherche = texte.trim().toLocaleLowerCase('fr');
  return projects.filter(project => {
    const titreCorrespond = project.title.toLocaleLowerCase('fr')
      .includes(recherche);
    const technoCorrespond = !technologie || project.technologies
      .split(',').map(t => t.trim()).includes(technologie);
    return titreCorrespond && technoCorrespond;
  });
}

Résultat attendu : le titre et la technologie doivent correspondre tous les deux. La casse du titre est ignorée ici ; les accents restent significatifs. Le découpage par virgule découle de notre contrat technologies en chaîne. Un autre modèle exigerait une autre lecture.

Combiner recherche et filtre sans perdre les données

<style>
      * {
        box-sizing: border-box;
      }
      body {
        margin: 0;
        padding: 20px;
        font-family: system-ui, sans-serif;
        color: #18324c;
        background: #f1f5f9;
      }
      button,
      input,
      select,
      textarea {
        font: inherit;
        max-width: 100%;
      }
      button {
        padding: 8px 12px;
        cursor: pointer;
      }
      input,
      select,
      textarea {
        padding: 8px;
      }
      :focus-visible {
        outline: 3px solid #2563eb;
        outline-offset: 3px;
      }
      article,
      .panel {
        padding: 16px;
        background: white;
        border-radius: 12px;
        min-width: 0;
      }
      .cards {
        display: grid;
        gap: 16px;
      }
      label {
        display: block;
        margin-block: 12px 4px;
      }
      h1 {
        font-size: 1.4rem;
      }
      h2 {
        font-size: 1.15rem;
      }
      p,
      h2,
      a {
        overflow-wrap: anywhere;
      }
      .note {
        font-size: 0.9rem;
      }

      @media (min-width: 700px) {
        .cards {
          grid-template-columns: repeat(2, minmax(0, 1fr));
        }
      }
      @media (min-width: 1000px) {
        .cards {
          grid-template-columns: repeat(3, minmax(0, 1fr));
        }
      }
    </style>

<h1>Portfolio : données, rendu et filtres</h1>
    <p class="note">
      Prototype en mémoire ; la version PHP/MySQL du TP conserve les données.
    </p>
    <label for="recherche">Rechercher dans les titres</label>
    <input id="recherche" type="search" />
    <label for="technologie">Technologie</label>
    <select id="technologie">
      <option value="">Toutes</option>
      <option>HTML</option>
      <option>CSS</option>
      <option>PHP</option>
    </select>
    <button id="reset">Réinitialiser les filtres</button>
    <p id="etat" role="status"></p>
    <section id="projets" class="cards"></section>

    <script>
      const projects = [
        {
          id: 1,
          title: "Festival étudiant",
          description: "Une identité et un site.",
          technologies: "HTML, CSS",
        },
        {
          id: 2,
          title: "Galerie photo",
          description: "Un catalogue de portraits.",
          technologies: "PHP, CSS",
        },
        {
          id: 3,
          title: "Tournoi du campus",
          description: "Une page événement.",
          technologies: "HTML",
        },
      ];
      function render() {
        const text = document
          .querySelector("#recherche")
          .value.trim()
          .toLocaleLowerCase("fr");
        const technology = document.querySelector("#technologie").value;
        const results = projects.filter(
          (p) =>
            p.title.toLocaleLowerCase("fr").includes(text) &&
            (!technology ||
              p.technologies
                .split(",")
                .map((t) => t.trim())
                .includes(technology)),
        );
        const cards = results.map((p) => {
          const article = document.createElement("article");
          const h2 = document.createElement("h2");
          h2.textContent = p.title;
          const description = document.createElement("p");
          description.textContent = p.description;
          const technologies = document.createElement("p");
          technologies.textContent = p.technologies;
          article.append(h2, description, technologies);
          return article;
        });
        document.querySelector("#projets").replaceChildren(...cards);
        document.querySelector("#etat").textContent = results.length
          ? `${results.length} projet(s)`
          : "Aucun résultat pour ces filtres.";
      }
      document.querySelector("#recherche").addEventListener("input", render);
      document.querySelector("#technologie").addEventListener("change", render);
      document.querySelector("#reset").addEventListener("click", () => {
        document.querySelector("#recherche").value = "";
        document.querySelector("#technologie").value = "";
        render();
      });
      render();
    </script>

Les événements nécessaires

input suit les modifications du champ de recherche ; change suffit pour le choix dans le select. Les labels décrivent les deux contrôles. Le bouton de remise à zéro restaure texte et technologie avant de relancer le rendu. Un compteur dans role="status" indique le nombre de résultats sans déplacer le focus.

Les données du petit portfolio tiennent en mémoire : il n’est pas nécessaire d’appeler PHP à chaque frappe. Pour un catalogue volumineux, une recherche serveur serait pertinente ; elle demanderait un contrat et des requêtes paramétrées distincts.

Tableau de référence — notions et opérations

NotionRègle à retenir
Filtre localLe filtre local sélectionne des données déjà reçues.
Source complèteLe tableau complet est conservé pour réinitialiser le filtre.
FilterArray.filter produit le sous-ensemble des éléments qui satisfont une condition.
Normalisationtrim enlève les espaces de bord de la recherche.
CassetoLocaleLowerCase(‘fr’) sert à comparer les titres sans tenir compte de la casse.
AccentsDans cet exemple, les accents restent significatifs.
Sous-chaîneincludes vérifie si le titre contient le texte recherché.
Recherche videUne chaîne vide n’exclut aucun titre avec includes.
Technologies chaîneLe contrat stocke technologies sous forme de chaîne séparée par virgules.
Découpagesplit(’,’) découpe la chaîne des technologies.
Nettoyage morceauxLes morceaux de technologies sont nettoyés par trim.
Combinaison&& exige que texte et technologie correspondent tous les deux.
Technologie videUne technologie vide signifie aucun filtre de technologie.
Inputinput suit les modifications du champ de recherche.
Changechange est utilisé pour le choix de technologie.
LabelsChaque contrôle de filtre possède un label.
Remise à zéroLa remise à zéro restaure texte et technologie avant de rendre.
CompteurLe compteur d’état indique le nombre de résultats sans déplacer le focus.
Aucun résultatAucun résultat de filtre ne signifie pas nécessairement une base vide.
Sans requête par frappeCe petit filtre local n’exige pas un appel PHP à chaque frappe.