Créer une application de recherche locale avec le Kit UI pour Places

1. Avant de commencer

Cet atelier de programmation vous explique comment créer une application de recherche locale entièrement interactive à l'aide du kit UI Places de Google Maps Platform.

Capture d'écran de l'application PlaceFinder terminée, montrant une carte de New York avec des repères, une barre latérale avec les résultats de recherche et une fiche d'informations ouverte.

Prérequis

  • Un projet Google Cloud avec les API et les identifiants nécessaires configurés.
  • Connaissances de base en HTML et en CSS
  • Comprendre le JavaScript moderne
  • Un navigateur Web récent, tel que la dernière version de Chrome.
  • Un éditeur de texte de votre choix.

Objectifs de l'atelier

  • Structurez une application de cartographie à l'aide d'une classe JavaScript.
  • Utiliser des composants Web pour afficher une carte
  • Utilisez l'élément Place Search pour effectuer une recherche de texte et afficher les résultats.
  • Créez et gérez par programmation des repères de carte AdvancedMarkerElement personnalisés.
  • Affichez l'élément "Détails du lieu" lorsqu'un utilisateur sélectionne un lieu.
  • Utilisez l'API Geocoding pour créer une interface dynamique et conviviale.

Prérequis

  • Un projet Google Cloud avec facturation activée
  • Une clé API Google Maps Platform
  • Un ID de carte
  • Les API suivantes sont activées :
    • API Maps JavaScript
    • Kit UI Places
    • API Geocoding

2. Configuration

Pour l'étape suivante, vous devez activer l'API Maps JavaScript, le kit d'interface utilisateur Places et l'API Geocoding.

Configurer Google Maps Platform

Si vous ne disposez pas encore d'un compte Google Cloud Platform et d'un projet pour lequel la facturation est activée, consultez le guide Premiers pas avec Google Maps Platform pour savoir comment créer un compte de facturation et un projet.

  1. Dans Cloud Console, cliquez sur le menu déroulant des projets, puis sélectionnez celui que vous souhaitez utiliser pour cet atelier de programmation.

  1. Activez les API et les SDK Google Maps Platform requis pour cet atelier de programmation dans Google Cloud Marketplace. Pour ce faire, suivez les étapes indiquées dans cette vidéo ou dans cette documentation.
  2. Générez une clé API sur la page Identifiants de Cloud Console. Vous pouvez suivre la procédure décrite dans cette vidéo ou dans cette documentation. Toutes les requêtes envoyées à Google Maps Platform nécessitent une clé API.

3. Le shell d'application et une carte fonctionnelle

Dans cette première étape, nous allons créer la mise en page visuelle complète de notre application et établir une structure propre basée sur des classes pour notre JavaScript. Cela nous donne une base solide sur laquelle nous pouvons nous appuyer. À la fin de cette section, vous disposerez d'une page stylisée affichant une carte interactive.

Créer le fichier HTML

Commencez par créer un fichier nommé index.html. Ce fichier contiendra la structure complète de notre application, y compris l'en-tête, les filtres de recherche, la barre latérale, le conteneur de carte et les composants Web nécessaires.

Copiez le code suivant dans index.html. Veillez à remplacer YOUR_API_KEY_HERE par votre propre clé API Google Maps Platform et DEMO_MAP_ID par votre propre ID de carte Google Maps Platform.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Local Search App</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!-- Google Fonts: Roboto -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
    <!-- GMP Bootstrap Loader -->
    <script>
      (g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
        key: "YOUR_API_KEY_HERE",
        v: "weekly",
        libraries: "places,maps,marker,geocoding"
      });
    </script>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <!-- Header for search controls -->
    <header class="top-header">
        <div class="logo">
            <svg viewBox="0 0 24 24" width="28" height="28"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" fill="currentColor"></path></svg>
            <span>PlaceFinder</span>
        </div>
        <div class="search-container">
            <input
              type="text"
              id="query-input"
              placeholder="e.g., burger in New York"
              value="burger"
            />
            <button id="search-button" aria-label="Search">Search</button>
        </div>
        <div class="filter-container">
            <label class="open-now-label">
              <input type="checkbox" id="open-now-filter"> Open Now
            </label>
            <select id="rating-filter" aria-label="Minimum rating">
              <option value="0" selected>Any rating</option>
              <option value="1">1+ </option>
              <option value="2">2+ ★★</option>
              <option value="3">3+ ★★★</option>
              <option value="4">4+ ★★★★</option>
              <option value="5">5 ★★★★★</option>
            </select>
             <select id="price-filter" aria-label="Price level">
              <option value="0" selected>Any Price</option>
              <option value="1">$</option>
              <option value="2">$$</option>
              <option value="3">$$$</option>
              <option value="4">$$$$</option>
            </select>
        </div>
    </header>

    <!-- Main content area -->
    <div class="app-container">
      <!-- Left Panel: Results -->
      <div class="sidebar">
          <div class="results-header">
            <h2 id="results-header-text">Results</h2>
          </div>
          <div class="results-container">
              <gmp-place-search id="place-search-list" class="hidden" selectable>
                <gmp-place-all-content></gmp-place-all-content>
                <gmp-place-text-search-request></gmp-place-text-search-request>
              </gmp-place-search>

              <div id="placeholder-message" class="placeholder">
                  <p>Your search results will appear here.</p>
              </div>

              <div id="loading-spinner" class="spinner-overlay">
                  <div class="spinner"></div>
              </div>
          </div>
      </div>

      <!-- Right Panel: Map -->
      <div class="map-container">
        <gmp-map
          center="40.758896,-73.985130"
          zoom="13"
          map-id="DEMO_MAP_ID"
        >
        </gmp-map>
        <div id="details-container">
            <gmp-place-details-compact>
                <gmp-place-details-place-request></gmp-place-details-place-request>
                <gmp-place-all-content></gmp-place-all-content>
            </gmp-place-details-compact>
        </div>
      </div>
    </div>
    <script src="script.js"></script>
  </body>
