Создайте приложение локального поиска с помощью Places UI Kit

1. Прежде чем начать

В этой лабораторной работе вы узнаете, как создать полностью интерактивное приложение локального поиска с использованием набора пользовательского интерфейса Places платформы Google Карт.

Скриншот готового приложения PlaceFinder, на котором показана карта Нью-Йорка с маркерами, боковая панель с результатами поиска и открытая карточка с подробностями.

Предпосылки

  • Проект Google Cloud с необходимыми настроенными API и учетными данными.
  • Базовые знания HTML и CSS.
  • Понимание современного JavaScript.
  • Современный веб-браузер, например, последняя версия Chrome.
  • Текстовый редактор по вашему выбору.

Что ты будешь делать?

  • Структурируйте картографическое приложение с помощью класса JavaScript.
  • Используйте веб-компоненты для отображения карты
  • Используйте элемент поиска «Место» для выполнения и отображения результатов текстового поиска.
  • Программное создание и управление пользовательскими маркерами карты AdvancedMarkerElement .
  • Отображение элемента сведений о месте при выборе пользователем местоположения.
  • Используйте API геокодирования для создания динамичного и удобного интерфейса.

Что вам понадобится

  • Проект Google Cloud с включенным биллингом
  • API-ключ платформы Google Карт
  • Идентификатор карты
  • Включены следующие API:
    • API JavaScript Карт
    • Комплект пользовательского интерфейса Places
    • API геокодирования

2. Настройте

Для следующего шага включения вам потребуется включить Maps JavaScript API, Places UI Kit и Geocoding API.

Настройте платформу Google Карт

Если у вас еще нет учетной записи Google Cloud Platform и проекта с включенным выставлением счетов, ознакомьтесь с руководством « Начало работы с Google Maps Platform», чтобы создать учетную запись для выставления счетов и проект.

  1. В Cloud Console щелкните раскрывающееся меню проектов и выберите проект, который вы хотите использовать для этой кодовой лаборатории.

  1. Включите API и SDK платформы Google Карт, необходимые для этой лабораторной работы, в Google Cloud Marketplace . Для этого следуйте инструкциям в этом видео или в этой документации .
  2. Сгенерируйте ключ API на странице «Учётные данные» в Cloud Console. Вы можете следовать инструкциям в этом видео или в этой документации . Для всех запросов к платформе Google Карт требуется ключ API.

3. Оболочка приложения и функциональная карта

На этом первом этапе мы создадим полную визуальную структуру нашего приложения и установим понятную структуру JavaScript на основе классов. Это заложит прочную основу для дальнейшего развития. К концу этого раздела у вас будет оформленная страница с интерактивной картой.

Создайте HTML-файл

Сначала создайте файл index.html . Этот файл будет содержать полную структуру нашего приложения, включая заголовок, фильтры поиска, боковую панель, контейнер карты и необходимые веб-компоненты.

Скопируйте следующий код в index.html . Обязательно замените YOUR_API_KEY_HERE на ваш ключ API платформы Google Карт, а DEMO_MAP_ID — на ваш идентификатор карты платформы Google Карт.

<!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-файл

Затем создайте файл style.css . Сейчас мы добавим все необходимые стили, чтобы изначально создать чистый и современный вид. Этот CSS-код отвечает за общую компоновку, цвета, шрифты и внешний вид всех элементов пользовательского интерфейса.

Скопируйте следующий код в 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

Наконец, создайте файл script.js . Мы разместим наше приложение внутри JavaScript-класса PlaceFinderApp . Это позволит упорядочить код и обеспечить чёткое управление состоянием.

Этот начальный код определит класс, найдет все наши HTML-элементы в constructor и создаст метод init() для загрузки библиотек платформы Google Карт.

Скопируйте следующий код в 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();
});

Ограничения API-ключа

Для работы этой практической работы вам может потребоваться добавить новое ограничение к вашему ключу API. Дополнительную информацию и инструкции по этому вопросу см. в статье «Ограничение ключей API».

Проверьте свою работу

Откройте файл index.html в веб-браузере. Вы увидите страницу с заголовком, содержащим строку поиска и фильтры, боковую панель с сообщением «Результаты вашего поиска появятся здесь» и большую карту с центром в Нью-Йорке. На этом этапе элементы управления поиском ещё не работают.

4. Реализуйте функцию поиска

