Criar um app de pesquisa local com o Kit de Interface do Places

1. Antes de começar

Este codelab ensina a criar um aplicativo de pesquisa local totalmente interativo usando o Kit de Interface do Places da Plataforma Google Maps.

Captura de tela do aplicativo PlaceFinder concluído, mostrando um mapa de Nova York com marcadores, uma barra lateral com resultados da pesquisa e um card de detalhes aberto.

Pré-requisitos

  • Um projeto do Google Cloud com as APIs e credenciais necessárias configuradas.
  • Ter conhecimento básico de HTML e CSS.
  • Conhecimento de JavaScript moderno.
  • Um navegador da Web moderno, como a versão mais recente do Chrome.
  • Um editor de texto de sua preferência.

Atividades deste laboratório

  • Estruturar um aplicativo de mapeamento usando uma classe JavaScript.
  • Usar componentes da Web para mostrar um mapa
  • Use o elemento de pesquisa de lugar para realizar e mostrar os resultados de uma pesquisa de texto.
  • Crie e gerencie marcadores de mapa AdvancedMarkerElement personalizados de maneira programática.
  • Mostrar o elemento de detalhes do lugar quando um usuário selecionar um local.
  • Use a API Geocoding para criar uma interface dinâmica e fácil de usar.

O que é necessário

  • Tenha um projeto do Google Cloud com o faturamento ativado.
  • Uma chave de API da Plataforma Google Maps
  • Um ID do mapa
  • As seguintes APIs ativadas:
    • API Maps JavaScript
    • Kit de interface do Google Places
    • API Geocoding

2. Começar a configuração

Para a etapa de ativação a seguir, é necessário ativar a API Maps JavaScript, o kit de interface do usuário Places e a API Geocoding.

Configurar a Plataforma Google Maps

Caso você ainda não tenha uma conta do Google Cloud Platform e um projeto com faturamento ativado, veja como criá-los no guia da Plataforma Google Maps.

  1. No Console do Cloud, clique no menu suspenso do projeto e selecione o projeto que você quer usar neste codelab.

  1. Ative as APIs e os SDKs da Plataforma Google Maps necessários para este codelab no Google Cloud Marketplace. Para fazer isso, siga as etapas descritas neste vídeo ou nesta documentação.
  2. Gere uma chave de API na página Credenciais do Console do Cloud. Siga as etapas indicadas neste vídeo ou nesta documentação. Todas as solicitações à Plataforma Google Maps exigem uma chave de API.

3. O shell do aplicativo e um mapa funcional

Nesta primeira etapa, vamos criar o layout visual completo do aplicativo e estabelecer uma estrutura limpa baseada em classes para o JavaScript. Isso nos dá uma base sólida para construir. Ao final desta seção, você terá uma página estilizada mostrando um mapa interativo.

Criar o arquivo HTML

Primeiro, crie um arquivo chamado index.html. Esse arquivo vai conter a estrutura completa do nosso aplicativo, incluindo o cabeçalho, filtros de pesquisa, barra lateral, contêiner do mapa e os componentes da Web necessários.

Copie o seguinte código em index.html. Substitua YOUR_API_KEY_HERE pela sua chave de API da Plataforma Google Maps e DEMO_MAP_ID pelo ID do mapa da Plataforma Google Maps.

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

Criar o arquivo CSS

Em seguida, crie um arquivo chamado style.css. Vamos adicionar todo o estilo necessário agora para estabelecer uma aparência limpa e moderna desde o início. Esse CSS processa o layout geral, as cores, as fontes e a aparência de todos os elementos da interface.

Copie o seguinte código em 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);
}

Criar a classe de aplicativo JavaScript

Por fim, crie um arquivo chamado script.js. Vamos estruturar nosso aplicativo em uma classe JavaScript chamada PlaceFinderApp. Isso mantém nosso código organizado e gerencia o estado de maneira limpa.

Esse código inicial vai definir a classe, encontrar todos os elementos HTML no constructor e criar um método init() para carregar as bibliotecas da Plataforma Google Maps.

Copie o seguinte código em 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();
});

Restrições da chave de API

Talvez seja necessário adicionar uma nova restrição à sua chave de API para que este codelab funcione. Consulte Restringir suas chaves de API para mais informações e orientações sobre como fazer isso.

Confira suas tarefas