</html>

Créer le fichier CSS

Ensuite, créez un fichier nommé style.css. Nous allons maintenant ajouter tous les styles nécessaires pour établir un look épuré et moderne dès le début. Ce CSS gère la mise en page globale, les couleurs, les polices et l'apparence de tous nos éléments d'interface utilisateur.

Copiez le code suivant dans style.css :

/* style.css */
:root {
  --primary-color: #1a73e8;
  --text-color: #202124;
  --text-color-light: #5f6368;
  --background-color: #f8f9fa;
  --panel-background: #ffffff;
  --border-color: #dadce0;
  --shadow-color: rgba(0, 0, 0, 0.1);
}

body {
  font-family: 'Roboto', sans-serif;
  margin: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background-color: var(--background-color);
  color: var(--text-color);
}

.hidden {
  display: none !important;
}

.top-header {
  display: flex;
  align-items: center;
  padding: 12px 24px;
  border-bottom: 1px solid var(--border-color);
  background-color: var(--panel-background);
  gap: 24px;
  flex-shrink: 0;
}

.logo {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 22px;
  font-weight: 700;
  color: var(--primary-color);
}

.search-container {
  display: flex;
  flex-grow: 1;
  max-width: 720px;
}

.search-container input {
  width: 100%;
  padding: 12px 16px;
  border: 1px solid var(--border-color);
  border-radius: 8px 0 0 8px;
  font-size: 16px;
  transition: box-shadow 0.2s ease;
}

.search-container input:focus {
  outline: none;
  border-color: var(--primary-color);
  box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
}

.search-container button {
  padding: 0 20px;
  border: 1px solid var(--primary-color);
  border-radius: 0 8px 8px 0;
  background-color: var(--primary-color);
  color: white;
  cursor: pointer;
  font-size: 16px;
  font-weight: 500;
  transition: background-color 0.2s ease;
}

.search-container button:hover {
  background-color: #185abc;
}

.filter-container {
  display: flex;
  gap: 12px;
  align-items: center;
}

.filter-container select, .open-now-label {
  padding: 10px 14px;
  border: 1px solid var(--border-color);
  border-radius: 8px;
  background-color: var(--panel-background);
  font-size: 14px;
  cursor: pointer;
  transition: border-color 0.2s ease;
}

.filter-container select:hover, .open-now-label:hover {
  border-color: #c0c2c5;
}

.open-now-label {
  display: flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
}

.app-container {
  display: flex;
  flex-grow: 1;
  overflow: hidden;
}

.sidebar {
  width: 35%;
  min-width: 380px;
  max-width: 480px;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--border-color);
  background-color: var(--panel-background);
  overflow: hidden;
}

.results-header {
  padding: 16px 24px;
  border-bottom: 1px solid var(--border-color);
  flex-shrink: 0;
}

.results-header h2 {
  margin: 0;
  font-size: 18px;
  font-weight: 500;
}

.results-container {
  flex-grow: 1;
  position: relative;
  overflow-y: auto;
  overflow-x: hidden;
}

.placeholder {
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: 2rem;
  box-sizing: border-box;
}

.placeholder p {
  color: var(--text-color-light);
  font-size: 1.1rem;
}

gmp-place-search {
  width: 100%;
}

.map-container {
  flex-grow: 1;
  position: relative;
}

gmp-map {
  width: 100%;
  height: 100%;
}

