1. Avant de commencer
Cet atelier de programmation vous explique comment créer une application de recherche locale entièrement interactive à l'aide du kit UI Places de Google Maps Platform.
Prérequis
- Un projet Google Cloud avec les API et les identifiants nécessaires configurés.
- Connaissances de base en HTML et en CSS
- Comprendre le JavaScript moderne
- Un navigateur Web récent, tel que la dernière version de Chrome.
- Un éditeur de texte de votre choix.
Objectifs de l'atelier
- Structurez une application de cartographie à l'aide d'une classe JavaScript.
- Utiliser des composants Web pour afficher une carte
- Utilisez l'élément Place Search pour effectuer une recherche de texte et afficher les résultats.
- Créez et gérez par programmation des repères de carte
AdvancedMarkerElement
personnalisés. - Affichez l'élément "Détails du lieu" lorsqu'un utilisateur sélectionne un lieu.
- Utilisez l'API Geocoding pour créer une interface dynamique et conviviale.
Prérequis
- Un projet Google Cloud avec facturation activée
- Une clé API Google Maps Platform
- Un ID de carte
- Les API suivantes sont activées :
- API Maps JavaScript
- Kit UI Places
- API Geocoding
2. Configuration
Pour l'étape suivante, vous devez activer l'API Maps JavaScript, le kit d'interface utilisateur Places et l'API Geocoding.
Configurer Google Maps Platform
Si vous ne disposez pas encore d'un compte Google Cloud Platform et d'un projet pour lequel la facturation est activée, consultez le guide Premiers pas avec Google Maps Platform pour savoir comment créer un compte de facturation et un projet.
- Dans Cloud Console, cliquez sur le menu déroulant des projets, puis sélectionnez celui que vous souhaitez utiliser pour cet atelier de programmation.
- Activez les API et les SDK Google Maps Platform requis pour cet atelier de programmation dans Google Cloud Marketplace. Pour ce faire, suivez les étapes indiquées dans cette vidéo ou dans cette documentation.
- Générez une clé API sur la page Identifiants de Cloud Console. Vous pouvez suivre la procédure décrite dans cette vidéo ou dans cette documentation. Toutes les requêtes envoyées à Google Maps Platform nécessitent une clé API.
3. Le shell d'application et une carte fonctionnelle
Dans cette première étape, nous allons créer la mise en page visuelle complète de notre application et établir une structure propre basée sur des classes pour notre JavaScript. Cela nous donne une base solide sur laquelle nous pouvons nous appuyer. À la fin de cette section, vous disposerez d'une page stylisée affichant une carte interactive.
Créer le fichier HTML
Commencez par créer un fichier nommé index.html
. Ce fichier contiendra la structure complète de notre application, y compris l'en-tête, les filtres de recherche, la barre latérale, le conteneur de carte et les composants Web nécessaires.
Copiez le code suivant dans index.html
. Veillez à remplacer YOUR_API_KEY_HERE
par votre propre clé API Google Maps Platform et DEMO_MAP_ID
par votre propre ID de carte 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>
Créer le fichier CSS
Ensuite, créez un fichier nommé style.css
. Nous allons maintenant ajouter tous les styles nécessaires pour établir un look épuré et moderne dès le début. Ce CSS gère la mise en page globale, les couleurs, les polices et l'apparence de tous nos éléments d'interface utilisateur.
Copiez le code suivant dans 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);
}
Créer la classe d'application JavaScript
Enfin, créez un fichier nommé script.js
. Nous allons structurer notre application dans une classe JavaScript appelée PlaceFinderApp
. Cela permet d'organiser notre code et de gérer l'état de manière claire.
Ce code initial définira la classe, trouvera tous nos éléments HTML dans le constructor
et créera une méthode init()
pour charger les bibliothèques Google Maps Platform.
Copiez le code suivant dans 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();
});
Restrictions de clé API
Pour que cet atelier de programmation fonctionne, vous devrez peut-être ajouter une restriction à votre clé API. Pour en savoir plus et obtenir des conseils sur la façon de procéder, consultez Restreindre vos clés API.
Vérifiez votre travail
Ouvrez le fichier index.html
dans votre navigateur Web. Vous devriez voir une page avec un en-tête contenant une barre de recherche et des filtres, une barre latérale avec le message "Vos résultats de recherche s'afficheront ici" et une grande carte centrée sur la ville de New York. À ce stade, les commandes de recherche ne sont pas encore fonctionnelles.
4. Implémenter une fonction de recherche
Dans cette section, nous allons donner vie à notre application en implémentant la fonctionnalité de recherche principale. Nous allons écrire le code qui s'exécute lorsqu'un utilisateur clique sur le bouton "Rechercher". Nous allons créer cette fonction en suivant les bonnes pratiques dès le début pour gérer les interactions utilisateur de manière fluide et éviter les bugs courants tels que les conditions de concurrence.
À la fin de cette étape, vous pourrez cliquer sur le bouton de recherche et voir un indicateur de chargement s'afficher pendant que l'application récupère les données en arrière-plan.
Créer la méthode de recherche
Tout d'abord, définissez la méthode performSearch
dans notre classe PlaceFinderApp
. Cette fonction sera au cœur de notre logique de recherche. Nous allons également introduire une variable d'instance, isSearchInProgress
, pour servir de "contrôleur d'accès". Cela empêche l'utilisateur de lancer une nouvelle recherche alors qu'une autre est déjà en cours, ce qui peut entraîner des erreurs.
La logique à l'intérieur de performSearch
peut sembler complexe. Nous allons donc la décomposer :
- Elle vérifie d'abord si une recherche est déjà en cours. Si c'est le cas, il ne se passe rien.
- Il définit l'indicateur
isSearchInProgress
surtrue
pour "verrouiller" la fonction. - Il affiche le spinner de chargement et prépare l'UI pour de nouveaux résultats.
- Elle définit la propriété
textQuery
de la requête de recherche surnull
. Il s'agit d'une étape cruciale qui oblige le composant Web à reconnaître qu'une nouvelle requête est en cours. - Il utilise un
setTimeout
avec un délai0
. Cette technique JavaScript standard planifie l'exécution du reste de notre code dans la prochaine tâche du navigateur, en s'assurant que le composant a d'abord traité la valeurnull
. Même si l'utilisateur recherche exactement la même chose deux fois, une nouvelle recherche sera toujours déclenchée.
Ajouter des écouteurs d'événements
Ensuite, nous devons appeler notre méthode performSearch
lorsque l'utilisateur interagit avec l'application. Nous allons créer une méthode attachEventListeners
pour regrouper tout notre code de gestion des événements. Pour l'instant, nous allons simplement ajouter un écouteur pour l'événement click
du bouton de recherche. Nous allons également ajouter un espace réservé pour un autre événement, gmp-load
, que nous utiliserons à l'étape suivante.
Mettre à jour le fichier JavaScript
Mettez à jour votre fichier script.js
avec le code suivant. Les sections nouvelles ou modifiées sont la méthode attachEventListeners
et la méthode 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();
});
Vérifiez votre travail
Enregistrez votre fichier script.js
et actualisez index.html
dans votre navigateur. La page devrait être identique à celle affichée précédemment. Cliquez ensuite sur le bouton "Rechercher" dans l'en-tête.
Deux choses devraient se produire :
- Le message d'espace réservé "Vos résultats de recherche apparaîtront ici" disparaît.
- L'icône de chargement s'affiche et continue de tourner.
Le spinner tournera indéfiniment, car nous ne lui avons pas encore dit quand s'arrêter. Nous le ferons dans la section suivante, lorsque nous afficherons les résultats. Cela confirme que notre fonction de recherche est déclenchée correctement.
5. Afficher les résultats et ajouter des repères
Maintenant que le déclencheur de recherche fonctionne, la prochaine tâche consiste à afficher les résultats à l'écran. Le code de cette section connectera la logique de recherche à l'UI. Une fois que l'élément de recherche de lieux a fini de charger les données, il déverrouille la recherche, masque le spinner de chargement et affiche un repère sur la carte pour chaque résultat.
Écouter la fin de la recherche
L'élément Place Search déclenche un événement gmp-load
lorsqu'il a récupéré les données. C'est le signal idéal pour que nous puissions traiter les résultats.
Tout d'abord, ajoutez un écouteur d'événements pour cet événement dans notre méthode attachEventListeners
.
Créer des méthodes de gestion des repères
Nous allons ensuite créer deux méthodes d'assistance : clearMarkers
et addMarkers
.
clearMarkers()
supprimera tous les repères d'une recherche précédente.addMarkers()
sera appelé par notre écouteurgmp-load
. Il parcourra la liste des lieux renvoyés par la recherche et créera unAdvancedMarkerElement
pour chacun d'eux. C'est également là que nous allons masquer l'icône de chargement et déverrouiller leisSearchInProgress
, ce qui termine le cycle de recherche.
Notez que nous stockons les repères dans un objet (this.markers
) en utilisant l'ID du lieu comme clé. Cela nous permettra de gérer les repères et de retrouver un repère spécifique ultérieurement.
Enfin, nous devons appeler clearMarkers()
au début de chaque nouvelle recherche. Le meilleur endroit pour cela est dans performSearch
.
Mettre à jour le fichier JavaScript
Mettez à jour votre fichier script.js
avec les nouvelles méthodes et les modifications apportées à attachEventListeners
et 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();
});
Vérifiez votre travail
Enregistrez vos fichiers et actualisez la page dans votre navigateur. Cliquez sur le bouton "Rechercher".
L'icône de chargement devrait maintenant apparaître un instant, puis disparaître. La barre latérale se remplit avec une liste de lieux correspondant au terme de recherche, et des repères correspondants devraient apparaître sur la carte. Pour l'instant, les repères ne font rien lorsqu'on clique dessus. Nous ajouterons cette interactivité dans la section suivante.
6. Activer les filtres de recherche et l'interactivité de la liste
Notre application peut désormais afficher des résultats de recherche, mais elle n'est pas encore interactive. Dans cette section, nous allons donner vie à tous les contrôles utilisateur. Nous allons activer les filtres, permettre la recherche avec la touche "Entrée" et associer les éléments de la liste de résultats à leurs emplacements correspondants sur la carte.
À la fin de cette étape, l'application sera entièrement réactive aux saisies de l'utilisateur.
Activer les filtres de recherche
Tout d'abord, la méthode performSearch
sera mise à jour pour lire les valeurs de tous les contrôles de filtre dans l'en-tête. Pour chaque filtre (prix, note et "Ouvert actuellement"), la propriété correspondante sera définie sur l'objet searchRequest
avant l'exécution de la recherche.
Ajouter des écouteurs d'événements pour tous les contrôles
Nous allons ensuite étendre notre méthode attachEventListeners
. Nous allons ajouter des écouteurs pour l'événement change
sur chaque commande de filtre, ainsi qu'un écouteur keydown
sur le champ de recherche pour détecter lorsque l'utilisateur appuie sur la touche "Entrée". Tous ces nouveaux écouteurs appelleront la méthode performSearch
.
Associer la liste des résultats à la carte
Pour créer une expérience fluide, lorsque l'utilisateur clique sur un élément de la liste des résultats de la barre latérale, la carte doit se recentrer sur cet emplacement.
Une nouvelle méthode, handleResultClick
, écoutera l'événement gmp-select
, qui est déclenché par l'élément de recherche de lieux lorsqu'un élément est sélectionné. Cette fonction permet de trouver l'emplacement du lieu associé et de faire défiler la carte jusqu'à celui-ci.
Pour que cela fonctionne, assurez-vous que l'attribut selectable
est présent sur votre composant gmp-place-search
dans 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>
Mettre à jour le fichier JavaScript
Mettez à jour votre fichier script.js
avec le code complet suivant. Cette version inclut la nouvelle méthode handleResultClick
et la logique mise à jour dans attachEventListeners
et 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();
});
Vérifiez votre travail
Enregistrez votre fichier script.js
et actualisez la page. L'application doit maintenant être très interactive.
Effectuez les vérifications suivantes :
- La recherche fonctionne lorsque vous appuyez sur "Entrée" dans le champ de recherche.
- Si vous modifiez l'un des filtres (prix, note, ouvert actuellement), une nouvelle recherche est lancée et les résultats sont mis à jour.
- Lorsque vous cliquez sur un élément de la liste dans la barre latérale, la carte se déplace de manière fluide vers l'emplacement de cet élément.
Dans la section suivante, nous allons implémenter la fiche d'informations qui s'affiche lorsqu'un repère est sélectionné.
7. Implémenter l'élément Place Details
Notre application est désormais entièrement interactive, mais il lui manque une fonctionnalité clé : la possibilité d'afficher plus d'informations sur un lieu sélectionné. Dans cette section, nous allons implémenter l'élément "Détails du lieu" qui s'affichera lorsqu'un utilisateur cliquera sur un repère sur la carte ou sélectionnera un élément dans l'élément "Recherche de lieux".
Créer un conteneur de fiche d'informations réutilisable
Le moyen le plus efficace d'afficher les détails d'un lieu sur la carte consiste à créer un conteneur unique et réutilisable. Nous allons utiliser un AdvancedMarkerElement
comme conteneur. Son contenu sera le widget gmp-place-details-compact
masqué que nous avons déjà dans notre index.html
.
Une nouvelle méthode, initDetailsPopup
, gérera la création de ce marqueur réutilisable. Il sera créé une seule fois lors du chargement de l'application et sera masqué au départ. Nous ajouterons également un écouteur à la carte principale dans cette méthode, afin que le fait de cliquer n'importe où sur la carte masque la fiche d'informations.
Mettre à jour le comportement en cas de clic sur le repère
Ensuite, nous devons mettre à jour ce qui se passe lorsqu'un utilisateur clique sur un repère. Le listener 'click'
à l'intérieur de la méthode addMarkers
sera désormais responsable de l'affichage de la fiche d'informations.
Lorsqu'un utilisateur clique sur un repère, l'écouteur :
- Faites glisser la carte jusqu'à l'emplacement du repère.
- Mettez à jour la fiche d'informations avec les informations concernant ce lieu spécifique.
- Positionnez la fiche d'informations à l'emplacement du repère et rendez-la visible.
Associer le clic sur la liste au clic sur le repère
Enfin, nous allons mettre à jour la méthode handleResultClick
. Au lieu de simplement faire glisser la carte, il déclenchera désormais par programmation l'événement click
sur le repère correspondant. Il s'agit d'un modèle puissant qui nous permet de réutiliser exactement la même logique pour les deux interactions, ce qui permet de garder notre code propre et facile à gérer.
Mettre à jour le fichier JavaScript
Mettez à jour votre fichier script.js
avec le code suivant. Les sections nouvelles ou modifiées sont la méthode initDetailsPopup
et les méthodes addMarkers
et handleResultClick
mises à jour.
// 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();
});
Vérifiez votre travail
Enregistrez votre fichier script.js
et actualisez la page. L'application devrait maintenant afficher les détails à la demande.
Effectuez les vérifications suivantes :
- Lorsque vous cliquez sur un repère sur la carte, celle-ci est désormais centrée et une fiche d'informations stylisée s'ouvre au-dessus du repère.
- Cliquer sur un élément de la liste des résultats dans la barre latérale a exactement le même effet.
- Si vous cliquez sur la carte en dehors de la fiche, celle-ci se ferme.
- Le lancement d'une nouvelle recherche ferme également toute fiche d'informations ouverte.
8. Peaufiner le résultat
Notre application est désormais entièrement fonctionnelle, mais nous pouvons ajouter quelques touches finales pour améliorer encore l'expérience utilisateur. Dans cette dernière section, nous allons implémenter deux fonctionnalités clés : un en-tête dynamique qui fournit un meilleur contexte pour les résultats de recherche et une mise en forme automatique pour la requête de recherche de l'utilisateur.
Créer un en-tête de résultats dynamiques
Pour le moment, l'en-tête de la barre latérale indique toujours "Résultats". Nous pouvons rendre cette information plus utile en la mettant à jour pour refléter la recherche en cours. Par exemple, "Burgers près de New York".
Pour ce faire, nous utiliserons l'API Geocoding afin de convertir les coordonnées du centre de la carte en un emplacement lisible, comme un nom de ville. Une nouvelle méthode async
, updateResultsHeader
, gérera cette logique. Il sera appelé à chaque fois qu'une recherche sera effectuée.
Mettre en forme la requête de recherche de l'utilisateur
Pour que l'UI soit propre et cohérente, nous mettrons automatiquement en forme le terme de recherche de l'utilisateur en "Title Case" (par exemple, "restaurant de burgers" devient "Restaurant de burgers"). Une fonction d'assistance, toTitleCase
, gérera cette transformation. La méthode performSearch
sera mise à jour pour utiliser cette fonction sur l'entrée de l'utilisateur avant d'effectuer la recherche et de mettre à jour l'en-tête.
Mettre à jour le fichier JavaScript
Mettez à jour votre fichier script.js
avec la version finale du code. Cela inclut les nouvelles méthodes toTitleCase
et updateResultsHeader
, ainsi que la méthode performSearch
mise à jour qui les intègre.
// 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();
});
Vérifiez votre travail
Enregistrez votre fichier script.js
et actualisez la page.
Vérifiez les fonctionnalités :
- Saisissez
pizza
(en minuscules) dans le champ de recherche, puis cliquez sur "Rechercher". Le texte de la zone doit être remplacé par "Pizza", et l'en-tête de la barre latérale doit être remplacé par "Pizza près de New York". - Fais glisser la carte vers une autre ville, comme Boston, puis effectue une nouvelle recherche. L'en-tête devrait devenir "Pizzas à proximité de Boston".
9. Félicitations
Vous venez de créer une application de recherche locale complète et interactive qui combine la simplicité du Kit UI pour Places et la puissance des API JavaScript Google Maps Platform de base.
Ce que vous avez appris
- Comment structurer une application de cartographie à l'aide d'une classe JavaScript pour gérer l'état et la logique.
- Découvrez comment utiliser le Kit UI pour Places avec l'API Maps JavaScript pour développer rapidement une UI.
- Découvrez comment ajouter et gérer de manière programmatique des repères avancés pour afficher des points d'intérêt personnalisés sur la carte.
- Découvrez comment utiliser le service de géocodage pour convertir des coordonnées en adresses lisibles et améliorer ainsi l'expérience utilisateur.
- Découvrez comment identifier et corriger les conditions de concurrence courantes dans une application interactive à l'aide d'indicateurs d'état et en vous assurant que les propriétés des composants sont correctement mises à jour.
Étape suivante
- Découvrez comment personnaliser les repères avancés en modifiant leur couleur ou leur échelle, ou même en utilisant du code HTML personnalisé.
- Découvrez la personnalisation de cartes dans Google Cloud pour adapter l'apparence de votre carte à votre marque.
- Essayez d'ajouter la bibliothèque de dessins pour permettre aux utilisateurs de dessiner des formes sur la carte afin de définir des zones de recherche.
- Aidez-nous à créer le contenu qui vous semble le plus utile en répondant à l'enquête suivante :
Quels autres ateliers de programmation souhaiteriez-vous voir ?
Vous ne trouvez pas l'atelier de programmation qui vous intéresse le plus ? Demandez-le en décrivant un nouveau problème ici.