Crea un'app di ricerca locale con Places UI Kit

1. Prima di iniziare

Questo codelab ti insegna a creare un'applicazione di ricerca locale completamente interattiva utilizzando Places UI Kit di Google Maps Platform.

Uno screenshot dell'applicazione PlaceFinder completata, che mostra una mappa di New York con indicatori, una barra laterale con i risultati di ricerca e una scheda dei dettagli aperta.

Prerequisiti

  • Un progetto Google Cloud con le API e le credenziali necessarie configurate.
  • Conoscenza di base di HTML e CSS.
  • Conoscenza del moderno JavaScript.
  • Un browser web moderno, ad esempio l'ultima versione di Chrome.
  • Un editor di testo a tua scelta.

In questo lab proverai a:

  • Strutturare un'applicazione di mappatura utilizzando una classe JavaScript.
  • Utilizzare i componenti web per visualizzare una mappa
  • Utilizza l'elemento di ricerca di luoghi per eseguire e visualizzare i risultati di una ricerca di testo.
  • Crea e gestisci in modo programmatico indicatori di mappa AdvancedMarkerElement personalizzati.
  • Visualizza l'elemento Mostra dettagli del luogo quando un utente seleziona una località.
  • Utilizza l'API Geocoding per creare un'interfaccia dinamica e intuitiva.

Che cosa ti serve

  • Un progetto Google Cloud con la fatturazione abilitata
  • Una chiave API di Google Maps Platform
  • Un ID mappa
  • Le seguenti API abilitate:
    • API Maps JavaScript
    • Kit UI di Places
    • API Geocoding

2. Configurazione

Per il passaggio di attivazione successivo, devi abilitare l'API Maps JavaScript, Places UI Kit e l'API Geocoding.

Configurare Google Maps Platform

Se non hai ancora un account Google Cloud Platform e un progetto con la fatturazione abilitata, consulta la guida Guida introduttiva a Google Maps Platform per creare un account di fatturazione e un progetto.

  1. Nella console Cloud, fai clic sul menu a discesa del progetto e seleziona il progetto che vuoi utilizzare per questo codelab.

  1. Abilita le API e gli SDK di Google Maps Platform richiesti per questo codelab in Google Cloud Marketplace. Per farlo, segui i passaggi descritti in questo video o in questa documentazione.
  2. Genera una chiave API nella pagina Credenziali di Cloud Console. Puoi seguire i passaggi descritti in questo video o in questa documentazione. Tutte le richieste a Google Maps Platform richiedono una chiave API.

3. La shell dell'applicazione e una mappa funzionale

In questo primo passaggio, creeremo il layout visivo completo per la nostra applicazione e stabiliremo una struttura pulita basata sulle classi per il nostro JavaScript. In questo modo, avremo una base solida su cui costruire. Al termine di questa sezione, avrai una pagina con uno stile che mostra una mappa interattiva.

Crea il file HTML

Innanzitutto, crea un file denominato index.html. Questo file conterrà la struttura completa della nostra applicazione, inclusi l'intestazione, i filtri di ricerca, la barra laterale, il contenitore della mappa e i componenti web necessari.

Copia il seguente codice in index.html. Assicurati di sostituire YOUR_API_KEY_HERE con la tua chiave API di Google Maps Platform e DEMO_MAP_ID con il tuo ID mappa di 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>

Crea il file CSS

Successivamente, crea un file denominato style.css. Aggiungeremo ora tutti gli stili necessari per creare un look pulito e moderno fin dall'inizio. Questo CSS gestisce il layout generale, i colori, i caratteri e l'aspetto di tutti gli elementi dell'interfaccia utente.

Copia il seguente codice in 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);
}

Crea la classe dell'applicazione JavaScript

Infine, crea un file denominato script.js. Struttureremo la nostra applicazione all'interno di una classe JavaScript denominata PlaceFinderApp. In questo modo, il codice è organizzato e lo stato viene gestito in modo pulito.

Questo codice iniziale definirà la classe, troverà tutti gli elementi HTML nel constructor e creerà un metodo init() per caricare le librerie di Google Maps Platform.

Copia il seguente codice in 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();
});

Limitazioni relative alle chiavi API

Per far funzionare questo codelab, potresti dover aggiungere una nuova limitazione alla tua chiave API. Per ulteriori informazioni e indicazioni su come procedere, consulta Limitare le chiavi API.

Controllare il lavoro

Apri il file index.html nel browser web. Dovresti visualizzare una pagina con un'intestazione contenente una barra di ricerca e filtri, una barra laterale con il messaggio "I risultati di ricerca verranno visualizzati qui" e una mappa di grandi dimensioni centrata su New York City. In questa fase, i controlli di ricerca non sono ancora funzionanti.

4. Implementare una funzione di ricerca

