Lokale Suchanwendung mit dem Places UI Kit erstellen

1. Hinweis

In diesem Codelab erfahren Sie, wie Sie mit dem Places UI Kit der Google Maps Platform eine vollständig interaktive Anwendung für die lokale Suche erstellen.

Ein Screenshot der fertigen PlaceFinder-Anwendung mit einer Karte von New York mit Markierungen, einer Seitenleiste mit Suchergebnissen und einer geöffneten Detailkarte.

Vorbereitung

  • Ein Google Cloud-Projekt mit den erforderlichen APIs und konfigurierten Anmeldedaten.
  • Grundkenntnisse in HTML und CSS.
  • Kenntnisse von modernem JavaScript
  • Einen modernen Webbrowser, z. B. die aktuelle Version von Chrome.
  • Ein Texteditor Ihrer Wahl.

Aufgaben

  • Eine Zuordnungsanwendung mit einer JavaScript-Klasse strukturieren
  • Karte mit Webkomponenten anzeigen
  • Mit dem Place Search-Element können Sie eine Textsuche durchführen und die Ergebnisse anzeigen.
  • Benutzerdefinierte AdvancedMarkerElement-Kartenmarkierungen programmatisch erstellen und verwalten.
  • Das Element „Ortsdetails“ anzeigen, wenn ein Nutzer einen Ort auswählt.
  • Mit der Geocoding API können Sie eine dynamische und nutzerfreundliche Benutzeroberfläche erstellen.

Voraussetzungen

  • Ein Google Cloud-Projekt mit aktivierter Abrechnung
  • Ein Google Maps Platform API-Schlüssel
  • Eine Karten-ID
  • Die folgenden APIs sind aktiviert:
    • Maps JavaScript API
    • UI-Kit für Places
    • Geocoding API

2. Einrichten

Im nächsten Schritt müssen Sie die Maps JavaScript API, das Places UI Kit und die Geocoding API aktivieren.

Google Maps Platform einrichten

Wenn Sie noch kein Google Cloud-Konto und kein Projekt mit aktivierter Abrechnung haben, lesen Sie bitte den Leitfaden Erste Schritte mit Google Maps Platform, um ein Rechnungskonto und ein Projekt zu erstellen.

  1. Klicken Sie in der Cloud Console auf das Drop-down-Menü für das Projekt und wählen Sie das Projekt aus, das Sie für dieses Codelab verwenden möchten.

  1. Aktivieren Sie die für dieses Codelab erforderlichen APIs und SDKs der Google Maps Platform im Google Cloud Marketplace. Folgen Sie dazu der Anleitung in diesem Video oder dieser Dokumentation.
  2. Generieren Sie einen API-Schlüssel in der Cloud Console auf der Seite Anmeldedaten. Folgen Sie dazu dieser Anleitung oder dieser Dokumentation. Für alle Anfragen an die Google Maps Platform ist ein API-Schlüssel erforderlich.

3. Die Anwendungsshell und eine funktionierende Karte

In diesem ersten Schritt erstellen wir das vollständige visuelle Layout für unsere Anwendung und richten eine übersichtliche, klassenbasierte Struktur für unser JavaScript ein. Das ist eine solide Grundlage, auf der wir aufbauen können. Am Ende dieses Abschnitts haben Sie eine Seite mit benutzerdefinierten Stilen, auf der eine interaktive Karte angezeigt wird.

HTML-Datei erstellen

Erstellen Sie zuerst eine Datei mit dem Namen index.html. Diese Datei enthält die vollständige Struktur unserer Anwendung, einschließlich des Headers, der Suchfilter, der Seitenleiste, des Kartencontainers und der erforderlichen Webkomponenten.

Kopieren Sie den folgenden Code in index.html. Ersetzen Sie YOUR_API_KEY_HERE durch Ihren eigenen Google Maps Platform API-Schlüssel und DEMO_MAP_ID durch Ihre eigene Google Maps Platform-Karten-ID.

<!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>

CSS-Datei erstellen

Erstellen Sie als Nächstes eine Datei mit dem Namen style.css. Wir fügen jetzt alle erforderlichen Formatierungen hinzu, um von Anfang an ein sauberes, modernes Erscheinungsbild zu schaffen. Dieses CSS steuert das allgemeine Layout, die Farben, die Schriftarten und das Erscheinungsbild aller UI-Elemente.

Kopieren Sie den folgenden Code 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);
}

JavaScript-Anwendungsklasse erstellen