В этом разделе мы реализуем наше приложение, реализуя базовую функциональность поиска. Мы напишем код, который будет запускаться при нажатии пользователем кнопки «Поиск». Мы создадим эту функцию с самого начала, руководствуясь лучшими практиками, чтобы корректно обрабатывать пользовательское взаимодействие и предотвращать распространённые ошибки, такие как состояния гонки.

К концу этого шага вы сможете нажать кнопку поиска и увидеть индикатор загрузки, пока приложение извлекает данные в фоновом режиме.

Создайте метод поиска

Сначала определим метод performSearch в нашем классе PlaceFinderApp . Эта функция станет основой нашей логики поиска. Мы также введём переменную экземпляра isSearchInProgress , которая будет действовать как «контролёр». Это предотвратит запуск пользователем нового поиска во время текущего, что может привести к ошибкам.

Логика внутри performSearch может показаться сложной, поэтому мы разберем ее подробнее:

  1. Сначала он проверяет, ведётся ли уже поиск. Если да, то ничего не делает.
  2. Он устанавливает флаг isSearchInProgress в true , чтобы «блокировать» функцию.
  3. Он отображает индикатор загрузки и подготавливает пользовательский интерфейс к получению новых результатов.
  4. Он устанавливает свойство textQuery поискового запроса в null . Это важный шаг, который заставляет веб-компонент распознавать поступление нового запроса.
  5. Он использует setTimeout с 0 задержкой. Этот стандартный приём JavaScript планирует выполнение оставшейся части кода в следующей задаче браузера, гарантируя, что компонент сначала обработает значение null . Даже если пользователь дважды выполнит поиск одного и того же значения, новый поиск всегда будет запущен.

Добавить прослушиватели событий

Далее нам нужно вызывать метод performSearch при взаимодействии пользователя с приложением. Мы создадим новый метод attachEventListeners , чтобы весь код обработки событий был в одном месте. Пока что мы просто добавим прослушиватель для события click кнопки поиска. Мы также добавим плейсхолдер для другого события, gmp-load , которое мы используем на следующем шаге.

Обновите файл JavaScript

Обновите файл script.js , добавив следующий код. Новые или изменённые разделы — это методы attachEventListeners и 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();
});

Проверьте свою работу

Сохраните файл script.js и обновите index.html в браузере. Страница должна выглядеть так же, как и раньше. Теперь нажмите кнопку «Поиск» в заголовке.

Вы должны увидеть две вещи:

  1. Заполнитель сообщения «Результаты вашего поиска появятся здесь» исчезнет.
  2. Появляется индикатор загрузки и продолжает вращаться.

Счётчик будет вращаться вечно, потому что мы ещё не указали ему время остановки. Мы сделаем это в следующем разделе, когда покажем результаты. Это подтверждает, что наша функция поиска срабатывает корректно.

5. Отобразите результаты и добавьте маркеры.

Теперь, когда триггер поиска работает, следующая задача — отобразить результаты на экране. Код в этом разделе свяжет логику поиска с пользовательским интерфейсом. После того, как элемент поиска по месту завершит загрузку данных, он снимет блокировку поиска, скроет индикатор загрузки и отобразит маркер на карте для каждого результата.

Прослушивание завершения поиска

Элемент поиска места запускает событие gmp-load после успешного получения данных. Это идеальный сигнал для обработки результатов.

Сначала добавьте прослушиватель событий для этого события в наш метод attachEventListeners .

Создание методов обработки маркеров

Далее мы создадим два новых вспомогательных метода: clearMarkers и addMarkers .

  • clearMarkers() удалит все маркеры из предыдущего поиска.
  • addMarkers() будет вызван нашим обработчиком gmp-load . Он пройдёт по списку мест, возвращённых поиском, и создаст новый элемент AdvancedMarkerElement для каждого из них. Здесь же мы скроем индикатор загрузки и снимем блокировку isSearchInProgress , завершив цикл поиска.

Обратите внимание, что мы сохраняем маркеры в объекте ( this.markers ), используя идентификатор места в качестве ключа. Это способ управления маркерами, который позволит нам позже найти нужный маркер.

Наконец, нам нужно вызывать clearMarkers() в начале каждого нового поиска. Лучше всего это сделать внутри performSearch .

Обновите файл JavaScript