.spinner-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(255, 255, 255, 0.7);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 100;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s, visibility 0.3s;
}
.spinner-overlay.visible {
  opacity: 1;
  visibility: visible;
}
.spinner {
  width: 48px;
  height: 48px;
  border: 4px solid #e0e0e0;
  border-top-color: var(--primary-color);
  border-radius: 50%;
  animation: spin 1s linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

gmp-place-details-compact {
  width: 350px;
  display: none;
  border: none;
  border-radius: 12px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

gmp-place-details-compact::after {
    content: '';
    position: absolute;
    bottom: -12px;
    left: 50%;
    transform: translateX(-50%);
    width: 24px;
    height: 12px;
    background-color: var(--panel-background);
    clip-path: polygon(50% 100%, 0 0, 100% 0);
}

Créer la classe d'application JavaScript

Enfin, créez un fichier nommé script.js. Nous allons structurer notre application dans une classe JavaScript appelée PlaceFinderApp. Cela permet d'organiser notre code et de gérer l'état de manière claire.

Ce code initial définira la classe, trouvera tous nos éléments HTML dans le constructor et créera une méthode init() pour charger les bibliothèques Google Maps Platform.

Copiez le code suivant dans script.js :

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    // We will add more initialization logic here in later steps.
  }
}

// Wait for the DOM to be ready, then create an instance of our app.
window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Restrictions de clé API

Pour que cet atelier de programmation fonctionne, vous devrez peut-être ajouter une restriction à votre clé API. Pour en savoir plus et obtenir des conseils sur la façon de procéder, consultez Restreindre vos clés API.

Vérifiez votre travail

Ouvrez le fichier index.html dans votre navigateur Web. Vous devriez voir une page avec un en-tête contenant une barre de recherche et des filtres, une barre latérale avec le message "Vos résultats de recherche s'afficheront ici" et une grande carte centrée sur la ville de New York. À ce stade, les commandes de recherche ne sont pas encore fonctionnelles.

4. Implémenter une fonction de recherche

Dans cette section, nous allons donner vie à notre application en implémentant la fonctionnalité de recherche principale. Nous allons écrire le code qui s'exécute lorsqu'un utilisateur clique sur le bouton "Rechercher". Nous allons créer cette fonction en suivant les bonnes pratiques dès le début pour gérer les interactions utilisateur de manière fluide et éviter les bugs courants tels que les conditions de concurrence.

À la fin de cette étape, vous pourrez cliquer sur le bouton de recherche et voir un indicateur de chargement s'afficher pendant que l'application récupère les données en arrière-plan.

Créer la méthode de recherche

Tout d'abord, définissez la méthode performSearch dans notre classe PlaceFinderApp. Cette fonction sera au cœur de notre logique de recherche. Nous allons également introduire une variable d'instance, isSearchInProgress, pour servir de "contrôleur d'accès". Cela empêche l'utilisateur de lancer une nouvelle recherche alors qu'une autre est déjà en cours, ce qui peut entraîner des erreurs.

La logique à l'intérieur de performSearch peut sembler complexe. Nous allons donc la décomposer :

  1. Elle vérifie d'abord si une recherche est déjà en cours. Si c'est le cas, il ne se passe rien.
  2. Il définit l'indicateur isSearchInProgress sur true pour "verrouiller" la fonction.
  3. Il affiche le spinner de chargement et prépare l'UI pour de nouveaux résultats.
  4. Elle définit la propriété textQuery de la requête de recherche sur null. Il s'agit d'une étape cruciale qui oblige le composant Web à reconnaître qu'une nouvelle requête est en cours.
  5. Il utilise un setTimeout avec un délai 0. Cette technique JavaScript standard planifie l'exécution du reste de notre code dans la prochaine tâche du navigateur, en s'assurant que le composant a d'abord traité la valeur null. Même si l'utilisateur recherche exactement la même chose deux fois, une nouvelle recherche sera toujours déclenchée.

Ajouter des écouteurs d'événements

Ensuite, nous devons appeler notre méthode performSearch lorsque l'utilisateur interagit avec l'application. Nous allons créer une méthode attachEventListeners pour regrouper tout notre code de gestion des événements. Pour l'instant, nous allons simplement ajouter un écouteur pour l'événement click du bouton de recherche. Nous allons également ajouter un espace réservé pour un autre événement, gmp-load, que nous utiliserons à l'étape suivante.

Mettre à jour le fichier JavaScript

Mettez à jour votre fichier script.js avec le code suivant. Les sections nouvelles ou modifiées sont la méthode attachEventListeners et la méthode performSearch.

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    // Call the new method to set up listeners
    this.attachEventListeners();
  }

  // NEW: Method to set up all event listeners
  attachEventListeners() {
    this.searchButton.addEventListener('click', this.performSearch.bind(this));
    // We will add the gmp-load listener in the next step
  }

  // NEW: Core search method
  async performSearch() {
    // Exit if a search is already in progress
    if (this.isSearchInProgress) {
      return;
    }
    // Set the lock
    this.isSearchInProgress = true;

    // Show the placeholder and spinner
    this.placeholderMessage.classList.add('hidden');
    this.placeSearch.classList.remove('hidden');
    this.showLoading(true);

    // Force a state change by clearing the query first.
    this.searchRequest.textQuery = null;

    // Defer setting the real properties to the next event loop cycle.
    setTimeout(async () => {
      const rawQuery = this.queryInput.value.trim();
      // If the query is empty, release the lock and hide the spinner
      if (!rawQuery) {
          this.showLoading(false);
          this.isSearchInProgress = false;
          return;
      };

      // For now, we just set the textQuery. We'll add filters later.
      this.searchRequest.textQuery = rawQuery;
      this.searchRequest.locationRestriction = this.map.getBounds();
    }, 0);
  }

  // NEW: Helper method to show/hide the spinner
  showLoading(visible) {
    this.loadingSpinner.classList.toggle('visible', visible);
  }
}