Erstellen Sie schließlich eine Datei mit dem Namen script.js. Wir strukturieren unsere Anwendung in einer JavaScript-Klasse namens PlaceFinderApp. So bleibt unser Code übersichtlich und der Status wird sauber verwaltet.

In diesem ersten Code wird die Klasse definiert, alle HTML-Elemente im constructor gefunden und eine init()-Methode zum Laden der Google Maps Platform-Bibliotheken erstellt.

Kopieren Sie den folgenden Code 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();
});

Einschränkungen für API-Schlüssel

Möglicherweise müssen Sie Ihrem API-Schlüssel eine neue Einschränkung hinzufügen, damit dieses Codelab funktioniert. Weitere Informationen und Anleitungen dazu finden Sie unter API-Schlüssel einschränken.

Ergebnisse prüfen

Öffnen Sie die Datei index.html in Ihrem Webbrowser. Sie sollten eine Seite mit einer Kopfzeile mit einer Suchleiste und Filtern, einer Seitenleiste mit der Meldung „Ihre Suchergebnisse werden hier angezeigt“ und einer großen Karte sehen, die auf New York City zentriert ist. Zu diesem Zeitpunkt sind die Suchsteuerelemente noch nicht funktionsfähig.

4. Suchfunktion implementieren

In diesem Abschnitt implementieren wir die Kernsuchfunktion, um unsere Anwendung zum Leben zu erwecken. Wir schreiben den Code, der ausgeführt wird, wenn ein Nutzer auf die Schaltfläche „Suchen“ klickt. Wir werden diese Funktion von Anfang an mit Best Practices entwickeln, um Nutzerinteraktionen reibungslos zu verarbeiten und häufige Fehler wie Race Conditions zu vermeiden.

Am Ende dieses Schritts können Sie auf die Suchschaltfläche klicken und sehen, wie ein Ladesymbol angezeigt wird, während die Anwendung Daten im Hintergrund abruft.

Suchmethode erstellen

Definieren Sie zuerst die Methode performSearch in der Klasse PlaceFinderApp. Diese Funktion ist das Herzstück unserer Suchlogik. Außerdem führen wir eine Instanzvariable, isSearchInProgress, als „Gatekeeper“ ein. So wird verhindert, dass der Nutzer eine neue Suche startet, während eine andere bereits läuft. Das kann zu Fehlern führen.

Die Logik in performSearch mag komplex erscheinen. Deshalb sehen wir sie uns genauer an:

  1. Zuerst wird geprüft, ob bereits eine Suche läuft. Wenn ja, passiert nichts.
  2. Das Flag isSearchInProgress wird auf true gesetzt, um die Funktion zu „sperren“.
  3. Der Ladespinner wird angezeigt und die Benutzeroberfläche wird für neue Ergebnisse vorbereitet.
  4. Dadurch wird die textQuery-Property der Suchanfrage auf null festgelegt. Dies ist ein wichtiger Schritt, damit die Webkomponente erkennt, dass eine neue Anfrage eingeht.
  5. Sie verwendet eine setTimeout mit einer 0-Verzögerung. Mit dieser Standard-JavaScript-Technik wird der Rest unseres Codes für die Ausführung in der nächsten Browseraufgabe geplant. So wird sichergestellt, dass die Komponente zuerst den null-Wert verarbeitet hat. Auch wenn der Nutzer zweimal nach genau demselben sucht, wird immer eine neue Suche ausgelöst.

Event-Listener hinzufügen

Als Nächstes müssen wir unsere performSearch-Methode aufrufen, wenn der Nutzer mit der App interagiert. Wir erstellen eine neue Methode, attachEventListeners, um den gesamten Code für die Ereignisverarbeitung an einem Ort zu speichern. Wir fügen jetzt einen Listener für das click-Ereignis der Suchschaltfläche hinzu. Außerdem fügen wir einen Platzhalter für ein weiteres Ereignis, gmp-load, hinzu, das wir im nächsten Schritt verwenden.

JavaScript-Datei aktualisieren

Aktualisieren Sie Ihre script.js-Datei mit dem folgenden Code. Die neuen oder geänderten Abschnitte sind die Methode attachEventListeners und die Methode 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();
});

Ergebnisse prüfen

Speichern Sie die Datei script.js und aktualisieren Sie index.html in Ihrem Browser. Die Seite sollte genauso aussehen wie zuvor. Klicken Sie nun in der Kopfzeile auf die Schaltfläche „Suchen“.

Sie sollten zwei Dinge sehen:

  1. Die Platzhalter-Meldung „Ihre Suchergebnisse werden hier angezeigt“ verschwindet.
  2. Das rotierende Ladesymbol wird angezeigt und dreht sich weiter.