In questa sezione, daremo vita alla nostra applicazione implementando la funzionalità di ricerca principale. Scriveremo il codice che viene eseguito quando un utente fa clic sul pulsante "Cerca". Creeremo questa funzione con le best practice fin dall'inizio per gestire le interazioni degli utenti in modo ottimale ed evitare bug comuni come le race condition.

Al termine di questo passaggio, potrai fare clic sul pulsante di ricerca e visualizzare un indicatore di caricamento mentre l'applicazione recupera i dati in background.

Creare il metodo di ricerca

Innanzitutto, definisci il metodo performSearch all'interno della classe PlaceFinderApp. Questa funzione sarà il fulcro della nostra logica di ricerca. Introdurremo anche una variabile di istanza, isSearchInProgress, che fungerà da "gatekeeper". In questo modo, l'utente non può avviare una nuova ricerca mentre una è già in corso, il che può causare errori.

La logica all'interno di performSearch potrebbe sembrare complessa, quindi la analizzeremo nel dettaglio:

  1. Innanzitutto, verifica se è già in corso una ricerca. In caso affermativo, non fa nulla.
  2. Imposta il flag isSearchInProgress su true per "bloccare" la funzione.
  3. Mostra l'indicatore di caricamento e prepara l'interfaccia utente per i nuovi risultati.
  4. Imposta la proprietà textQuery della richiesta di ricerca su null. Si tratta di un passaggio fondamentale che costringe il componente web a riconoscere che sta per arrivare una nuova richiesta.
  5. Utilizza un setTimeout con un ritardo di 0. Questa tecnica JavaScript standard pianifica l'esecuzione del resto del codice nell'attività successiva del browser, assicurandosi che il componente abbia elaborato prima il valore null. Anche se l'utente cerca esattamente la stessa cosa due volte, viene sempre attivata una nuova ricerca.

Aggiungere listener di eventi

Successivamente, dobbiamo chiamare il nostro metodo performSearch quando l'utente interagisce con l'app. Creeremo un nuovo metodo, attachEventListeners, per mantenere tutto il codice di gestione degli eventi in un unico posto. Per ora, aggiungiamo un listener per l'evento click del pulsante di ricerca. Aggiungeremo anche un segnaposto per un altro evento, gmp-load, che utilizzeremo nel passaggio successivo.

Aggiorna il file JavaScript

Aggiorna il file script.js con il seguente codice. Le sezioni nuove o modificate sono il metodo attachEventListeners e il metodo 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();
});

Controllare il lavoro

Salva il file script.js e aggiorna index.html nel browser. La pagina dovrebbe avere lo stesso aspetto di prima. Ora, fai clic sul pulsante "Cerca" nell'intestazione.

Dovresti vedere due cose:

  1. Il messaggio segnaposto "I risultati di ricerca verranno visualizzati qui" scompare.
  2. Viene visualizzata la rotellina di caricamento che continua a girare.

Il cursore continuerà a girare all'infinito perché non gli abbiamo ancora detto quando fermarsi. Lo faremo nella prossima sezione, quando mostreremo i risultati. Ciò conferma che la nostra funzione di ricerca viene attivata correttamente.

5. Visualizzare i risultati e aggiungere indicatori

Ora che l'attivazione della ricerca è funzionale, il passaggio successivo consiste nel visualizzare i risultati sullo schermo. Il codice in questa sezione collegherà la logica di ricerca alla UI. Una volta terminato il caricamento dei dati, l'elemento di ricerca di luoghi rilascia il "blocco" della ricerca, nasconde l'indicatore di caricamento e mostra un indicatore sulla mappa per ogni risultato.

Ascolta il completamento della ricerca

L'elemento di ricerca di luoghi attiva un evento gmp-load quando ha recuperato correttamente i dati. Questo è il segnale perfetto per elaborare i risultati.

Per prima cosa, aggiungi un listener di eventi per questo evento nel nostro metodo attachEventListeners.

Creare metodi di gestione dei marcatori

Successivamente, creeremo due nuovi metodi helper: clearMarkers e addMarkers.

  • clearMarkers() rimuoverà tutti i marcatori di una ricerca precedente.
  • addMarkers() verrà chiamato dal nostro ascoltatore gmp-load. Scorrerà l'elenco dei luoghi restituiti dalla ricerca e creerà un nuovo AdvancedMarkerElement per ciascuno. È qui che nasconderemo anche l'indicatore di caricamento e rilasceremo il blocco isSearchInProgress, completando il ciclo di ricerca.

Tieni presente che memorizziamo i marcatori in un oggetto (this.markers) utilizzando l'ID luogo come chiave. Questo è un modo per gestire i marcatori e ci consentirà di trovare un marcatore specifico in un secondo momento.

Infine, dobbiamo chiamare clearMarkers() all'inizio di ogni nuova ricerca. Il posto migliore per farlo è all'interno di performSearch.