Abra o arquivo index.html no navegador da Web. Você vai encontrar uma página com um cabeçalho que contém uma barra de pesquisa e filtros, uma barra lateral com a mensagem "Seus resultados da pesquisa vão aparecer aqui" e um mapa grande centralizado na cidade de Nova York. Nessa etapa, os controles de pesquisa ainda não estão funcionando.

4. Implementar uma função de pesquisa

Nesta seção, vamos dar vida ao nosso aplicativo implementando a funcionalidade principal de pesquisa. Vamos escrever o código que é executado quando um usuário clica no botão "Pesquisar". Vamos criar essa função com práticas recomendadas desde o início para processar as interações do usuário de maneira adequada e evitar bugs comuns, como condições de corrida.

Ao final desta etapa, você poderá clicar no botão de pesquisa e ver um spinner de carregamento aparecer enquanto o aplicativo busca dados em segundo plano.

Criar o método de pesquisa

Primeiro, defina o método performSearch dentro da classe PlaceFinderApp. Essa função será a essência da nossa lógica de pesquisa. Também vamos apresentar uma variável de instância, isSearchInProgress, para atuar como um "gatekeeper". Isso impede que o usuário inicie uma nova pesquisa enquanto uma já está em andamento, o que pode causar erros.

A lógica dentro de performSearch pode parecer complexa, então vamos explicar:

  1. Primeiro, ela verifica se uma pesquisa já está em andamento. Se for, nada acontece.
  2. Ele define a flag isSearchInProgress como true para "bloquear" a função.
  3. Ele mostra o spinner de carregamento e prepara a interface para novos resultados.
  4. Ele define a propriedade textQuery da solicitação de pesquisa como null. Essa é uma etapa crucial que força o componente da Web a reconhecer que uma nova solicitação está chegando.
  5. Ele usa um setTimeout com um atraso de 0. Essa técnica padrão do JavaScript agenda o restante do nosso código para ser executado na próxima tarefa do navegador, garantindo que o componente tenha processado o valor null primeiro. Mesmo que o usuário pesquise exatamente a mesma coisa duas vezes, uma nova pesquisa sempre será acionada.

Adicione listeners de eventos

Em seguida, precisamos chamar o método performSearch quando o usuário interage com o app. Vamos criar um novo método, attachEventListeners, para manter todo o código de processamento de eventos em um só lugar. Por enquanto, vamos adicionar um listener ao evento click do botão de pesquisa. Também vamos adicionar um marcador de posição para outro evento, gmp-load, que usaremos na próxima etapa.

Atualizar o arquivo JavaScript

Atualize o arquivo script.js com o seguinte código. As seções novas ou alteradas são os métodos 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();

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

Confira suas tarefas

Salve o arquivo script.js e atualize index.html no navegador. A página vai parecer a mesma de antes. Agora, clique no botão "Pesquisar" no cabeçalho.

Duas coisas vão acontecer:

  1. A mensagem de marcador de posição "Os resultados da pesquisa vão aparecer aqui" desaparece.
  2. O ícone de carregamento aparece e continua girando.

O spinner vai girar para sempre porque ainda não dissemos quando ele deve parar. Vamos fazer isso na próxima seção, quando mostrarmos os resultados. Isso confirma que nossa função de pesquisa está sendo acionada corretamente.

5. Mostrar resultados e adicionar marcadores

Agora que o gatilho de pesquisa está funcionando, a próxima tarefa é mostrar os resultados na tela. O código nesta seção vai conectar a lógica de pesquisa à interface. Quando o elemento de pesquisa de lugar terminar de carregar os dados, ele vai liberar o "bloqueio" da pesquisa, ocultar o ícone de carregamento e mostrar um marcador no mapa para cada resultado.

Ouvir a conclusão da pesquisa

O elemento de pesquisa de lugar aciona um evento gmp-load quando busca dados com sucesso. Esse é o indicador perfeito para processarmos os resultados.

Primeiro, adicione um listener para esse evento no método attachEventListeners.

Criar métodos de processamento de marcadores

Em seguida, vamos criar dois novos métodos auxiliares: clearMarkers e addMarkers.

  • clearMarkers() remove todos os marcadores de uma pesquisa anterior.
  • O addMarkers() será chamado pelo nosso listener gmp-load. Ele vai percorrer a lista de lugares retornados pela pesquisa e criar um novo AdvancedMarkerElement para cada um. É aqui também que vamos ocultar o ícone de carregamento e liberar o bloqueio isSearchInProgress, concluindo o ciclo de pesquisa.