Der Spinner dreht sich endlos, weil wir noch nicht festgelegt haben, wann er anhalten soll. Das werden wir im nächsten Abschnitt tun, wenn wir die Ergebnisse präsentieren. So wird bestätigt, dass unsere Suchfunktion richtig ausgelöst wird.

5. Ergebnisse anzeigen und Markierungen hinzufügen

Nachdem der Suchauslöser funktioniert, müssen die Ergebnisse auf dem Bildschirm angezeigt werden. Der Code in diesem Abschnitt verbindet die Suchlogik mit der Benutzeroberfläche. Sobald das Place Search Element das Laden der Daten abgeschlossen hat, wird die Suchsperre aufgehoben, der Lade-Spinner ausgeblendet und für jedes Ergebnis eine Markierung auf der Karte angezeigt.

Auf das Ende der Suche warten

Das Place Search Element löst ein gmp-load-Ereignis aus, wenn Daten erfolgreich abgerufen wurden. Das ist das perfekte Signal für uns, um die Ergebnisse zu verarbeiten.

Fügen Sie zuerst einen Event-Listener für dieses Ereignis in unserer attachEventListeners-Methode hinzu.

Methoden für die Markierungsbehandlung erstellen

Als Nächstes erstellen wir zwei neue Hilfsmethoden: clearMarkers und addMarkers.

  • Mit clearMarkers() werden alle Markierungen aus einer vorherigen Suche entfernt.
  • addMarkers() wird von unserem gmp-load-Listener aufgerufen. Die Liste der von der Suche zurückgegebenen Orte wird durchlaufen und für jeden Ort wird ein neues AdvancedMarkerElement-Objekt erstellt. Hier wird auch der Ladespinner ausgeblendet und die isSearchInProgress-Sperre aufgehoben, wodurch der Suchvorgang abgeschlossen wird.

Beachten Sie, dass wir Markierungen in einem Objekt (this.markers) speichern und die Orts-ID als Schlüssel verwenden. So können wir Markierungen verwalten und später eine bestimmte Markierung finden.

Schließlich müssen wir clearMarkers() zu Beginn jeder neuen Suche aufrufen. Am besten ist es, wenn Sie das in performSearch tun.

JavaScript-Datei aktualisieren

Aktualisieren Sie die Datei script.js mit den neuen Methoden und den Änderungen an attachEventListeners und 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();
});

Ergebnisse prüfen

Speichern Sie Ihre Dateien und aktualisieren Sie die Seite in Ihrem Browser. Klicke auf "Suchen".

Das Ladesymbol sollte nun kurz angezeigt und dann wieder ausgeblendet werden. In der Seitenleiste wird eine Liste mit Orten angezeigt, die für den Suchbegriff relevant sind. Auf der Karte sollten entsprechende Markierungen zu sehen sein. Wenn Sie auf die Markierungen klicken, passiert noch nichts. Wir fügen die Interaktivität im nächsten Abschnitt hinzu.

6. Suchfilter und Listeninteraktivität aktivieren

In unserer Anwendung können jetzt Suchergebnisse angezeigt werden, sie ist aber noch nicht interaktiv. In diesem Abschnitt werden alle Nutzersteuerungen zum Leben erweckt. Wir aktivieren die Filter, ermöglichen die Suche mit der Eingabetaste und verknüpfen die Elemente in der Ergebnisliste mit den entsprechenden Orten auf der Karte.

Nach Abschluss dieses Schritts reagiert die Anwendung vollständig auf Nutzereingaben.

Suchfilter aktivieren

Zuerst wird die Methode performSearch aktualisiert, um die Werte aus allen Filtersteuerelementen in der Kopfzeile zu lesen. Für jeden Filter (Preis, Bewertung und „Jetzt geöffnet“) wird die entsprechende Eigenschaft für das searchRequest-Objekt festgelegt, bevor die Suche ausgeführt wird.

Event-Listener für alle Steuerelemente hinzufügen

Als Nächstes erweitern wir die attachEventListeners-Methode. Wir fügen Listener für das change-Ereignis in jedem Filtersteuerelement sowie einen keydown-Listener in der Sucheingabe hinzu, um zu erkennen, wenn der Nutzer die Eingabetaste drückt. Alle diese neuen Listener rufen die Methode performSearch auf.

Ergebnisliste mit der Karte verbinden