Aggiorna il file JavaScript

Aggiorna il file script.js con i nuovi metodi e le modifiche apportate a attachEventListeners e 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();
});

Controllare il lavoro

Salva i file e aggiorna la pagina nel browser. Fai clic sul pulsante "Cerca".

Ora dovrebbe apparire per un momento l'indicatore di caricamento e poi scomparire. La barra laterale verrà compilata con un elenco di luoghi pertinenti al termine di ricerca e dovresti vedere gli indicatori corrispondenti sulla mappa. I marcatori non fanno ancora nulla quando vengono selezionati. Aggiungeremo l'interattività nella sezione successiva.

6. Attivare i filtri di ricerca e l'interattività degli elenchi

La nostra applicazione ora può mostrare i risultati di ricerca, ma non è ancora interattiva. In questa sezione, daremo vita a tutti i controlli utente. Attiveremo i filtri, consentiremo la ricerca con il tasto Invio e collegheremo gli elementi nell'elenco dei risultati alle posizioni corrispondenti sulla mappa.

Al termine di questo passaggio, l'applicazione risponderà completamente all'input dell'utente.

Attivare i filtri di ricerca

Innanzitutto, il metodo performSearch verrà aggiornato per leggere i valori di tutti i controlli dei filtri nell'intestazione. Per ogni filtro (prezzo, valutazione e "Aperto ora"), la proprietà corrispondente verrà impostata sull'oggetto searchRequest prima dell'esecuzione della ricerca.

Aggiungere listener di eventi per tutti i controlli

Successivamente, espanderemo il nostro metodo attachEventListeners. Aggiungeremo listener per l'evento change a ogni controllo del filtro, nonché un listener keydown all'input di ricerca per rilevare quando l'utente preme il tasto Invio. Tutti questi nuovi ascoltatori chiameranno il metodo performSearch.

Collegare l'elenco dei risultati alla mappa

Per creare un'esperienza fluida, se fai clic su un elemento nell'elenco dei risultati della barra laterale, la mappa deve essere centrata su quella posizione.

Un nuovo metodo, handleResultClick, ascolterà l'evento gmp-select, che viene attivato dall'elemento di ricerca di luoghi quando viene fatto clic su un elemento. Questa funzione trova la posizione del luogo associato e sposta la mappa in modo fluido.

Affinché funzioni, assicurati che l'attributo selectable sia presente nel componente gmp-place-search in 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>

Aggiorna il file JavaScript

Aggiorna il file script.js con il seguente codice completo. Questa versione include il nuovo metodo handleResultClick e la logica aggiornata in attachEventListeners e 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();
});

Controllare il lavoro

Salva il file script.js e aggiorna la pagina. L'applicazione ora dovrebbe essere altamente interattiva.

Verifica quanto segue:

  • La ricerca premendo "Invio" nella casella di ricerca funziona.
  • La modifica di uno qualsiasi dei filtri (Prezzo, Valutazione, Aperto ora) attiva una nuova ricerca e aggiorna i risultati.
  • Se fai clic su un elemento nell'elenco dei risultati della barra laterale, la mappa si sposta in modo fluido sulla posizione dell'elemento.

Nella sezione successiva implementeremo la scheda dei dettagli che viene visualizzata quando si fa clic su un indicatore.

7. Implementare l'elemento Place Details

La nostra applicazione è ora completamente interattiva, ma manca una funzionalità chiave: la possibilità di visualizzare maggiori informazioni su un luogo selezionato. In questa sezione implementeremo l'elemento Dettagli luogo che verrà visualizzato quando un utente fa clic su un indicatore sulla mappa o seleziona un elemento nell'elemento Ricerca luogo.

Creare un contenitore di schede dei dettagli riutilizzabile

Il modo più efficiente per visualizzare i dettagli dei luoghi sulla mappa è creare un singolo contenitore riutilizzabile. Utilizzeremo un AdvancedMarkerElement come contenitore. I suoi contenuti saranno il widget gmp-place-details-compact nascosto che abbiamo già in index.html.

Un nuovo metodo, initDetailsPopup, gestirà la creazione di questo indicatore riutilizzabile. Verrà creato una sola volta al caricamento dell'applicazione e inizierà nascosto. In questo metodo aggiungeremo anche un listener alla mappa principale, in modo che facendo clic in un punto qualsiasi della mappa la scheda dei dettagli venga nascosta.

Aggiornare il comportamento del clic sul marcatore

Successivamente, dobbiamo aggiornare cosa succede quando un utente fa clic su un segnaposto. Il listener 'click' all'interno del metodo addMarkers ora sarà responsabile della visualizzazione della scheda dei dettagli.

Quando viene fatto clic su un indicatore, il listener:

  1. Sposta la mappa fino alla posizione del segnaposto.
  2. Aggiorna la scheda dei dettagli con le informazioni relative a quel luogo specifico.
  3. Posiziona la scheda dei dettagli nella posizione del marcatore e rendila visibile.