Обновите файл script.js , добавив новые методы и изменения в attachEventListeners и 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();
});

Проверьте свою работу

Сохраните файлы и обновите страницу в браузере. Нажмите кнопку «Поиск».

Индикатор загрузки должен появиться на мгновение, а затем исчезнуть. На боковой панели появится список мест, соответствующих поисковому запросу, а на карте появятся соответствующие маркеры. Маркеры пока не активируются при нажатии; мы добавим эту интерактивность в следующем разделе.

6. Активируйте фильтры поиска и интерактивность списка.

Наше приложение теперь может отображать результаты поиска, но пока не интерактивно. В этом разделе мы реализуем все пользовательские элементы управления. Мы активируем фильтры, включим поиск по клавише «Enter» и свяжем элементы в списке результатов с соответствующими им местоположениями на карте.

К концу этого шага приложение будет полностью реагировать на действия пользователя.

Активировать фильтры поиска

Во-первых, метод performSearch будет обновлён для чтения значений всех элементов управления фильтрами в заголовке. Для каждого фильтра (цена, рейтинг и «Открыто сейчас») соответствующее свойство будет установлено в объекте searchRequest перед выполнением поиска.

Добавить прослушиватели событий для всех элементов управления

Далее мы расширим наш метод attachEventListeners . Мы добавим прослушиватели события change для каждого элемента управления фильтром, а также прослушиватель keydown для поля поиска, чтобы отслеживать нажатие клавиши Enter пользователем. Все эти новые прослушиватели будут вызывать метод performSearch .

Подключите список результатов к карте

Для создания бесперебойного взаимодействия щелчок по элементу в списке результатов на боковой панели должен фокусировать карту на этом местоположении.

Новый метод handleResultClick будет отслеживать событие gmp-select , которое активируется элементом поиска места при щелчке по элементу. Эта функция находит местоположение соответствующего места и плавно перемещает карту к нему.

Чтобы это работало, убедитесь, что атрибут selectable присутствует в компоненте gmp-place-search в 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>

Обновите файл JavaScript

Обновите файл script.js , добавив следующий полный код. Эта версия включает новый метод handleResultClick и обновлённую логику в attachEventListeners и 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();
});

Проверьте свою работу

Сохраните файл script.js и обновите страницу. Теперь приложение должно быть максимально интерактивным.

Проверьте следующее:

  • Поиск путем нажатия «Enter» в строке поиска работает.
  • Изменение любого из фильтров (Цена, Рейтинг, Открыто сейчас) запускает новый поиск и обновляет результаты.
  • Нажатие на элемент в списке результатов на боковой панели теперь плавно перемещает карту к местоположению этого элемента.

В следующем разделе мы реализуем карточку с подробностями, которая появляется при нажатии на маркер.

7. Реализуйте элемент «Сведения о месте»

Наше приложение теперь полностью интерактивно, но в нём отсутствует ключевая функция: возможность просмотра дополнительной информации о выбранном месте. В этом разделе мы реализуем элемент «Сведения о месте», который будет появляться при щелчке пользователя по маркеру на карте или выборе элемента в элементе поиска места.

Создайте многоразовый контейнер для карточек с данными

Самый эффективный способ отображения информации о месте на карте — создать единый многоразовый контейнер. В качестве такого контейнера мы будем использовать AdvancedMarkerElement . Его содержимым будет скрытый виджет gmp-place-details-compact который уже есть в нашем index.html .

Новый метод initDetailsPopup будет отвечать за создание этого многоразового маркера. Он будет создан один раз при загрузке приложения и будет запущен скрытым. Мы также добавим в этот метод прослушиватель к основной карте, чтобы щелчок в любом месте карты скрывал карточку с подробностями.

Обновить поведение щелчка маркера

Далее нам нужно обновить код, который реагирует на нажатие пользователем маркера места. Обработчик 'click' внутри метода addMarkers теперь будет отвечать за отображение карточки с подробностями.

При щелчке по маркеру слушатель:

  1. Переместите карту к местоположению маркера.
  2. Обновите карточку сведений, указав информацию по конкретному месту.
  3. Расположите карточку с подробностями в месте расположения маркера и сделайте ее видимой.

Свяжите щелчок списка с щелчком маркера