// Wait for the DOM to be ready, then create an instance of our app.
window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Vérifiez votre travail

Enregistrez votre fichier script.js et actualisez index.html dans votre navigateur. La page devrait être identique à celle affichée précédemment. Cliquez ensuite sur le bouton "Rechercher" dans l'en-tête.

Deux choses devraient se produire :

  1. Le message d'espace réservé "Vos résultats de recherche apparaîtront ici" disparaît.
  2. L'icône de chargement s'affiche et continue de tourner.

Le spinner tournera indéfiniment, car nous ne lui avons pas encore dit quand s'arrêter. Nous le ferons dans la section suivante, lorsque nous afficherons les résultats. Cela confirme que notre fonction de recherche est déclenchée correctement.

5. Afficher les résultats et ajouter des repères

Maintenant que le déclencheur de recherche fonctionne, la prochaine tâche consiste à afficher les résultats à l'écran. Le code de cette section connectera la logique de recherche à l'UI. Une fois que l'élément de recherche de lieux a fini de charger les données, il déverrouille la recherche, masque le spinner de chargement et affiche un repère sur la carte pour chaque résultat.

Écouter la fin de la recherche

L'élément Place Search déclenche un événement gmp-load lorsqu'il a récupéré les données. C'est le signal idéal pour que nous puissions traiter les résultats.

Tout d'abord, ajoutez un écouteur d'événements pour cet événement dans notre méthode attachEventListeners.

Créer des méthodes de gestion des repères

Nous allons ensuite créer deux méthodes d'assistance : clearMarkers et addMarkers.

  • clearMarkers() supprimera tous les repères d'une recherche précédente.
  • addMarkers() sera appelé par notre écouteur gmp-load. Il parcourra la liste des lieux renvoyés par la recherche et créera un AdvancedMarkerElement pour chacun d'eux. C'est également là que nous allons masquer l'icône de chargement et déverrouiller le isSearchInProgress, ce qui termine le cycle de recherche.

Notez que nous stockons les repères dans un objet (this.markers) en utilisant l'ID du lieu comme clé. Cela nous permettra de gérer les repères et de retrouver un repère spécifique ultérieurement.

Enfin, nous devons appeler clearMarkers() au début de chaque nouvelle recherche. Le meilleur endroit pour cela est dans performSearch.

Mettre à jour le fichier JavaScript

Mettez à jour votre fichier script.js avec les nouvelles méthodes et les modifications apportées à attachEventListeners et performSearch.

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    this.attachEventListeners();
  }

  attachEventListeners() {
    this.searchButton.addEventListener('click', this.performSearch.bind(this));
    // NEW: Listen for when the search component has loaded results
    this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
  }

  // NEW: Method to clear markers from a previous search
  clearMarkers() {
    for (const marker of Object.values(this.markers)) {
      marker.map = null;
    }
    this.markers = {};
  }

  // NEW: Method to add markers for new search results
  addMarkers() {
    // Release the lock and hide the spinner
    this.isSearchInProgress = false;
    this.showLoading(false);

    const places = this.placeSearch.places;
    if (!places || places.length === 0) return;

    // Create a new marker for each place result
    for (const place of places) {
      if (!place.location || !place.id) continue;
      const marker = new this.AdvancedMarkerElement({
        map: this.map,
        position: place.location,
        title: place.displayName,
      });
      // Store marker by its place ID for access later
      this.markers[place.id] = marker;
    }
  }

  async performSearch() {
    if (this.isSearchInProgress) {
      return;
    }
    this.isSearchInProgress = true;
    this.placeholderMessage.classList.add('hidden');
    this.placeSearch.classList.remove('hidden');
    this.showLoading(true);

    // NEW: Clear old markers before starting a new search
    this.clearMarkers();

    this.searchRequest.textQuery = null;

    setTimeout(async () => {
      const rawQuery = this.queryInput.value.trim();
      if (!rawQuery) {
          this.showLoading(false);
          this.isSearchInProgress = false;
          return;
      };

      this.searchRequest.textQuery = rawQuery;
      this.searchRequest.locationRestriction = this.map.getBounds();
    }, 0);
  }

  showLoading(visible) {
    this.loadingSpinner.classList.toggle('visible', visible);
  }
}

window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Vérifiez votre travail

Enregistrez vos fichiers et actualisez la page dans votre navigateur. Cliquez sur le bouton "Rechercher".