Collega il clic sull'elenco al clic sul marcatore

Infine, aggiorneremo il metodo handleResultClick. Anziché spostare semplicemente la mappa, ora attiverà a livello di programmazione l'evento click sul relativo indicatore. Si tratta di un pattern efficace che ci consente di riutilizzare la stessa logica per entrambe le interazioni, mantenendo il codice pulito e gestibile.

Aggiorna il file JavaScript

Aggiorna il file script.js con il seguente codice. Le sezioni nuove o modificate sono il metodo initDetailsPopup e i metodi aggiornati addMarkers e handleResultClick.

// 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();
});

Controllare il lavoro

Salva il file script.js e aggiorna la pagina. L'applicazione ora dovrebbe mostrare i dettagli su richiesta.

Verifica quanto segue:

  • Se fai clic su un indicatore sulla mappa, la mappa viene centrata e sopra l'indicatore si apre una scheda dettagli con uno stile.
  • Fare clic su un elemento nell'elenco dei risultati della barra laterale ha lo stesso effetto.
  • Se fai clic sulla mappa lontano dalla scheda, questa si chiude.
  • L'avvio di una nuova ricerca chiude anche le schede dei dettagli aperte.

8. Aggiungere gli ultimi ritocchi

La nostra applicazione è ora completamente funzionante, ma possiamo aggiungere alcuni tocchi finali per migliorare ulteriormente l'esperienza utente. In questa sezione finale, implementeremo due funzionalità chiave: un'intestazione dinamica che fornisce un contesto migliore per i risultati di ricerca e la formattazione automatica della query di ricerca dell'utente.

Creare un'intestazione dei risultati dinamica

Al momento, l'intestazione della barra laterale indica sempre "Risultati". Possiamo renderlo più informativo aggiornandolo in modo che rifletta la ricerca attuale. Ad esempio, "Hamburger vicino a New York".

Per farlo, utilizzeremo l'API Geocoding per convertire le coordinate del centro della mappa in una posizione leggibile, ad esempio il nome di una città. Un nuovo metodo async, updateResultsHeader, gestirà questa logica. Verrà chiamato ogni volta che viene eseguita una ricerca.

Formatta la query di ricerca dell'utente

Per garantire che l'interfaccia utente abbia un aspetto pulito e coerente, formattiamo automaticamente il termine di ricerca dell'utente in "Title Case" (ad es. "burger restaurant" diventa "Burger Restaurant"). Una funzione helper, toTitleCase, gestirà questa trasformazione. Il metodo performSearch verrà aggiornato per utilizzare questa funzione sull'input dell'utente prima di eseguire la ricerca e aggiornare l'intestazione.

Aggiorna il file JavaScript

Aggiorna il file script.js con la versione finale del codice. Sono inclusi i nuovi metodi toTitleCase e updateResultsHeader e il metodo performSearch aggiornato che li integra.

// 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();
});

Controllare il lavoro

Salva il file script.js e aggiorna la pagina.

Verifica le funzionalità:

  • Digita pizza (tutto in minuscolo) nella casella di ricerca e fai clic su Cerca. Il testo nella casella dovrebbe cambiare in "Pizza" e l'intestazione nella barra laterale dovrebbe aggiornarsi con la dicitura "Pizzerie vicino a New York".
  • Sposta la mappa su un'altra città, ad esempio Boston, e cerca di nuovo. L'intestazione dovrebbe aggiornarsi a "Pizzeria vicino a Boston".

9. Complimenti

Hai creato correttamente un'applicazione di ricerca locale completa e interattiva che combina la semplicità di Places UI Kit con la potenza delle API JavaScript di base di Google Maps Platform.

Che cosa hai imparato

  • Come strutturare un'applicazione di mapping utilizzando una classe JavaScript per gestire lo stato e la logica.
  • Come utilizzare Places UI Kit con l'API Google Maps JavaScript per lo sviluppo rapido dell'interfaccia utente.
  • Come aggiungere e gestire in modo programmatico i marker avanzati per visualizzare punti di interesse personalizzati sulla mappa.
  • Come utilizzare il servizio di geocodifica per trasformare le coordinate in indirizzi leggibili per una migliore esperienza utente.
  • Come identificare e correggere le condizioni di competizione comuni in un'applicazione interattiva utilizzando i flag di stato e assicurandosi che le proprietà dei componenti vengano aggiornate correttamente.

Passaggi successivi

Quali altri codelab vorresti vedere?

Visualizzazione dei dati sulle mappe Scopri di più sulla personalizzazione dello stile delle mie mappe Creare edifici per interazioni 3D nelle mappe

Non riesci a trovare il codelab che ti interessa di più? Richiedilo con un nuovo problema qui.