Um eine nahtlose Nutzererfahrung zu ermöglichen, sollte die Karte auf den entsprechenden Ort fokussiert werden, wenn ein Nutzer in der Seitenleiste auf ein Element in der Ergebnisliste klickt.

Eine neue Methode, handleResultClick, wartet auf das gmp-select-Ereignis, das vom Place Search Element ausgelöst wird, wenn auf ein Element geklickt wird. Mit dieser Funktion wird der Standort des zugehörigen Orts ermittelt und die Karte wird sanft dorthin geschwenkt.

Dazu muss das Attribut selectable in Ihrer gmp-place-search-Komponente in index.html vorhanden sein.

<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>

JavaScript-Datei aktualisieren

Aktualisieren Sie Ihre script.js-Datei mit dem folgenden vollständigen Code. Diese Version enthält die neue Methode handleResultClick und die aktualisierte Logik in attachEventListeners und 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();
});

Ergebnisse prüfen

Speichern Sie die Datei script.js und aktualisieren Sie die Seite. Die Anwendung sollte jetzt sehr interaktiv sein.

Gehen Sie so vor:

  • Die Suche durch Drücken der Eingabetaste im Suchfeld funktioniert.
  • Wenn Sie einen der Filter (Preis, Bewertung, Jetzt geöffnet) ändern, wird eine neue Suche gestartet und die Ergebnisse werden aktualisiert.
  • Wenn Sie in der Seitenleiste auf ein Element in der Ergebnisliste klicken, wird die Karte jetzt sanft zum Standort dieses Elements geschwenkt.

Im nächsten Abschnitt implementieren wir die Detailkarte, die angezeigt wird, wenn auf eine Markierung geklickt wird.

7. „Place Details“-Element implementieren

Unsere Anwendung ist jetzt vollständig interaktiv, aber es fehlt eine wichtige Funktion: die Möglichkeit, weitere Informationen zu einem ausgewählten Ort aufzurufen. In diesem Abschnitt implementieren wir das Element „Ortsdetails“, das angezeigt wird, wenn ein Nutzer auf eine Markierung auf der Karte klickt oder ein Element im Element „Ortssuche“ auswählt.

Wiederverwendbaren Container für Detailkarten erstellen

Am effizientesten lassen sich Ortsdetails auf der Karte anzeigen, wenn Sie einen einzelnen, wiederverwendbaren Container erstellen. Wir verwenden einen AdvancedMarkerElement als Container. Der Inhalt ist das ausgeblendete gmp-place-details-compact-Widget, das wir bereits in unserem index.html haben.

Eine neue Methode, initDetailsPopup, übernimmt das Erstellen dieser wiederverwendbaren Markierung. Sie wird einmal beim Laden der Anwendung erstellt und ist anfangs ausgeblendet. Wir fügen in dieser Methode auch einen Listener für die Hauptkarte hinzu, damit die Detailkarte ausgeblendet wird, wenn Sie auf eine beliebige Stelle auf der Karte klicken.

Klickverhalten für Markierungen aktualisieren

Als Nächstes müssen wir festlegen, was passiert, wenn ein Nutzer auf eine Ortsmarkierung klickt. Der 'click'-Listener in der addMarkers-Methode ist jetzt für das Anzeigen der Detailkarte verantwortlich.

Wenn auf eine Markierung geklickt wird, führt der Listener folgende Aktionen aus:

  1. Schwenken Sie die Karte zur Position der Markierung.
  2. Aktualisieren Sie die Detailkarte mit den Informationen für diesen Ort.
  3. Positionieren Sie die Detailkarte am Standort der Markierung und machen Sie sie sichtbar.

Klick auf Liste mit Klick auf Markierung verknüpfen

Schließlich aktualisieren wir die Methode handleResultClick. Anstatt die Karte nur zu verschieben, wird jetzt programmatisch das click-Ereignis für die entsprechende Markierung ausgelöst. Dieses leistungsstarke Muster ermöglicht es uns, dieselbe Logik für beide Interaktionen wiederzuverwenden, wodurch unser Code sauber und wartungsfreundlich bleibt.

JavaScript-Datei aktualisieren

Aktualisieren Sie Ihre script.js-Datei mit dem folgenden Code. Die neuen oder geänderten Abschnitte sind die Methode initDetailsPopup und die aktualisierten Methoden addMarkers und 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();
});

Ergebnisse prüfen

Speichern Sie die Datei script.js und aktualisieren Sie die Seite. Die Anwendung sollte jetzt Details auf Anfrage anzeigen.