L'icône de chargement devrait maintenant apparaître un instant, puis disparaître. La barre latérale se remplit avec une liste de lieux correspondant au terme de recherche, et des repères correspondants devraient apparaître sur la carte. Pour l'instant, les repères ne font rien lorsqu'on clique dessus. Nous ajouterons cette interactivité dans la section suivante.

6. Activer les filtres de recherche et l'interactivité de la liste

Notre application peut désormais afficher des résultats de recherche, mais elle n'est pas encore interactive. Dans cette section, nous allons donner vie à tous les contrôles utilisateur. Nous allons activer les filtres, permettre la recherche avec la touche "Entrée" et associer les éléments de la liste de résultats à leurs emplacements correspondants sur la carte.

À la fin de cette étape, l'application sera entièrement réactive aux saisies de l'utilisateur.

Activer les filtres de recherche

Tout d'abord, la méthode performSearch sera mise à jour pour lire les valeurs de tous les contrôles de filtre dans l'en-tête. Pour chaque filtre (prix, note et "Ouvert actuellement"), la propriété correspondante sera définie sur l'objet searchRequest avant l'exécution de la recherche.

Ajouter des écouteurs d'événements pour tous les contrôles

Nous allons ensuite étendre notre méthode attachEventListeners. Nous allons ajouter des écouteurs pour l'événement change sur chaque commande de filtre, ainsi qu'un écouteur keydown sur le champ de recherche pour détecter lorsque l'utilisateur appuie sur la touche "Entrée". Tous ces nouveaux écouteurs appelleront la méthode performSearch.

Associer la liste des résultats à la carte

Pour créer une expérience fluide, lorsque l'utilisateur clique sur un élément de la liste des résultats de la barre latérale, la carte doit se recentrer sur cet emplacement.

Une nouvelle méthode, handleResultClick, écoutera l'événement gmp-select, qui est déclenché par l'élément de recherche de lieux lorsqu'un élément est sélectionné. Cette fonction permet de trouver l'emplacement du lieu associé et de faire défiler la carte jusqu'à celui-ci.

Pour que cela fonctionne, assurez-vous que l'attribut selectable est présent sur votre composant gmp-place-search dans index.html.

<gmp-place-search id="place-search-list" class="hidden" selectable>
    <gmp-place-all-content></gmp-place-all-content>
    <gmp-place-text-search-request></gmp-place-text-search-request>
</gmp-place-search>

Mettre à jour le fichier JavaScript

Mettez à jour votre fichier script.js avec le code complet suivant. Cette version inclut la nouvelle méthode handleResultClick et la logique mise à jour dans attachEventListeners et performSearch.

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    this.attachEventListeners();
  }

  // UPDATED: All event listeners are now attached
  attachEventListeners() {
    // Listen for the 'Enter' key press in the search input
    this.queryInput.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') {
        event.preventDefault();
        this.performSearch();
      }
    });

    // Listen for a sidebar result click
    this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));

    this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
    this.searchButton.addEventListener('click', this.performSearch.bind(this));
    this.priceFilter.addEventListener('change', this.performSearch.bind(this));
    this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
    this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
  }

  clearMarkers() {
    for (const marker of Object.values(this.markers)) {
      marker.map = null;
    }
    this.markers = {};
  }

  addMarkers() {
    this.isSearchInProgress = false;
    this.showLoading(false);

    const places = this.placeSearch.places;
    if (!places || places.length === 0) return;

    for (const place of places) {
      if (!place.location || !place.id) continue;
      const marker = new this.AdvancedMarkerElement({
        map: this.map,
        position: place.location,
        title: place.displayName,
      });
      this.markers[place.id] = marker;
    }
  }

  // NEW: Function to handle clicks on the results list
  handleResultClick(event) {
    const place = event.place;
    if (!place || !place.location) return;
    // Pan the map to the selected place
    this.map.panTo(place.location);
  }

  // UPDATED: Search function now includes all filters
  async performSearch() {
    if (this.isSearchInProgress) {
      return;
    }
    this.isSearchInProgress = true;
    this.placeholderMessage.classList.add('hidden');
    this.placeSearch.classList.remove('hidden');
    this.showLoading(true);
    this.clearMarkers();

    this.searchRequest.textQuery = null;

    setTimeout(async () => {
      const rawQuery = this.queryInput.value.trim();
      if (!rawQuery) {
          this.showLoading(false);
          this.isSearchInProgress = false;
          return;
      };

      this.searchRequest.textQuery = rawQuery;
      this.searchRequest.locationRestriction = this.map.getBounds();

      // Add filter values to the request
      const selectedPrice = this.priceFilter.value;
      let priceLevels = [];
      switch (selectedPrice) {
        case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
        case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
        case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
        case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
        default: priceLevels = null; break;
      }
      this.searchRequest.priceLevels = priceLevels;

      const selectedRating = parseFloat(this.ratingFilter.value);
      this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
      this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
    }, 0);
  }

  showLoading(visible) {
    this.loadingSpinner.classList.toggle('visible', visible);
  }
}