Observe que estamos armazenando marcadores em um objeto (this.markers) usando o ID do lugar como chave. Essa é uma maneira de gerenciar marcadores e nos permite encontrar um marcador específico mais tarde.

Por fim, precisamos chamar clearMarkers() no início de cada nova pesquisa. O melhor lugar para isso é dentro de performSearch.

Atualizar o arquivo JavaScript

Atualize o arquivo script.js com os novos métodos e as mudanças em 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();
});

Confira suas tarefas

Salve os arquivos e atualize a página no navegador. Clique no botão "Pesquisar".

O ícone de carregamento vai aparecer por um momento e depois desaparecer. A barra lateral será preenchida com uma lista de lugares relevantes para o termo de pesquisa, e os marcadores correspondentes vão aparecer no mapa. Os marcadores ainda não fazem nada quando clicados. Vamos adicionar essa interatividade na próxima seção.

6. Ativar os filtros de pesquisa e a interatividade da lista

Nosso aplicativo agora pode mostrar resultados da pesquisa, mas ainda não é interativo. Nesta seção, vamos dar vida a todos os controles do usuário. Vamos ativar os filtros, permitir a pesquisa com a tecla "Enter" e conectar os itens na lista de resultados aos locais correspondentes no mapa.

Ao final desta etapa, o aplicativo vai parecer totalmente responsivo à entrada do usuário.

Ativar os filtros de pesquisa

Primeiro, o método performSearch será atualizado para ler os valores de todos os controles de filtro no cabeçalho. Para cada filtro (preço, classificação e "Aberto agora"), a propriedade correspondente será definida no objeto searchRequest antes da execução da pesquisa.

Adicionar listeners de eventos para todos os controles

Em seguida, vamos expandir nosso método attachEventListeners. Vamos adicionar listeners para o evento change em cada controle de filtro, bem como um listener keydown na entrada de pesquisa para detectar quando o usuário pressiona a tecla "Enter". Todos esses novos listeners vão chamar o método performSearch.

Conectar a lista de resultados ao mapa

Para criar uma experiência perfeita, clicar em um item na lista de resultados da barra lateral deve focar o mapa nesse local.

Um novo método, handleResultClick, vai detectar o evento gmp-select, que é disparado pelo elemento de pesquisa de lugar quando um item é clicado. Essa função encontra o local associado e move o mapa até ele.

Para que isso funcione, verifique se o atributo selectable está presente no componente gmp-place-search em 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>

Atualizar o arquivo JavaScript

Atualize o arquivo script.js com o código completo a seguir. Essa versão inclui o novo método handleResultClick e a lógica atualizada em 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();
});

Confira suas tarefas

Salve o arquivo script.js e atualize a página. O aplicativo agora deve ser altamente interativo.

Confirme o seguinte:

  • A pesquisa pressionando "Enter" na caixa de pesquisa funciona.
  • Mudar qualquer um dos filtros (Preço, Avaliação, Aberto agora) aciona uma nova pesquisa e atualiza os resultados.
  • Ao clicar em um item na lista de resultados da barra lateral, o mapa agora se move suavemente para a localização desse item.

Na próxima seção, vamos implementar o card de detalhes que aparece quando um marcador é clicado.

7. Implementar o elemento Place Details

Nosso aplicativo agora é totalmente interativo, mas está faltando um recurso importante: a capacidade de ver mais informações sobre um lugar selecionado. Nesta seção, vamos implementar o elemento de detalhes do lugar que aparece quando um usuário clica em um marcador no mapa ou seleciona um item no elemento de pesquisa de lugar.

Criar um contêiner de card de detalhes reutilizável

A maneira mais eficiente de mostrar detalhes de lugares no mapa é criar um único contêiner reutilizável. Vamos usar um AdvancedMarkerElement como contêiner. O conteúdo dele será o widget gmp-place-details-compact oculto que já temos no nosso index.html.

Um novo método, initDetailsPopup, vai processar a criação desse marcador reutilizável. Ele será criado uma vez quando o aplicativo for carregado e vai começar oculto. Também vamos adicionar um listener ao mapa principal nesse método para que clicar em qualquer lugar no mapa oculte o card de detalhes.

Atualizar o comportamento de clique no marcador

Em seguida, precisamos atualizar o que acontece quando um usuário clica em um marcador de lugar. O listener 'click' dentro do método addMarkers agora será responsável por mostrar o cartão de detalhes.