Gehen Sie so vor:

  • Wenn Sie auf eine Markierung auf der Karte klicken, wird die Karte jetzt zentriert und eine formatierte Detailkarte über der Markierung geöffnet.
  • Wenn Sie in der Seitenleiste auf ein Element in der Ergebnisliste klicken, passiert genau dasselbe.
  • Wenn Sie auf die Karte außerhalb der Karte klicken, wird sie geschlossen.
  • Wenn Sie eine neue Suche starten, wird auch jede geöffnete Detailkarte geschlossen.

8. Letzte Korrekturen vornehmen

Unsere Anwendung ist jetzt voll funktionsfähig, aber es gibt noch ein paar letzte Details, die wir hinzufügen können, um die Nutzerfreundlichkeit noch weiter zu verbessern. In diesem letzten Abschnitt implementieren wir zwei wichtige Funktionen: eine dynamische Kopfzeile, die einen besseren Kontext für die Suchergebnisse bietet, und eine automatische Formatierung für die Suchanfrage des Nutzers.

Dynamische Überschrift für Ergebnisse erstellen

Derzeit lautet die Überschrift der Seitenleiste immer „Ergebnisse“. Wir können die Informationen verbessern, indem wir sie an die aktuelle Suche anpassen. Beispiel: „Burger in der Nähe von New York“

Dazu verwenden wir die Geocoding API, um die Koordinaten des Kartenmittelpunkts in einen menschenlesbaren Standort wie einen Städtenamen umzuwandeln. Diese Logik wird von der neuen async-Methode updateResultsHeader verarbeitet. Sie wird jedes Mal aufgerufen, wenn eine Suche durchgeführt wird.

Suchanfrage des Nutzers formatieren

Damit die Benutzeroberfläche einheitlich aussieht, formatieren wir den Suchbegriff des Nutzers automatisch in „Title Case“ (z.B. „burger restaurant“ wird zu „Burger Restaurant“. Eine Hilfsfunktion, toTitleCase, übernimmt diese Transformation. Die performSearch-Methode wird aktualisiert, um diese Funktion für die Eingabe des Nutzers zu verwenden, bevor die Suche durchgeführt und die Kopfzeile aktualisiert wird.

JavaScript-Datei aktualisieren

Aktualisieren Sie die script.js-Datei mit der endgültigen Version des Codes. Dazu gehören die neuen Methoden toTitleCase und updateResultsHeader sowie die aktualisierte Methode performSearch, in die sie integriert sind.

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

Ergebnisse prüfen

Speichern Sie die Datei script.js und aktualisieren Sie die Seite.

Funktionen prüfen:

  • Geben Sie pizza (alles klein geschrieben) in das Suchfeld ein und klicken Sie auf „Suchen“. Der Text im Feld sollte sich in „Pizza“ ändern und die Überschrift in der Seitenleiste sollte zu „Pizza in der Nähe von New York“ aktualisiert werden.
  • Schwenken Sie die Karte zu einer anderen Stadt, z. B. Boston, und suchen Sie noch einmal. Die Überschrift sollte zu „Pizza in der Nähe von Boston“ aktualisiert werden.

9. Glückwunsch

Sie haben erfolgreich eine vollständige, interaktive lokale Suchanwendung erstellt, die die Einfachheit des Places UI Kit mit der Leistungsfähigkeit der wichtigsten JavaScript-APIs der Google Maps Platform kombiniert.

Das haben Sie gelernt

  • So strukturieren Sie eine Mapping-Anwendung mit einer JavaScript-Klasse, um Status und Logik zu verwalten.
  • So verwenden Sie das Places UI Kit mit der Google Maps JavaScript API für die schnelle Entwicklung von Benutzeroberflächen.
  • Erweiterte Markierungen programmatisch hinzufügen und verwalten, um benutzerdefinierte POIs auf der Karte anzuzeigen.
  • So verwenden Sie den Geocoding Service, um Koordinaten in visuell lesbare Adressen umzuwandeln und so die Nutzerfreundlichkeit zu verbessern.
  • Hier erfahren Sie, wie Sie häufige Race Conditions in einer interaktiven Anwendung mithilfe von Status-Flags identifizieren und beheben und dafür sorgen, dass Komponentenattribute richtig aktualisiert werden.

Nächste Schritte

Welche anderen Codelabs würden Sie sich wünschen?

Datenvisualisierung auf Karten Weitere Informationen zum Anpassen des Stils von Karten 3D-Interaktionen in Karten entwickeln

Sie können das Codelab, das Sie am meisten interessiert, nicht finden? Hier können Sie sie mit einem neuen Problem anfordern.