window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Vérifiez votre travail

Enregistrez votre fichier script.js et actualisez la page. L'application doit maintenant être très interactive.

Effectuez les vérifications suivantes :

  • La recherche fonctionne lorsque vous appuyez sur "Entrée" dans le champ de recherche.
  • Si vous modifiez l'un des filtres (prix, note, ouvert actuellement), une nouvelle recherche est lancée et les résultats sont mis à jour.
  • Lorsque vous cliquez sur un élément de la liste dans la barre latérale, la carte se déplace de manière fluide vers l'emplacement de cet élément.

Dans la section suivante, nous allons implémenter la fiche d'informations qui s'affiche lorsqu'un repère est sélectionné.

7. Implémenter l'élément Place Details

Notre application est désormais entièrement interactive, mais il lui manque une fonctionnalité clé : la possibilité d'afficher plus d'informations sur un lieu sélectionné. Dans cette section, nous allons implémenter l'élément "Détails du lieu" qui s'affichera lorsqu'un utilisateur cliquera sur un repère sur la carte ou sélectionnera un élément dans l'élément "Recherche de lieux".

Créer un conteneur de fiche d'informations réutilisable

Le moyen le plus efficace d'afficher les détails d'un lieu sur la carte consiste à créer un conteneur unique et réutilisable. Nous allons utiliser un AdvancedMarkerElement comme conteneur. Son contenu sera le widget gmp-place-details-compact masqué que nous avons déjà dans notre index.html.

Une nouvelle méthode, initDetailsPopup, gérera la création de ce marqueur réutilisable. Il sera créé une seule fois lors du chargement de l'application et sera masqué au départ. Nous ajouterons également un écouteur à la carte principale dans cette méthode, afin que le fait de cliquer n'importe où sur la carte masque la fiche d'informations.

Mettre à jour le comportement en cas de clic sur le repère

Ensuite, nous devons mettre à jour ce qui se passe lorsqu'un utilisateur clique sur un repère. Le listener 'click' à l'intérieur de la méthode addMarkers sera désormais responsable de l'affichage de la fiche d'informations.

Lorsqu'un utilisateur clique sur un repère, l'écouteur :

  1. Faites glisser la carte jusqu'à l'emplacement du repère.
  2. Mettez à jour la fiche d'informations avec les informations concernant ce lieu spécifique.
  3. Positionnez la fiche d'informations à l'emplacement du repère et rendez-la visible.

Associer le clic sur la liste au clic sur le repère

Enfin, nous allons mettre à jour la méthode handleResultClick. Au lieu de simplement faire glisser la carte, il déclenchera désormais par programmation l'événement click sur le repère correspondant. Il s'agit d'un modèle puissant qui nous permet de réutiliser exactement la même logique pour les deux interactions, ce qui permet de garder notre code propre et facile à gérer.

Mettre à jour le fichier JavaScript

Mettez à jour votre fichier script.js avec le code suivant. Les sections nouvelles ou modifiées sont la méthode initDetailsPopup et les méthodes addMarkers et handleResultClick mises à jour.

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    // NEW: Call the method to initialize the details card
    this.initDetailsPopup();
    this.attachEventListeners();
  }

  attachEventListeners() {
    this.queryInput.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') {
        event.preventDefault();
        this.performSearch();
      }
    });
    this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
    this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
    this.searchButton.addEventListener('click', this.performSearch.bind(this));
    this.priceFilter.addEventListener('change', this.performSearch.bind(this));
    this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
    this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
  }

  // NEW: Method to set up the reusable details card
  initDetailsPopup() {
    this.detailsPopup = new this.AdvancedMarkerElement({
      content: this.placeDetailsWidget,
      map: null,
      zIndex: 100
    });
    this.map.addListener('click', () => { this.detailsPopup.map = null; });
  }

  clearMarkers() {
    for (const marker of Object.values(this.markers)) {
      marker.map = null;
    }
    this.markers = {};
  }

  // UPDATED: The marker's click listener now shows the details card
  addMarkers() {
    this.isSearchInProgress = false;
    this.showLoading(false);

    const places = this.placeSearch.places;
    if (!places || places.length === 0) return;

    for (const place of places) {
      if (!place.location || !place.id) continue;
      const marker = new this.AdvancedMarkerElement({
        map: this.map,
        position: place.location,
        title: place.displayName,
      });
      // Add the click listener to show the details card
      marker.addListener('click', (event) => {
        event.stop();
        this.map.panTo(place.location);
        this.placeDetailsRequest.place = place;
        this.placeDetailsWidget.style.display = 'block';
        this.detailsPopup.position = place.location;
        this.detailsPopup.map = this.map;
      });
      this.markers[place.id] = marker;
    }
  }

  // UPDATED: This now triggers the marker's click event
  handleResultClick(event) {
    const place = event.place;
    if (!place || !place.id) return;
    const marker = this.markers[place.id];
    if (marker) {
      // Programmatically trigger the marker's click event
      marker.click();
    }
  }

  async performSearch() {
    if (this.isSearchInProgress) return;
    this.isSearchInProgress = true;
    this.placeholderMessage.classList.add('hidden');
    this.placeSearch.classList.remove('hidden');
    this.showLoading(true);
    this.clearMarkers();
    // Hide the details card when a new search starts
    if (this.detailsPopup) this.detailsPopup.map = null;

    this.searchRequest.textQuery = null;

    setTimeout(async () => {
      const rawQuery = this.queryInput.value.trim();
      if (!rawQuery) {
          this.showLoading(false);
          this.isSearchInProgress = false;
          return;
      };

      this.searchRequest.textQuery = rawQuery;
      this.searchRequest.locationRestriction = this.map.getBounds();

      const selectedPrice = this.priceFilter.value;
      let priceLevels = [];
      switch (selectedPrice) {
        case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
        case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
        case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
        case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
        default: priceLevels = null; break;
      }
      this.searchRequest.priceLevels = priceLevels;

      const selectedRating = parseFloat(this.ratingFilter.value);
      this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
      this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
    }, 0);
  }

  showLoading(visible) {
    this.loadingSpinner.classList.toggle('visible', visible);
  }
}