Наконец, мы обновим метод handleResultClick . Вместо простого панорамирования карты он теперь будет программно инициировать событие click по соответствующему маркеру. Это мощный шаблон, позволяющий повторно использовать одну и ту же логику для обоих взаимодействий, сохраняя при этом чистоту и удобство поддержки кода.

Обновите файл JavaScript

Обновите файл script.js , добавив следующий код. Новые или изменённые разделы — это метод initDetailsPopup и обновлённые методы addMarkers и 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();
});

Проверьте свою работу

Сохраните файл script.js и обновите страницу. Теперь приложение должно отображать информацию по запросу.

Проверьте следующее:

  • Теперь нажатие на маркер на карте центрирует карту и открывает стилизованную карточку с подробностями над маркером.
  • Нажатие на элемент в списке результатов на боковой панели делает то же самое.
  • Нажатие на карту за пределами карточки закрывает ее.
  • Начало нового поиска также закрывает любую открытую карточку с подробностями.

8. Нанесите финишный слой полировки.

Наше приложение теперь полностью функционально, но осталось добавить несколько последних штрихов, чтобы сделать его ещё удобнее для пользователя. В этом заключительном разделе мы реализуем две ключевые функции: динамический заголовок, который обеспечит лучший контекст для результатов поиска, и автоматическое форматирование поискового запроса пользователя.

Создать динамический заголовок результатов

Сейчас заголовок боковой панели всегда отображает «Результаты». Мы можем сделать его более информативным, обновив его в соответствии с текущим поиском. Например, «Бургеры рядом с Нью-Йорком».

Для этого мы воспользуемся API геокодирования, чтобы преобразовать координаты центра карты в удобное для восприятия местоположении, например, название города. Эту логику будет обрабатывать новый async метод updateResultsHeader . Он будет вызываться при каждом поиске.

Форматировать поисковый запрос пользователя

Чтобы интерфейс выглядел аккуратно и единообразно, мы автоматически преобразуем поисковый запрос пользователя в «Заглавные буквы» (например, «ресторан бургеров» станет «Ресторан бургеров»). Вспомогательная функция toTitleCase выполнит это преобразование. Метод performSearch будет обновлён для использования этой функции при вводе пользователем перед выполнением поиска и обновлением заголовка.

Обновите файл JavaScript

Обновите файл script.js , добавив финальную версию кода. Это включает новые методы toTitleCase и updateResultsHeader , а также обновлённый метод 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.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();
});

Проверьте свою работу

Сохраните файл script.js и обновите страницу.

Проверьте характеристики:

  • Введите слово pizza (строчными буквами) в поле поиска и нажмите «Поиск». Текст в поле должен измениться на «Пицца», а заголовок на боковой панели должен обновиться на «Пицца рядом с Нью-Йорком».
  • Переместите карту на другой город, например, Бостон, и повторите поиск. Заголовок должен измениться на «Пицца рядом с Бостоном».

9. Поздравления

Вы успешно создали полноценное интерактивное приложение локального поиска, которое сочетает в себе простоту Places UI Kit с мощью основных JavaScript API платформы Google Maps.

Что вы узнали

  • Как структурировать картографическое приложение, используя класс JavaScript для управления состоянием и логикой.
  • Как использовать Places UI Kit с Google Maps JavaScript API для быстрой разработки пользовательского интерфейса.
  • Как программно добавлять и управлять расширенными маркерами для отображения пользовательских точек интереса на карте.
  • Как использовать Службу геокодирования для преобразования координат в удобочитаемые адреса для лучшего пользовательского опыта.
  • Как выявить и устранить распространенные состояния гонки в интерактивном приложении, используя флаги состояния и гарантируя правильное обновление свойств компонентов.

Что дальше?

  • Узнайте больше о настройке расширенных маркеров путем изменения их цвета, масштаба или даже использования пользовательского HTML-кода.
  • Изучите возможности облачного оформления карт, чтобы настроить внешний вид и содержание вашей карты в соответствии с вашим брендом.
  • Попробуйте добавить библиотеку рисования , чтобы пользователи могли рисовать фигуры на карте для определения областей поиска.
  • Помогите нам создать контент, который будет вам наиболее полезен, ответив на следующий опрос:

Какие еще практические занятия вы хотели бы увидеть?

Визуализация данных на картах Подробнее о настройке стиля моих карт Создание 3D-взаимодействий на картах

Не нашли нужную вам практическую работу? Запросите её в новом выпуске здесь .