1. Zanim zaczniesz
Z tego ćwiczenia w Codelabs dowiesz się, jak utworzyć w pełni interaktywną aplikację do wyszukiwania lokalnego za pomocą interfejsu Places UI Kit w Google Maps Platform.
Wymagania wstępne
- projekt Google Cloud ze skonfigurowanymi niezbędnymi interfejsami API i danymi logowania;
- Podstawowa znajomość języków HTML i CSS.
- Znajomość nowoczesnego kodu JavaScript.
- nowoczesna przeglądarka, np. najnowsza wersja Chrome;
- dowolny edytor tekstu;
Jakie zadania wykonasz
- Strukturyzowanie aplikacji do mapowania za pomocą klasy JavaScript.
- Wyświetlanie mapy za pomocą komponentów internetowych
- Użyj elementu wyszukiwania miejsca, aby przeprowadzić wyszukiwanie tekstowe i wyświetlić jego wyniki.
- Programowe tworzenie niestandardowych znaczników mapy
AdvancedMarkerElement
i zarządzanie nimi. - Wyświetl element Szczegóły miejsca, gdy użytkownik wybierze lokalizację.
- Użyj interfejsu Geocoding API, aby utworzyć dynamiczny i przyjazny dla użytkownika interfejs.
Czego potrzebujesz
- projekt Google Cloud z włączonymi płatnościami;
- Klucz interfejsu API Google Maps Platform
- Identyfikator mapy
- Włączone interfejsy API:
- Maps JavaScript API
- Pakiet UI do Miejsc
- Geocoding API
2. Konfiguracja
W kolejnym kroku musisz włączyć interfejs Maps JavaScript API, pakiet UI Kit Miejsc i interfejs Geocoding API.
Konfigurowanie Google Maps Platform
Jeśli nie masz jeszcze konta Google Cloud Platform i projektu z włączonymi płatnościami, zapoznaj się z przewodnikiem Pierwsze kroki z Google Maps Platform, aby utworzyć konto rozliczeniowe i projekt.
- W konsoli Google Cloud kliknij menu projektu i wybierz projekt, którego chcesz użyć w tym samouczku.
- Włącz interfejsy API i pakiety SDK Google Maps Platform wymagane w tym samouczku w Google Cloud Marketplace. Aby to zrobić, wykonaj czynności opisane w tym filmie lub tej dokumentacji.
- Wygeneruj klucz interfejsu API na stronie Dane logowania w konsoli Cloud. Możesz wykonać czynności opisane w tym filmie lub tej dokumentacji. Wszystkie żądania wysyłane do Google Maps Platform wymagają klucza interfejsu API.
3. powłoka aplikacji i funkcjonalna mapa,
W pierwszym kroku utworzymy pełny układ wizualny naszej aplikacji i ustalimy przejrzystą strukturę opartą na klasach dla naszego kodu JavaScript. Daje nam to solidne podstawy do dalszego rozwoju. Po zakończeniu tej sekcji będziesz mieć stronę ze stylem, na której wyświetla się interaktywna mapa.
Tworzenie pliku HTML
Najpierw utwórz plik o nazwie index.html
. Ten plik będzie zawierał pełną strukturę naszej aplikacji, w tym nagłówek, filtry wyszukiwania, pasek boczny, kontener mapy i niezbędne komponenty internetowe.
Skopiuj ten kod do pliku index.html
. Pamiętaj, aby zastąpić YOUR_API_KEY_HERE
własnym kluczem interfejsu Google Maps Platform API, a DEMO_MAP_ID
własnym identyfikatorem mapy Google Maps Platform.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Local Search App</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Google Fonts: Roboto -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<!-- GMP Bootstrap Loader -->
<script>
(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
key: "YOUR_API_KEY_HERE",
v: "weekly",
libraries: "places,maps,marker,geocoding"
});
</script>
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<!-- Header for search controls -->
<header class="top-header">
<div class="logo">
<svg viewBox="0 0 24 24" width="28" height="28"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" fill="currentColor"></path></svg>
<span>PlaceFinder</span>
</div>
<div class="search-container">
<input
type="text"
id="query-input"
placeholder="e.g., burger in New York"
value="burger"
/>
<button id="search-button" aria-label="Search">Search</button>
</div>
<div class="filter-container">
<label class="open-now-label">
<input type="checkbox" id="open-now-filter"> Open Now
</label>
<select id="rating-filter" aria-label="Minimum rating">
<option value="0" selected>Any rating</option>
<option value="1">1+ ★</option>
<option value="2">2+ ★★</option>
<option value="3">3+ ★★★</option>
<option value="4">4+ ★★★★</option>
<option value="5">5 ★★★★★</option>
</select>
<select id="price-filter" aria-label="Price level">
<option value="0" selected>Any Price</option>
<option value="1">$</option>
<option value="2">$$</option>
<option value="3">$$$</option>
<option value="4">$$$$</option>
</select>
</div>
</header>
<!-- Main content area -->
<div class="app-container">
<!-- Left Panel: Results -->
<div class="sidebar">
<div class="results-header">
<h2 id="results-header-text">Results</h2>
</div>
<div class="results-container">
<gmp-place-search id="place-search-list" class="hidden" selectable>
<gmp-place-all-content></gmp-place-all-content>
<gmp-place-text-search-request></gmp-place-text-search-request>
</gmp-place-search>
<div id="placeholder-message" class="placeholder">
<p>Your search results will appear here.</p>
</div>
<div id="loading-spinner" class="spinner-overlay">
<div class="spinner"></div>
</div>
</div>
</div>
<!-- Right Panel: Map -->
<div class="map-container">
<gmp-map
center="40.758896,-73.985130"
zoom="13"
map-id="DEMO_MAP_ID"
>
</gmp-map>
<div id="details-container">
<gmp-place-details-compact>
<gmp-place-details-place-request></gmp-place-details-place-request>
<gmp-place-all-content></gmp-place-all-content>
</gmp-place-details-compact>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
Tworzenie pliku CSS
Następnie utwórz plik o nazwie style.css
. Dodamy teraz wszystkie niezbędne style, aby od początku uzyskać czysty, nowoczesny wygląd. Ten arkusz CSS odpowiada za ogólny układ, kolory, czcionki i wygląd wszystkich elementów interfejsu.
Skopiuj ten kod do pliku 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);
}
Tworzenie klasy aplikacji JavaScript
Na koniec utwórz plik o nazwie script.js
. Nasza aplikacja będzie miała strukturę klasy JavaScript o nazwie PlaceFinderApp
. Dzięki temu nasz kod jest uporządkowany, a stan jest zarządzany w sposób przejrzysty.
Ten początkowy kod zdefiniuje klasę, znajdzie wszystkie elementy HTML w constructor
i utworzy metodę init()
do wczytywania bibliotek Google Maps Platform.
Skopiuj ten kod do pliku 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();
});
Ograniczenia klucza interfejsu API
Aby ten Codelab działał, może być konieczne dodanie nowego ograniczenia do klucza interfejsu API. Więcej informacji i wskazówki znajdziesz w artykule Ograniczanie kluczy interfejsu API.
Sprawdzanie efektów pracy
Otwórz index.html
w przeglądarce. Powinna się wyświetlić strona z nagłówkiem zawierającym pasek wyszukiwania i filtry, paskiem bocznym z komunikatem „Wyniki wyszukiwania pojawią się tutaj” oraz dużą mapą z Nowym Jorkiem w centrum. Na tym etapie elementy sterujące wyszukiwania nie działają jeszcze.
4. Implementowanie funkcji wyszukiwania
W tej sekcji ożywimy naszą aplikację, wdrażając podstawową funkcję wyszukiwania. Napiszemy kod, który będzie uruchamiany, gdy użytkownik kliknie przycisk „Szukaj”. Od początku będziemy tworzyć tę funkcję zgodnie z najlepszymi praktykami, aby sprawnie obsługiwać interakcje użytkowników i zapobiegać typowym błędom, takim jak wyścigi.
Po wykonaniu tego kroku możesz kliknąć przycisk wyszukiwania i zobaczyć wskaźnik ładowania, który będzie się wyświetlać, gdy aplikacja będzie pobierać dane w tle.
Tworzenie metody wyszukiwania
Najpierw określ metodę performSearch
w klasie PlaceFinderApp
. Ta funkcja będzie podstawą naszej logiki wyszukiwania. Wprowadzimy też zmienną instancji isSearchInProgress
, która będzie pełnić rolę „strażnika”. Zapobiega to rozpoczęciu przez użytkownika nowego wyszukiwania, gdy jedno jest już w toku, co może prowadzić do błędów.
Logika w performSearch
może wydawać się skomplikowana, więc ją rozbijemy:
- Najpierw sprawdza, czy wyszukiwanie jest już w toku. Jeśli tak, nie robi nic.
- Ustawia flagę
isSearchInProgress
natrue
, aby „zablokować” funkcję. - Wyświetla spinner ładowania i przygotowuje interfejs do wyświetlenia nowych wyników.
- Ustawia właściwość
textQuery
żądania wyszukiwania nanull
. To kluczowy krok, który zmusza komponent internetowy do rozpoznania, że nadchodzi nowe żądanie. - Używa przy tym metody
setTimeout
z opóźnieniem0
. Ta standardowa technika JavaScriptu planuje wykonanie pozostałej części kodu w następnym zadaniu przeglądarki, dzięki czemu komponent najpierw przetworzy wartośćnull
. Nawet jeśli użytkownik wyszuka dokładnie to samo 2 razy, zawsze zostanie uruchomione nowe wyszukiwanie.
Dodawanie detektorów zdarzeń
Następnie musimy wywołać metodę performSearch
, gdy użytkownik wchodzi w interakcję z aplikacją. Aby cały kod obsługi zdarzeń był w jednym miejscu, utworzymy nową metodę attachEventListeners
. Na razie dodamy tylko detektor zdarzenia click
przycisku wyszukiwania. Dodamy też symbol zastępczy dla innego zdarzenia, gmp-load
, którego użyjemy w następnym kroku.
Aktualizowanie pliku JavaScript
Zaktualizuj plik script.js
tym kodem. Nowe lub zmienione sekcje to metoda attachEventListeners
i metoda 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();
});
Sprawdzanie efektów pracy
Zapisz plik script.js
i odśwież go index.html
w przeglądarce. Strona powinna wyglądać tak samo jak wcześniej. Teraz kliknij przycisk „Szukaj” w nagłówku.
Powinny nastąpić 2 rzeczy:
- Zniknie komunikat zastępczy „Tutaj wyświetlą się wyniki wyszukiwania”.
- Pojawi się wskaźnik wczytywania, który będzie się obracać.
Spinner będzie się kręcić w nieskończoność, ponieważ nie powiedzieliśmy mu jeszcze, kiedy ma się zatrzymać. Zrobimy to w następnej sekcji, w której wyświetlimy wyniki. Potwierdza to, że nasza funkcja wyszukiwania jest wywoływana prawidłowo.
5. Wyświetlanie wyników i dodawanie znaczników
Teraz, gdy wyzwalacz wyszukiwania działa, następnym zadaniem jest wyświetlenie wyników na ekranie. Kod w tej sekcji połączy logikę wyszukiwania z interfejsem. Gdy element wyszukiwania miejsca zakończy wczytywanie danych, zwolni „blokadę” wyszukiwania, ukryje wskaźnik wczytywania i wyświetli na mapie znacznik dla każdego wyniku.
Oczekiwanie na zakończenie wyszukiwania
Element wyszukiwania miejsca wywołuje zdarzenie gmp-load
, gdy pobierze dane. To idealny sygnał, który pozwala nam przetworzyć wyniki.
Najpierw dodaj detektor zdarzeń dla tego zdarzenia w naszej metodzie attachEventListeners
.
Tworzenie metod obsługi markerów
Następnie utworzymy 2 nowe metody pomocnicze: clearMarkers
i addMarkers
.
clearMarkers()
usunie wszystkie znaczniki z poprzedniego wyszukiwania.addMarkers()
zostanie wywołana przezgmp-load
. Pętla będzie przechodzić przez listę miejsc zwróconych przez wyszukiwarkę i tworzyć nowy elementAdvancedMarkerElement
dla każdego z nich. W tym miejscu ukryjemy też spinner ładowania i zwolnimy blokadęisSearchInProgress
, co zakończy cykl wyszukiwania.
Zwróć uwagę, że znaczniki przechowujemy w obiekcie (this.markers
), używając identyfikatora miejsca jako klucza. Jest to sposób zarządzania markerami, który umożliwia późniejsze znalezienie konkretnego markera.
Na koniec musimy wywołać funkcję clearMarkers()
na początku każdego nowego wyszukiwania. Najlepszym miejscem na to jest performSearch
.
Aktualizowanie pliku JavaScript
Zaktualizuj plik script.js
, dodając nowe metody oraz zmiany w attachEventListeners
i 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();
});
Sprawdzanie efektów pracy
Zapisz pliki i odśwież stronę w przeglądarce. Kliknij przycisk „Szukaj”.
Wskaźnik wczytywania powinien się na chwilę pojawić, a potem zniknąć. Na pasku bocznym pojawi się lista miejsc powiązanych z wyszukiwanym hasłem, a na mapie powinny się wyświetlić odpowiednie znaczniki. Markery nie reagują jeszcze na kliknięcie. Dodamy tę interaktywność w następnej sekcji.
6. Aktywowanie filtrów wyszukiwania i interaktywności listy
Nasza aplikacja może teraz wyświetlać wyniki wyszukiwania, ale nie jest jeszcze interaktywna. W tej sekcji omówimy wszystkie ustawienia użytkownika. Aktywujemy filtry, włączymy wyszukiwanie za pomocą klawisza „Enter” i połączymy elementy na liście wyników z odpowiednimi lokalizacjami na mapie.
Po wykonaniu tego kroku aplikacja będzie w pełni reagować na dane wejściowe użytkownika.
Aktywowanie filtrów wyszukiwania
Najpierw metoda performSearch
zostanie zaktualizowana, aby odczytywać wartości ze wszystkich elementów sterujących filtrami w nagłówku. W przypadku każdego filtra (cena, ocena i „Otwarte teraz”) odpowiednia właściwość zostanie ustawiona w obiekcie searchRequest
przed wykonaniem wyszukiwania.
Dodawanie detektorów zdarzeń do wszystkich elementów sterujących
Następnie rozszerzymy naszą metodę attachEventListeners
. Do każdego elementu sterującego filtrem dodamy detektory zdarzeń change
, a do pola wyszukiwania detektor keydown
, aby wykrywać, kiedy użytkownik naciśnie klawisz „Enter”. Wszyscy nowi słuchacze będą wywoływać metodę performSearch
.
Połącz listę wyników z mapą
Aby zapewnić płynność działania, kliknięcie elementu na liście wyników na pasku bocznym powinno powodować skupienie mapy na tej lokalizacji.
Nowa metoda handleResultClick
będzie nasłuchiwać zdarzenia gmp-select
, które jest wywoływane przez element wyszukiwania miejsc po kliknięciu elementu. Ta funkcja znajdzie lokalizację powiązanego miejsca i płynnie przesunie do niej mapę.
Aby to działało, upewnij się, że atrybut selectable
jest obecny w komponencie gmp-place-search
w 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>
Aktualizowanie pliku JavaScript
Zaktualizuj plik script.js
, dodając ten kompletny kod. Ta wersja zawiera nową metodę handleResultClick
oraz zaktualizowaną logikę w przypadku attachEventListeners
i 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();
});
Sprawdzanie efektów pracy
Zapisz plik script.js
i odśwież stronę. Aplikacja powinna być teraz bardzo interaktywna.
Potwierdź te informacje:
- Wyszukiwanie przez naciśnięcie klawisza „Enter” w polu wyszukiwania działa.
- Zmiana dowolnego filtra (Cena, Ocena, Otwarte teraz) powoduje nowe wyszukiwanie i aktualizację wyników.
- Kliknięcie elementu na liście wyników na pasku bocznym powoduje teraz płynne przesuwanie mapy do lokalizacji tego elementu.
W następnej sekcji zaimplementujemy kartę szczegółów, która będzie się pojawiać po kliknięciu znacznika.
7. Implementowanie elementu Informacje o miejscu
Nasza aplikacja jest już w pełni interaktywna, ale brakuje jej kluczowej funkcji: możliwości wyświetlania większej ilości informacji o wybranym miejscu. W tej sekcji zaimplementujemy element szczegółów miejsca, który będzie się wyświetlać, gdy użytkownik kliknie znacznik na mapie lub wybierze element w elemencie wyszukiwania miejsca.
Tworzenie kontenera karty szczegółów wielokrotnego użytku
Najskuteczniejszym sposobem wyświetlania szczegółów miejsca na mapie jest utworzenie jednego kontenera wielokrotnego użytku. Użyjemy AdvancedMarkerElement
jako tego kontenera. Jego zawartością będzie ukryty widżet gmp-place-details-compact
, który mamy już w index.html
.
Nowa metoda initDetailsPopup
będzie obsługiwać tworzenie tego markera wielokrotnego użytku. Zostanie utworzony raz podczas wczytywania aplikacji i będzie początkowo ukryty. Dodamy też odbiornik do głównej mapy, aby kliknięcie dowolnego miejsca na mapie powodowało ukrycie karty szczegółów.
Aktualizowanie zachowania po kliknięciu znacznika
Następnie musimy zaktualizować działanie po kliknięciu przez użytkownika znacznika miejsca. Za wyświetlanie karty szczegółów będzie teraz odpowiadać odbiornik 'click'
w metodzie addMarkers
.
Gdy użytkownik kliknie marker, odbiornik:
- Przesuń mapę do lokalizacji znacznika.
- Zaktualizuj kartę szczegółów, podając informacje o tym konkretnym miejscu.
- Umieść kartę szczegółów w lokalizacji znacznika i spraw, aby była widoczna.
Połącz kliknięcie listy z kliknięciem znacznika
Na koniec zaktualizujemy metodę handleResultClick
. Zamiast tylko przesuwać mapę, będzie teraz programowo wywoływać zdarzenie click
na odpowiednim znaczniku. Jest to zaawansowany wzorzec, który pozwala nam używać tej samej logiki w przypadku obu interakcji, dzięki czemu nasz kod jest przejrzysty i łatwy w utrzymaniu.
Aktualizowanie pliku JavaScript
Zaktualizuj plik script.js
tym kodem. Nowe lub zmienione sekcje to metoda initDetailsPopup
oraz zaktualizowane metody addMarkers
i 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();
});
Sprawdzanie efektów pracy
Zapisz plik script.js
i odśwież stronę. Aplikacja powinna teraz wyświetlać szczegóły na żądanie.
Potwierdź te informacje:
- Kliknięcie znacznika na mapie powoduje teraz wyśrodkowanie mapy i otwarcie nad znacznikiem karty ze stylizowanymi szczegółami.
- Kliknięcie elementu na liście wyników na pasku bocznym daje dokładnie ten sam efekt.
- Kliknięcie mapy poza kartą spowoduje jej zamknięcie.
- Rozpoczęcie nowego wyszukiwania powoduje też zamknięcie otwartej karty szczegółów.
8. Dodaj ostatnie szlify
Nasza aplikacja jest już w pełni funkcjonalna, ale możemy jeszcze wprowadzić kilka poprawek, aby zwiększyć zadowolenie użytkowników. W tej ostatniej sekcji wdrożymy 2 kluczowe funkcje: dynamiczny nagłówek, który zapewnia lepszy kontekst wyników wyszukiwania, oraz automatyczne formatowanie zapytania użytkownika.
Tworzenie dynamicznego nagłówka wyników
Obecnie nagłówek paska bocznego zawsze brzmi „Wyniki”. Możemy to ulepszyć, aktualizując je tak, aby odzwierciedlało bieżące wyszukiwanie. Na przykład „Burgery w pobliżu Warszawy”.
W tym celu użyjemy interfejsu Geocoding API, aby przekonwertować współrzędne środka mapy na czytelną dla człowieka lokalizację, np. nazwę miasta. Tą logiką będzie się zajmować nowa metoda async
o nazwie updateResultsHeader
. Będzie ona wywoływana przy każdym wyszukiwaniu.
Formatowanie zapytania użytkownika
Aby interfejs wyglądał przejrzyście i spójnie, automatycznie sformatujemy wyszukiwane hasło użytkownika w formacie „Title Case” (np. „burger restaurant” zmieni się na „Burger Restaurant”. Przekształceniem tym zajmie się funkcja pomocnicza toTitleCase
. Metoda performSearch
zostanie zaktualizowana, aby używać tej funkcji w danych wejściowych użytkownika przed przeprowadzeniem wyszukiwania i zaktualizowaniem nagłówka.
Aktualizowanie pliku JavaScript
Zaktualizuj plik script.js
ostateczną wersją kodu. Obejmuje to nowe metody toTitleCase
i updateResultsHeader
oraz zaktualizowaną metodę performSearch
, która je integruje.
// 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();
});
Sprawdzanie efektów pracy
Zapisz plik script.js
i odśwież stronę.
Sprawdź funkcje:
- W polu wyszukiwania wpisz
pizza
(wszystkie małe litery) i kliknij Szukaj. Tekst w polu powinien zmienić się na „Pizza”, a nagłówek na pasku bocznym powinien zostać zaktualizowany i wyświetlać „Pizza w pobliżu Nowego Jorku”. - Przesuń mapę do innego miasta, np. Bostonu, i wyszukaj ponownie. Nagłówek powinien zmienić się na „Pizza w pobliżu Bostonu”.
9. Gratulacje
Udało Ci się utworzyć kompletną, interaktywną aplikację do wyszukiwania lokalnego, która łączy prostotę interfejsu Places UI Kit z możliwościami podstawowych interfejsów API Google Maps Platform w JavaScript.
Czego się dowiedziałeś
- Jak skonstruować aplikację do mapowania za pomocą klasy JavaScript do zarządzania stanem i logiką.
- Jak używać interfejsu Places UI Kit z interfejsem Google Maps JavaScript API do szybkiego tworzenia interfejsu.
- Jak programowo dodawać zaawansowane znaczniki i nimi zarządzać, aby wyświetlać na mapie niestandardowe ciekawe miejsca.
- Jak używać usługi geokodowania, aby przekształcać współrzędne w czytelne dla użytkowników adresy, co zwiększa wygodę korzystania z usługi.
- Jak identyfikować i naprawiać typowe sytuacje wyścigu w aplikacji interaktywnej za pomocą flag stanu i upewniać się, że właściwości komponentów są prawidłowo aktualizowane.
Co dalej?
- Dowiedz się więcej o dostosowywaniu zaawansowanych znaczników przez zmianę ich koloru, skali lub użycie niestandardowego kodu HTML.
- Poznaj definiowanie stylów map w Google Cloud, aby dostosować wygląd i styl mapy do swojej marki.
- Spróbuj dodać bibliotekę rysowania, aby umożliwić użytkownikom rysowanie kształtów na mapie w celu określania obszarów wyszukiwania.
- Pomóż nam tworzyć treści, które będą dla Ciebie najbardziej przydatne, i wypełnij poniższą ankietę:
Jakie inne codelaby chcesz zobaczyć?
Nie możesz znaleźć warsztatów, które Cię najbardziej interesują? Zgłoś problem tutaj