window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Vérifiez votre travail

Enregistrez votre fichier script.js et actualisez la page. L'application devrait maintenant afficher les détails à la demande.

Effectuez les vérifications suivantes :

  • Lorsque vous cliquez sur un repère sur la carte, celle-ci est désormais centrée et une fiche d'informations stylisée s'ouvre au-dessus du repère.
  • Cliquer sur un élément de la liste des résultats dans la barre latérale a exactement le même effet.
  • Si vous cliquez sur la carte en dehors de la fiche, celle-ci se ferme.
  • Le lancement d'une nouvelle recherche ferme également toute fiche d'informations ouverte.

8. Peaufiner le résultat

Notre application est désormais entièrement fonctionnelle, mais nous pouvons ajouter quelques touches finales pour améliorer encore l'expérience utilisateur. Dans cette dernière section, nous allons implémenter deux fonctionnalités clés : un en-tête dynamique qui fournit un meilleur contexte pour les résultats de recherche et une mise en forme automatique pour la requête de recherche de l'utilisateur.

Créer un en-tête de résultats dynamiques

Pour le moment, l'en-tête de la barre latérale indique toujours "Résultats". Nous pouvons rendre cette information plus utile en la mettant à jour pour refléter la recherche en cours. Par exemple, "Burgers près de New York".

Pour ce faire, nous utiliserons l'API Geocoding afin de convertir les coordonnées du centre de la carte en un emplacement lisible, comme un nom de ville. Une nouvelle méthode async, updateResultsHeader, gérera cette logique. Il sera appelé à chaque fois qu'une recherche sera effectuée.

Mettre en forme la requête de recherche de l'utilisateur

Pour que l'UI soit propre et cohérente, nous mettrons automatiquement en forme le terme de recherche de l'utilisateur en "Title Case" (par exemple, "restaurant de burgers" devient "Restaurant de burgers"). Une fonction d'assistance, toTitleCase, gérera cette transformation. La méthode performSearch sera mise à jour pour utiliser cette fonction sur l'entrée de l'utilisateur avant d'effectuer la recherche et de mettre à jour l'en-tête.

Mettre à jour le fichier JavaScript