Quando um marcador é clicado, o listener faz o seguinte:

  1. Mova o mapa até a localização do marcador.
  2. Atualize o card de detalhes com as informações desse lugar específico.
  3. Posicione o card de detalhes no local do marcador e deixe-o visível.

Conectar o clique na lista ao clique no marcador

Por fim, vamos atualizar o método handleResultClick. Em vez de apenas mover o mapa, ele agora vai acionar de maneira programática o evento click no marcador correspondente. Esse é um padrão eficiente que permite reutilizar a mesma lógica para as duas interações, mantendo o código limpo e fácil de manter.

Atualizar o arquivo JavaScript

Atualize o arquivo script.js com o seguinte código. As seções novas ou alteradas são o método initDetailsPopup e os métodos addMarkers e handleResultClick atualizados.

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

Confira suas tarefas

Salve o arquivo script.js e atualize a página. O aplicativo vai mostrar detalhes sob demanda.

Confirme o seguinte:

  • Ao clicar em um marcador no mapa, ele é centralizado e um card de detalhes estilizado é aberto sobre o marcador.
  • Clicar em um item na lista de resultados da barra lateral faz exatamente a mesma coisa.
  • Clicar no mapa longe do card fecha ele.
  • Iniciar uma nova pesquisa também fecha qualquer card de detalhes aberto.

8. Dê o toque final

Nosso aplicativo agora está totalmente funcional, mas podemos adicionar alguns toques finais para melhorar ainda mais a experiência do usuário. Nesta última seção, vamos implementar dois recursos principais: um cabeçalho dinâmico que oferece um contexto melhor para os resultados da pesquisa e formatação automática para a consulta de pesquisa do usuário.

Criar um cabeçalho de resultados dinâmico

No momento, o cabeçalho da barra lateral sempre diz "Resultados". Podemos tornar isso mais informativo atualizando para refletir a pesquisa atual. Por exemplo, "Hambúrgueres perto de São Paulo".

Para isso, vamos usar a API Geocoding para converter as coordenadas centrais do mapa em um local legível, como o nome de uma cidade. Um novo método async, updateResultsHeader, vai processar essa lógica. Ele será chamado sempre que uma pesquisa for realizada.

Formatar a consulta de pesquisa do usuário

Para garantir que a interface pareça limpa e consistente, vamos formatar automaticamente o termo de pesquisa do usuário em "Title Case" (por exemplo, "restaurante de hambúrguer" se torna "Restaurante de hambúrguer". Uma função auxiliar, toTitleCase, vai processar essa transformação. O método performSearch será atualizado para usar essa função na entrada do usuário antes de realizar a pesquisa e atualizar o cabeçalho.

Atualizar o arquivo JavaScript

Atualize o arquivo script.js com a versão final do código. Isso inclui os novos métodos toTitleCase e updateResultsHeader e o método performSearch atualizado que os 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();
});

Confira suas tarefas

Salve o arquivo script.js e atualize a página.

Verifique os recursos:

  • Digite pizza (tudo em minúsculas) na caixa de pesquisa e clique em "Pesquisar". O texto na caixa vai mudar para "Pizza", e o cabeçalho na barra lateral vai ser atualizado para "Pizza perto de Nova York".
  • Mova o mapa para outra cidade, como Boston, e pesquise novamente. O cabeçalho vai mudar para "Pizza perto de Boston".

9. Parabéns

Você criou um aplicativo de pesquisa local completo e interativo que combina a simplicidade do Kit de Interface do Places com o poder das principais APIs JavaScript da Plataforma Google Maps.

O que você aprendeu

  • Como estruturar um aplicativo de mapeamento usando uma classe JavaScript para gerenciar estado e lógica.
  • Como usar o Kit de Interface do Places com a API Google Maps JavaScript para desenvolvimento rápido de interface.
  • Como adicionar e gerenciar de forma programática Marcadores avançados para mostrar pontos de interesse personalizados no mapa.
  • Como usar o serviço de geocodificação para transformar coordenadas em endereços legíveis e melhorar a experiência do usuário.
  • Como identificar e corrigir condições de disputa comuns em um aplicativo interativo usando flags de estado e garantindo que as propriedades do componente sejam atualizadas corretamente.

A seguir

Quais outros codelabs você quer ver?

Visualização de dados em mapas Como personalizar o estilo dos meus mapas Como criar interações 3D em mapas

Não encontrou o codelab que mais interessa? Solicite-o aqui.