Mettez à jour votre fichier script.js avec la version finale du code. Cela inclut les nouvelles méthodes toTitleCase et updateResultsHeader, ainsi que la méthode performSearch mise à jour qui les intègre.

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    this.initDetailsPopup();
    this.attachEventListeners();
  }

  attachEventListeners() {
    this.queryInput.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') {
        event.preventDefault();
        this.performSearch();
      }
    });
    this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
    this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
    this.searchButton.addEventListener('click', this.performSearch.bind(this));
    this.priceFilter.addEventListener('change', this.performSearch.bind(this));
    this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
    this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
  }

  initDetailsPopup() {
    this.detailsPopup = new this.AdvancedMarkerElement({
      content: this.placeDetailsWidget,
      map: null,
      zIndex: 100
    });
    this.map.addListener('click', () => { this.detailsPopup.map = null; });
  }

  // NEW: Helper function to format text to Title Case
  toTitleCase(str) {
    if (!str) return '';
    return str.toLowerCase().split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
  }

  showLoading(visible) {
    this.loadingSpinner.classList.toggle('visible', visible);
  }

  clearMarkers() {
    for (const marker of Object.values(this.markers)) { marker.map = null; }
    this.markers = {};
  }

  addMarkers() {
    this.isSearchInProgress = false;
    this.showLoading(false);

    const places = this.placeSearch.places;
    if (!places || places.length === 0) return;

    for (const place of places) {
      if (!place.location || !place.id) continue;
      const marker = new this.AdvancedMarkerElement({
        map: this.map,
        position: place.location,
        title: place.displayName,
      });
      marker.addListener('click', (event) => {
        event.stop();
        this.map.panTo(place.location);
        this.placeDetailsRequest.place = place;
        this.placeDetailsWidget.style.display = 'block';
        this.detailsPopup.position = place.location;
        this.detailsPopup.map = this.map;
      });
      this.markers[place.id] = marker;
    }
  }

  handleResultClick(event) {
    const place = event.place;
    if (!place || !place.id) return;
    const marker = this.markers[place.id];
    if (marker) {
      marker.click();
    }
  }

  // UPDATED: Now integrates formatting and the dynamic header
  async performSearch() {
    if (this.isSearchInProgress) return;
    this.isSearchInProgress = true;
    this.placeholderMessage.classList.add('hidden');
    this.placeSearch.classList.remove('hidden');
    this.showLoading(true);
    this.clearMarkers();
    if (this.detailsPopup) this.detailsPopup.map = null;

    this.searchRequest.textQuery = null;

    setTimeout(async () => {
      const rawQuery = this.queryInput.value.trim();
      if (!rawQuery) {
          this.showLoading(false);
          this.isSearchInProgress = false;
          return;
      };

      // Format the query and update the input box value
      const formattedQuery = this.toTitleCase(rawQuery);
      this.queryInput.value = formattedQuery;

      // Update the header with the new query and location
      await this.updateResultsHeader(formattedQuery);

      // Pass the formatted query to the search request
      this.searchRequest.textQuery = formattedQuery;
      this.searchRequest.locationRestriction = this.map.getBounds();

      const selectedPrice = this.priceFilter.value;
      let priceLevels = [];
      switch (selectedPrice) {
        case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
        case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
        case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
        case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
        default: priceLevels = null; break;
      }
      this.searchRequest.priceLevels = priceLevels;

      const selectedRating = parseFloat(this.ratingFilter.value);
      this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
      this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
    }, 0);
  }

  // NEW: Method to update the sidebar header with geocoded location
  async updateResultsHeader(query) {
    try {
      const response = await this.geocoder.geocode({ location: this.map.getCenter() });
      if (response.results && response.results.length > 0) {
        const cityResult = response.results.find(r => r.types.includes('locality')) || response.results[0];
        const city = cityResult.address_components[0].long_name;
        this.resultsHeaderText.textContent = `${query} near ${city}`;
      } else {
        this.resultsHeaderText.textContent = `${query} near current map area`;
      }
    } catch (error) {
      console.error("Geocoding failed:", error);
      this.resultsHeaderText.textContent = `Results for ${query}`;
    }
  }
}

window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Vérifiez votre travail

Enregistrez votre fichier script.js et actualisez la page.

Vérifiez les fonctionnalités :

  • Saisissez pizza (en minuscules) dans le champ de recherche, puis cliquez sur "Rechercher". Le texte de la zone doit être remplacé par "Pizza", et l'en-tête de la barre latérale doit être remplacé par "Pizza près de New York".
  • Fais glisser la carte vers une autre ville, comme Boston, puis effectue une nouvelle recherche. L'en-tête devrait devenir "Pizzas à proximité de Boston".

9. Félicitations

Vous venez de créer une application de recherche locale complète et interactive qui combine la simplicité du Kit UI pour Places et la puissance des API JavaScript Google Maps Platform de base.

Ce que vous avez appris

  • Comment structurer une application de cartographie à l'aide d'une classe JavaScript pour gérer l'état et la logique.
  • Découvrez comment utiliser le Kit UI pour Places avec l'API Maps JavaScript pour développer rapidement une UI.
  • Découvrez comment ajouter et gérer de manière programmatique des repères avancés pour afficher des points d'intérêt personnalisés sur la carte.
  • Découvrez comment utiliser le service de géocodage pour convertir des coordonnées en adresses lisibles et améliorer ainsi l'expérience utilisateur.
  • Découvrez comment identifier et corriger les conditions de concurrence courantes dans une application interactive à l'aide d'indicateurs d'état et en vous assurant que les propriétés des composants sont correctement mises à jour.

Étape suivante

  • Découvrez comment personnaliser les repères avancés en modifiant leur couleur ou leur échelle, ou même en utilisant du code HTML personnalisé.
  • Découvrez la personnalisation de cartes dans Google Cloud pour adapter l'apparence de votre carte à votre marque.
  • Essayez d'ajouter la bibliothèque de dessins pour permettre aux utilisateurs de dessiner des formes sur la carte afin de définir des zones de recherche.
  • Aidez-nous à créer le contenu qui vous semble le plus utile en répondant à l'enquête suivante :

Quels autres ateliers de programmation souhaiteriez-vous voir ?

Visualisation des données sur les cartes En savoir plus sur la personnalisation du style de mes cartes Concevoir des interactions 3D dans Google Maps

Vous ne trouvez pas l'atelier de programmation qui vous intéresse le plus ? Demandez-le en décrivant un nouveau problème ici.