Build a local search app with Places UI Kit

1. Before you begin

This codelab teaches you how to build a fully interactive local search application by using Google Maps Platform Places UI Kit.

A screenshot of the finished PlaceFinder application, showing a map of NewYork with markers, a sidebar with search results, and a details cardopen.

Prerequisites

  • A Google Cloud project with the necessary APIs and credentials configured.
  • Basic knowledge of HTML and CSS.
  • Understanding of modern JavaScript.
  • A modern web browser, such as the latest version of Chrome.
  • A text editor of your choice.

What you'll do

  • Structure a mapping application using a JavaScript class.
  • Use Web Components to display a map
  • Use Place Search Element to perform and display the results of a Text Search.
  • Programmatically create and manage custom AdvancedMarkerElement map markers.
  • Display Place Details Element when a user selects a location.
  • Use the Geocoding API to create a dynamic and user-friendly interface.

What you'll need

  • A Google Cloud project with billing enabled
  • A Google Maps Platform API key
  • A Map ID
  • The following APIs enabled:
    • Maps JavaScript API
    • Places UI Kit
    • Geocoding API

2. Get set up

For the following enablement step, you'll need to enable the Maps JavaScript API, Places UI Kit, and Geocoding API.

Set up Google Maps Platform

If you do not already have a Google Cloud Platform account and a project with billing enabled, please see the Getting Started with Google Maps Platform guide to create a billing account and a project.

  1. In the Cloud Console , click the project drop-down menu and select the project that you want to use for this codelab.

  1. Enable the Google Maps Platform APIs and SDKs required for this codelab in the Google Cloud Marketplace . To do so, follow the steps in this video or this documentation .
  2. Generate an API key in the Credentials page of Cloud Console. You can follow the steps in this video or this documentation . All requests to Google Maps Platform require an API key.

3. The application shell and a functional map

In this first step, we will create the complete visual layout for our application and establish a clean, class-based structure for our JavaScript. This gives us a solid foundation to build upon. By the end of this section, you will have a styled page displaying an interactive map.

Create the HTML file

First, create a file named index.html . This file will contain the complete structure of our application, including the header, search filters, sidebar, map container, and the necessary web components.

Copy the following code into index.html . Be sure to replace YOUR_API_KEY_HERE with your own Google Maps Platform API key, and DEMO_MAP_ID with your own Google Maps Platform Map ID.

 < ! 
 DOCTYPE 
  
 html 
>
< html 
  
 lang 
 = 
 "en" 
>  
< head 
>  
< title>Local 
  
 Search 
  
 App 
< / 
 title 
>  
< meta 
  
 charset 
 = 
 "utf-8" 
  
 / 
>  
< meta 
  
 name 
 = 
 "viewport" 
  
 content 
 = 
 "width=device-width, initial-scale=1.0" 
  
 / 
>  
< !-- 
  
 Google 
  
 Fonts 
 : 
  
 Roboto 
  
 -- 
>  
< link 
  
 rel 
 = 
 "preconnect" 
  
 href 
 = 
 "https://fonts.googleapis.com" 
>  
< link 
  
 rel 
 = 
 "preconnect" 
  
 href 
 = 
 "https://fonts.gstatic.com" 
  
 crossorigin 
>  
< link 
  
 href 
 = 
 "https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" 
  
 rel 
 = 
 "stylesheet" 
>  
< !-- 
  
 GMP 
  
 Bootstrap 
  
 Loader 
  
 -- 
>  
< script 
>  
 ( 
 g 
 = 
> { 
 var 
  
 h 
 , 
 a 
 , 
 k 
 , 
 p 
 = 
 "The Google Maps JavaScript API" 
 , 
 c 
 = 
 "google" 
 , 
 l 
 = 
 "importLibrary" 
 , 
 q 
 = 
 "__ib__" 
 , 
 m 
 = 
 document 
 , 
 b 
 = 
 window 
 ; 
 b 
 = 
 b 
 [ 
 c 
 ] 
 || 
 ( 
 b 
 [ 
 c 
 ] 
 = 
 {}); 
 var 
  
 d 
 = 
 b 
 . 
 maps 
 || 
 ( 
 b 
 . 
 maps 
 = 
 {}), 
 r 
 = 
 new 
  
 Set 
 , 
 e 
 = 
 new 
  
 URLSearchParams 
 , 
 u 
 = 
 () 
 = 
> h 
 || 
 ( 
 h 
 = 
 new 
  
 Promise 
 ( 
 async 
 ( 
 f 
 , 
 n 
 ) 
 = 
> { 
 await 
  
 ( 
 a 
 = 
 m 
 . 
 createElement 
 ( 
 "script" 
 )); 
 e 
 . 
 set 
 ( 
 "libraries" 
 ,[... 
 r 
 ] 
 + 
 "" 
 ); 
 for 
 ( 
 k 
  
 in 
  
 g 
 ) 
 e 
 . 
 set 
 ( 
 k 
 . 
 replace 
 ( 
 / 
 [ 
 A 
 - 
 Z 
 ] 
 / 
 g 
 , 
 t 
 = 
> "_" 
 + 
 t 
 [ 
 0 
 ]. 
 toLowerCase 
 ()), 
 g 
 [ 
 k 
 ]); 
 e 
 . 
 set 
 ( 
 "callback" 
 , 
 c 
 + 
 ".maps." 
 + 
 q 
 ); 
 a 
 . 
 src 
 = 
 ` 
 https 
 : 
 //maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({ 
  
 key 
 : 
  
 "YOUR_API_KEY_HERE" 
 , 
  
 v 
 : 
  
 "weekly" 
 , 
  
 libraries 
 : 
  
 "places,maps,marker,geocoding" 
  
 }); 
  
< / 
 script 
>  
< link 
  
 rel 
 = 
 "stylesheet" 
  
 type 
 = 
 "text/css" 
  
 href 
 = 
 "style.css" 
  
 / 
>  
< / 
 head 
>  
< body 
>  
< !-- 
  
 Header 
  
 for 
  
 search 
  
 controls 
  
 -- 
>  
< header 
  
 class 
 = 
 "top-header" 
>  
< div 
  
 class 
 = 
 "logo" 
>  
< svg 
  
 viewBox 
 = 
 "0 0 24 24" 
  
 width 
 = 
 "28" 
  
 height 
 = 
 "28" 
>< path 
  
 d 
 = 
 "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" 
  
 fill 
 = 
 "currentColor" 
>< / 
 path 
>< / 
 svg 
>  
< span>PlaceFinder 
< / 
 span 
>  
< / 
 div 
>  
< div 
  
 class 
 = 
 "search-container" 
>  
< input 
  
 type 
 = 
 "text" 
  
 id 
 = 
 "query-input" 
  
 placeholder 
 = 
 "e.g., burger in New York" 
  
 value 
 = 
 "burger" 
  
 / 
>  
< button 
  
 id 
 = 
 "search-button" 
  
 aria 
 - 
 label 
 = 
 "Search" 
> Search 
< / 
 button 
>  
< / 
 div 
>  
< div 
  
 class 
 = 
 "filter-container" 
>  
< label 
  
 class 
 = 
 "open-now-label" 
>  
< input 
  
 type 
 = 
 "checkbox" 
  
 id 
 = 
 "open-now-filter" 
>  
 Open 
  
 Now 
  
< / 
 label 
>  
< select 
  
 id 
 = 
 "rating-filter" 
  
 aria 
 - 
 label 
 = 
 "Minimum rating" 
>  
< option 
  
 value 
 = 
 "0" 
  
 selected>Any 
  
 rating 
< / 
 option 
>  
< option 
  
 value 
 = 
 "1" 
> 1 
 + 
  
  
< / 
 option 
>  
< option 
  
 value 
 = 
 "2" 
> 2 
 + 
  
 ★★ 
< / 
 option 
>  
< option 
  
 value 
 = 
 "3" 
> 3 
 + 
  
 ★★★ 
< / 
 option 
>  
< option 
  
 value 
 = 
 "4" 
> 4 
 + 
  
 ★★★★ 
< / 
 option 
>  
< option 
  
 value 
 = 
 "5" 
> 5 
  
 ★★★★★ 
< / 
 option 
>  
< / 
 select 
>  
< select 
  
 id 
 = 
 "price-filter" 
  
 aria 
 - 
 label 
 = 
 "Price level" 
>  
< option 
  
 value 
 = 
 "0" 
  
 selected>Any 
  
 Price 
< / 
 option 
>  
< option 
  
 value 
 = 
 "1" 
> $ 
< / 
 option 
>  
< option 
  
 value 
 = 
 "2" 
> $$ 
< / 
 option 
>  
< option 
  
 value 
 = 
 "3" 
> $$$ 
< / 
 option 
>  
< option 
  
 value 
 = 
 "4" 
> $$$$ 
< / 
 option 
>  
< / 
 select 
>  
< / 
 div 
>  
< / 
 header 
>  
< !-- 
  
 Main 
  
 content 
  
 area 
  
 -- 
>  
< div 
  
 class 
 = 
 "app-container" 
>  
< !-- 
  
 Left 
  
 Panel 
 : 
  
 Results 
  
 -- 
>  
< div 
  
 class 
 = 
 "sidebar" 
>  
< div 
  
 class 
 = 
 "results-header" 
>  
< h2 
  
 id 
 = 
 "results-header-text" 
> Results 
< / 
 h2 
>  
< / 
 div 
>  
< div 
  
 class 
 = 
 "results-container" 
>  
< gmp 
 - 
 place 
 - 
 search 
  
 id 
 = 
 "place-search-list" 
  
 class 
 = 
 "hidden" 
  
 selectable 
>  
< gmp 
 - 
 place 
 - 
 all 
 - 
 content 
>< / 
 gmp 
 - 
 place 
 - 
 all 
 - 
 content 
>  
< gmp 
 - 
 place 
 - 
 text 
 - 
 search 
 - 
 request 
>< / 
 gmp 
 - 
 place 
 - 
 text 
 - 
 search 
 - 
 request 
>  
< / 
 gmp 
 - 
 place 
 - 
 search 
>  
< div 
  
 id 
 = 
 "placeholder-message" 
  
 class 
 = 
 "placeholder" 
>  
< p>Your 
  
 search 
  
 results 
  
 will 
  
 appear 
  
 here 
 . 
< / 
 p 
>  
< / 
 div 
>  
< div 
  
 id 
 = 
 "loading-spinner" 
  
 class 
 = 
 "spinner-overlay" 
>  
< div 
  
 class 
 = 
 "spinner" 
>< / 
 div 
>  
< / 
 div 
>  
< / 
 div 
>  
< / 
 div 
>  
< !-- 
  
 Right 
  
 Panel 
 : 
  
 Map 
  
 -- 
>  
< div 
  
 class 
 = 
 "map-container" 
>  
< gmp 
 - 
 map 
  
 center 
 = 
 "40.758896,-73.985130" 
  
 zoom 
 = 
 "13" 
  
 map 
 - 
 id 
 = 
 "DEMO_MAP_ID" 
  
>  
< / 
 gmp 
 - 
 map 
>  
< div 
  
 id 
 = 
 "details-container" 
>  
< gmp 
 - 
 place 
 - 
 details 
 - 
 compact 
>  
< gmp 
 - 
 place 
 - 
 details 
 - 
 place 
 - 
 request 
>< / 
 gmp 
 - 
 place 
 - 
 details 
 - 
 place 
 - 
 request 
>  
< gmp 
 - 
 place 
 - 
 all 
 - 
 content 
>< / 
 gmp 
 - 
 place 
 - 
 all 
 - 
 content 
>  
< / 
 gmp 
 - 
 place 
 - 
 details 
 - 
 compact 
>  
< / 
 div 
>  
< / 
 div 
>  
< / 
 div 
>  
< script 
  
 src 
 = 
 "script.js" 
>< / 
 script 
>  
< / 
 body 
>
< / 
 html 
> 

Create the CSS file

Next, create a file named style.css . We will add all the necessary styling now to establish a clean, modern look from the start. This CSS handles the overall layout, colors, fonts, and the appearance of all our UI elements.

Copy the following code into 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 
 : 
  
 100 
 vh 
 ; 
  
 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 
 : 
  
 12 
 px 
  
 24 
 px 
 ; 
  
 border 
 - 
 bottom 
 : 
  
 1 
 px 
  
 solid 
  
 var 
 ( 
 -- 
 border 
 - 
 color 
 ); 
  
 background 
 - 
 color 
 : 
  
 var 
 ( 
 -- 
 panel 
 - 
 background 
 ); 
  
 gap 
 : 
  
 24 
 px 
 ; 
  
 flex 
 - 
 shrink 
 : 
  
 0 
 ; 
 } 
 . 
 logo 
  
 { 
  
 display 
 : 
  
 flex 
 ; 
  
 align 
 - 
 items 
 : 
  
 center 
 ; 
  
 gap 
 : 
  
 8 
 px 
 ; 
  
 font 
 - 
 size 
 : 
  
 22 
 px 
 ; 
  
 font 
 - 
 weight 
 : 
  
 700 
 ; 
  
 color 
 : 
  
 var 
 ( 
 -- 
 primary 
 - 
 color 
 ); 
 } 
 . 
 search 
 - 
 container 
  
 { 
  
 display 
 : 
  
 flex 
 ; 
  
 flex 
 - 
 grow 
 : 
  
 1 
 ; 
  
 max 
 - 
 width 
 : 
  
 720 
 px 
 ; 
 } 
 . 
 search 
 - 
 container 
  
 input 
  
 { 
  
 width 
 : 
  
 100 
 % 
 ; 
  
 padding 
 : 
  
 12 
 px 
  
 16 
 px 
 ; 
  
 border 
 : 
  
 1 
 px 
  
 solid 
  
 var 
 ( 
 -- 
 border 
 - 
 color 
 ); 
  
 border 
 - 
 radius 
 : 
  
 8 
 px 
  
 0 
  
 0 
  
 8 
 px 
 ; 
  
 font 
 - 
 size 
 : 
  
 16 
 px 
 ; 
  
 transition 
 : 
  
 box 
 - 
 shadow 
  
 0.2 
 s 
  
 ease 
 ; 
 } 
 . 
 search 
 - 
 container 
  
 input 
 : 
 focus 
  
 { 
  
 outline 
 : 
  
 none 
 ; 
  
 border 
 - 
 color 
 : 
  
 var 
 ( 
 -- 
 primary 
 - 
 color 
 ); 
  
 box 
 - 
 shadow 
 : 
  
 0 
  
 0 
  
 0 
  
 2 
 px 
  
 rgba 
 ( 
 26 
 , 
  
 115 
 , 
  
 232 
 , 
  
 0.2 
 ); 
 } 
 . 
 search 
 - 
 container 
  
 button 
  
 { 
  
 padding 
 : 
  
 0 
  
 20 
 px 
 ; 
  
 border 
 : 
  
 1 
 px 
  
 solid 
  
 var 
 ( 
 -- 
 primary 
 - 
 color 
 ); 
  
 border 
 - 
 radius 
 : 
  
 0 
  
 8 
 px 
  
 8 
 px 
  
 0 
 ; 
  
 background 
 - 
 color 
 : 
  
 var 
 ( 
 -- 
 primary 
 - 
 color 
 ); 
  
 color 
 : 
  
 white 
 ; 
  
 cursor 
 : 
  
 pointer 
 ; 
  
 font 
 - 
 size 
 : 
  
 16 
 px 
 ; 
  
 font 
 - 
 weight 
 : 
  
 500 
 ; 
  
 transition 
 : 
  
 background 
 - 
 color 
  
 0.2 
 s 
  
 ease 
 ; 
 } 
 . 
 search 
 - 
 container 
  
 button 
 : 
 hover 
  
 { 
  
 background 
 - 
 color 
 : 
  
 #185abc; 
 } 
 . 
 filter 
 - 
 container 
  
 { 
  
 display 
 : 
  
 flex 
 ; 
  
 gap 
 : 
  
 12 
 px 
 ; 
  
 align 
 - 
 items 
 : 
  
 center 
 ; 
 } 
 . 
 filter 
 - 
 container 
  
 select 
 , 
  
 . 
 open 
 - 
 now 
 - 
 label 
  
 { 
  
 padding 
 : 
  
 10 
 px 
  
 14 
 px 
 ; 
  
 border 
 : 
  
 1 
 px 
  
 solid 
  
 var 
 ( 
 -- 
 border 
 - 
 color 
 ); 
  
 border 
 - 
 radius 
 : 
  
 8 
 px 
 ; 
  
 background 
 - 
 color 
 : 
  
 var 
 ( 
 -- 
 panel 
 - 
 background 
 ); 
  
 font 
 - 
 size 
 : 
  
 14 
 px 
 ; 
  
 cursor 
 : 
  
 pointer 
 ; 
  
 transition 
 : 
  
 border 
 - 
 color 
  
 0.2 
 s 
  
 ease 
 ; 
 } 
 . 
 filter 
 - 
 container 
  
 select 
 : 
 hover 
 , 
  
 . 
 open 
 - 
 now 
 - 
 label 
 : 
 hover 
  
 { 
  
 border 
 - 
 color 
 : 
  
 #c0c2c5; 
 } 
 . 
 open 
 - 
 now 
 - 
 label 
  
 { 
  
 display 
 : 
  
 flex 
 ; 
  
 align 
 - 
 items 
 : 
  
 center 
 ; 
  
 gap 
 : 
  
 8 
 px 
 ; 
  
 white 
 - 
 space 
 : 
  
 nowrap 
 ; 
 } 
 . 
 app 
 - 
 container 
  
 { 
  
 display 
 : 
  
 flex 
 ; 
  
 flex 
 - 
 grow 
 : 
  
 1 
 ; 
  
 overflow 
 : 
  
 hidden 
 ; 
 } 
 . 
 sidebar 
  
 { 
  
 width 
 : 
  
 35 
 % 
 ; 
  
 min 
 - 
 width 
 : 
  
 380 
 px 
 ; 
  
 max 
 - 
 width 
 : 
  
 480 
 px 
 ; 
  
 display 
 : 
  
 flex 
 ; 
  
 flex 
 - 
 direction 
 : 
  
 column 
 ; 
  
 border 
 - 
 right 
 : 
  
 1 
 px 
  
 solid 
  
 var 
 ( 
 -- 
 border 
 - 
 color 
 ); 
  
 background 
 - 
 color 
 : 
  
 var 
 ( 
 -- 
 panel 
 - 
 background 
 ); 
  
 overflow 
 : 
  
 hidden 
 ; 
 } 
 . 
 results 
 - 
 header 
  
 { 
  
 padding 
 : 
  
 16 
 px 
  
 24 
 px 
 ; 
  
 border 
 - 
 bottom 
 : 
  
 1 
 px 
  
 solid 
  
 var 
 ( 
 -- 
 border 
 - 
 color 
 ); 
  
 flex 
 - 
 shrink 
 : 
  
 0 
 ; 
 } 
 . 
 results 
 - 
 header 
  
 h2 
  
 { 
  
 margin 
 : 
  
 0 
 ; 
  
 font 
 - 
 size 
 : 
  
 18 
 px 
 ; 
  
 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 
 : 
  
 2 
 rem 
 ; 
  
 box 
 - 
 sizing 
 : 
  
 border 
 - 
 box 
 ; 
 } 
 . 
 placeholder 
  
 p 
  
 { 
  
 color 
 : 
  
 var 
 ( 
 -- 
 text 
 - 
 color 
 - 
 light 
 ); 
  
 font 
 - 
 size 
 : 
  
 1.1 
 rem 
 ; 
 } 
 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.3 
 s 
 , 
  
 visibility 
  
 0.3 
 s 
 ; 
 } 
 . 
 spinner 
 - 
 overlay 
 . 
 visible 
  
 { 
  
 opacity 
 : 
  
 1 
 ; 
  
 visibility 
 : 
  
 visible 
 ; 
 } 
 . 
 spinner 
  
 { 
  
 width 
 : 
  
 48 
 px 
 ; 
  
 height 
 : 
  
 48 
 px 
 ; 
  
 border 
 : 
  
 4 
 px 
  
 solid 
  
 #e0e0e0; 
  
 border 
 - 
 top 
 - 
 color 
 : 
  
 var 
 ( 
 -- 
 primary 
 - 
 color 
 ); 
  
 border 
 - 
 radius 
 : 
  
 50 
 % 
 ; 
  
 animation 
 : 
  
 spin 
  
 1 
 s 
  
 linear 
  
 infinite 
 ; 
 } 
 @ 
 keyframes 
  
 spin 
  
 { 
  
 to 
  
 { 
  
 transform 
 : 
  
 rotate 
 ( 
 360 
 deg 
 ); 
  
 } 
 } 
 gmp 
 - 
 place 
 - 
 details 
 - 
 compact 
  
 { 
  
 width 
 : 
  
 350 
 px 
 ; 
  
 display 
 : 
  
 none 
 ; 
  
 border 
 : 
  
 none 
 ; 
  
 border 
 - 
 radius 
 : 
  
 12 
 px 
 ; 
  
 box 
 - 
 shadow 
 : 
  
 0 
  
 4 
 px 
  
 12 
 px 
  
 rgba 
 ( 
 0 
 , 
 0 
 , 
 0 
 , 
 0.15 
 ); 
 } 
 gmp 
 - 
 place 
 - 
 details 
 - 
 compact 
 :: 
 after 
  
 { 
  
 content 
 : 
  
 '' 
 ; 
  
 position 
 : 
  
 absolute 
 ; 
  
 bottom 
 : 
  
 - 
 12 
 px 
 ; 
  
 left 
 : 
  
 50 
 % 
 ; 
  
 transform 
 : 
  
 translateX 
 ( 
 - 
 50 
 % 
 ); 
  
 width 
 : 
  
 24 
 px 
 ; 
  
 height 
 : 
  
 12 
 px 
 ; 
  
 background 
 - 
 color 
 : 
  
 var 
 ( 
 -- 
 panel 
 - 
 background 
 ); 
  
 clip 
 - 
 path 
 : 
  
 polygon 
 ( 
 50 
 % 
  
 100 
 % 
 , 
  
 0 
  
 0 
 , 
  
 100 
 % 
  
 0 
 ); 
 } 
 

Create the JavaScript application class

Finally, create a file named script.js . We will structure our application inside a JavaScript class called PlaceFinderApp . This keeps our code organized and manages state cleanly.

This initial code will define the class, find all our HTML elements in the constructor , and create an init() method to load the Google Maps Platform libraries.

Copy the following code into script.js :

  // 
  
 script 
 . 
 js 
 class 
  
 PlaceFinderApp 
  
 { 
  
 constructor 
 () 
  
 { 
  
 // 
  
 Get 
  
 all 
  
 DOM 
  
 element 
  
 references 
  
 this 
 . 
 queryInput 
  
 = 
  
 document 
 . 
 getElementById 
 ( 
 'query-input' 
 ); 
  
 this 
 . 
 priceFilter 
  
 = 
  
 document 
 . 
 getElementById 
 ( 
 'price-filter' 
 ); 
  
 this 
 . 
 ratingFilter 
  
 = 
  
 document 
 . 
 getElementById 
 ( 
 'rating-filter' 
 ); 
  
 this 
 . 
 openNowFilter 
  
 = 
  
 document 
 . 
 getElementById 
 ( 
 'open-now-filter' 
 ); 
  
 this 
 . 
 searchButton 
  
 = 
  
 document 
 . 
 getElementById 
 ( 
 'search-button' 
 ); 
  
 this 
 . 
 placeSearch 
  
 = 
  
 document 
 . 
 getElementById 
 ( 
 'place-search-list' 
 ); 
  
 this 
 . 
 gMap 
  
 = 
  
 document 
 . 
 querySelector 
 ( 
 'gmp-map' 
 ); 
  
 this 
 . 
 loadingSpinner 
  
 = 
  
 document 
 . 
 getElementById 
 ( 
 'loading-spinner' 
 ); 
  
 this 
 . 
 resultsHeaderText 
  
 = 
  
 document 
 . 
 getElementById 
 ( 
 'results-header-text' 
 ); 
  
 this 
 . 
 placeholderMessage 
  
 = 
  
 document 
 . 
 getElementById 
 ( 
 'placeholder-message' 
 ); 
  
 this 
 . 
 placeDetailsWidget 
  
 = 
  
 document 
 . 
 querySelector 
 ( 
 'gmp-place-details-compact' 
 ); 
  
 this 
 . 
 placeDetailsRequest 
  
 = 
  
 this 
 . 
 placeDetailsWidget 
 . 
 querySelector 
 ( 
 'gmp-place-details-place-request' 
 ); 
  
 this 
 . 
 searchRequest 
  
 = 
  
 this 
 . 
 placeSearch 
 . 
 querySelector 
 ( 
 'gmp-place-text-search-request' 
 ); 
  
 // 
  
 Initialize 
  
 instance 
  
 variables 
  
 this 
 . 
 map 
  
 = 
  
 null 
 ; 
  
 this 
 . 
 geocoder 
  
 = 
  
 null 
 ; 
  
 this 
 . 
 markers 
  
 = 
  
 {}; 
  
 this 
 . 
 detailsPopup 
  
 = 
  
 null 
 ; 
  
 this 
 . 
 PriceLevel 
  
 = 
  
 null 
 ; 
  
 this 
 . 
 isSearchInProgress 
  
 = 
  
 false 
 ; 
  
 // 
  
 Start 
  
 the 
  
 application 
  
 this 
 . 
 init 
 (); 
  
 } 
  
 async 
  
 init 
 () 
  
 { 
  
 // 
  
 Import 
  
 libraries 
  
 await 
  
 google 
 . 
 maps 
 . 
 importLibrary 
 ( 
 "maps" 
 ); 
  
 const 
  
 { 
  
 Place 
 , 
  
 PriceLevel 
  
 } 
  
 = 
  
 await 
  
 google 
 . 
 maps 
 . 
 importLibrary 
 ( 
 "places" 
 ); 
  
 const 
  
 { 
  
 AdvancedMarkerElement 
  
 } 
  
 = 
  
 await 
  
 google 
 . 
 maps 
 . 
 importLibrary 
 ( 
 "marker" 
 ); 
  
 const 
  
 { 
  
 Geocoder 
  
 } 
  
 = 
  
 await 
  
 google 
 . 
 maps 
 . 
 importLibrary 
 ( 
 "geocoding" 
 ); 
  
 // 
  
 Make 
  
 classes 
  
 available 
  
 to 
  
 the 
  
 instance 
  
 this 
 . 
 PriceLevel 
  
 = 
  
 PriceLevel 
 ; 
  
 this 
 . 
 AdvancedMarkerElement 
  
 = 
  
 AdvancedMarkerElement 
 ; 
  
 this 
 . 
 map 
  
 = 
  
 this 
 . 
 gMap 
 . 
 innerMap 
 ; 
  
 this 
 . 
 geocoder 
  
 = 
  
 new 
  
 Geocoder 
 (); 
  
 // 
  
 We 
  
 will 
  
 add 
  
 more 
  
 initialization 
  
 logic 
  
 here 
  
 in 
  
 later 
  
 steps 
 . 
  
 } 
 } 
 // 
  
 Wait 
  
 for 
  
 the 
  
 DOM 
  
 to 
  
 be 
  
 ready 
 , 
  
 then 
  
 create 
  
 an 
  
 instance 
  
 of 
  
 our 
  
 app 
 . 
 window 
 . 
 addEventListener 
 ( 
 'DOMContentLoaded' 
 , 
  
 () 
  
 = 
>  
 { 
  
 new 
  
 PlaceFinderApp 
 (); 
 }); 
 

API Key restrictions

You may need to add a new restriction to your API key for this Codelab to work. See Restrict your API keys for more information and guidance on how to do this.

Check your work

Open the index.html file in your web browser. You should see a page with a header containing a search bar and filters, a sidebar with the message "Your search results will appear here," and a large map centered on New York City. At this stage, the search controls are not yet functional.

4. Implement a search function

In this section, we'll bring our application to life by implementing the core search functionality. We will write the code that runs when a user clicks the "Search" button. We will build this function with best practices from the start to handle user interactions gracefully and prevent common bugs like race conditions.

By the end of this step, you will be able to click the search button and see a loading spinner appear while the application fetches data in the background.

Create the search method

First, define the performSearch method inside our PlaceFinderApp class. This function will be the heart of our search logic. We will also introduce an instance variable, isSearchInProgress , to act as a "gatekeeper." This prevents the user from starting a new search while one is already in progress, which can lead to errors.

The logic inside performSearch might seem complex, so we'll break it down:

  1. It first checks if a search is already in progress. If so, it does nothing.
  2. It sets the isSearchInProgress flag to true to "lock" the function.
  3. It shows the loading spinner and prepares the UI for new results.
  4. It sets the search request's textQuery property to null . This is a crucial step that forces the web component to recognize that a new request is coming.
  5. It uses a setTimeout with a 0 delay. This standard JavaScript technique schedules the rest of our code to run in the next browser task, making sure the component has processed the null value first. Even if the user searches for the exact same thing twice, a new search will always be triggered.

Add event listeners

Next, we need to call our performSearch method when the user interacts with the app. We'll create a new method, attachEventListeners , to keep all our event-handling code in one place. For now, we'll just add a listener for the search button's click event. We'll also add a placeholder for another event, gmp-load , which we'll use in the next step.

Update the JavaScript file

Update your script.js file with the following code. The new or changed sections are the attachEventListeners method and the performSearch method.

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

Check your work

Save your script.js file and refresh index.html in your browser. The page should look the same as before. Now, click the "Search" button in the header.

You should see two things happen:

  1. The placeholder message "Your search results will appear here" disappears.
  2. The loading spinner appears and continues spinning.

The spinner will spin forever because we haven't told it when to stop yet. We'll do that in the next section when we display the results. This confirms that our search function is being triggered correctly.

5. Display results and add markers

Now that the search trigger is functional, the next task is to display the results on the screen. The code in this section will connect the search logic to the UI. Once the Place Search Element finishes loading data, it will release the search "lock," hide the loading spinner, and display a marker on the map for each result.

Listen for search completion

The Place Search Element fires a gmp-load event when it has successfully fetched data. This is the perfect signal for us to process the results.

First, add an event listener for this event in our attachEventListeners method.

Create marker-handling methods

Next, we'll create two new helper methods: clearMarkers and addMarkers .

  • clearMarkers() will remove any markers from a previous search.
  • addMarkers() will be called by our gmp-load listener. It will loop through the list of places returned by the search and create a new AdvancedMarkerElement for each one. This is also where we'll hide the loading spinner and release the isSearchInProgress lock, completing the search cycle.

Notice that we're storing markers in an object ( this.markers ) using the Place ID as the key. This is a way to manage markers, and will allow us to find a specific marker later.

Finally, we need to call clearMarkers() at the start of every new search. The best place for this is inside performSearch .

Update the JavaScript file

Update your script.js file with the new methods and the changes to attachEventListeners and 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 
 (); 
 }); 
 

Check your work

Save your files and refresh the page in your browser. Click the "Search" button.

The loading spinner should now appear for a moment and then disappear. The sidebar will populate with a list of places relevant to the search term, and you should see corresponding markers appear on the map. The markers don't do anything when clicked yet; we'll add that interactivity in the next section.

6. Activate the search filters and list interactivity

Our application can now display search results, but it's not yet interactive. In this section, we will bring all the user controls to life. We will activate the filters, enable searching with the "Enter" key, and connect the items in the results list to their corresponding locations on the map.

By the end of this step, the application will feel fully responsive to user input.

Activate the search filters

First, the performSearch method will be updated to read the values from all the filter controls in the header. For each filter (price, rating, and "Open Now"), the corresponding property will be set on the searchRequest object before the search is executed.

Add event listeners for all controls

Next, we will expand our attachEventListeners method. We'll add listeners for the change event on each filter control, as well as a keydown listener on the search input to detect when the user presses the "Enter" key. All of these new listeners will call the performSearch method.

Connect the results list to the map

To create a seamless experience, clicking an item in the sidebar results list should focus the map on that location.

A new method, handleResultClick , will listen for the gmp-select event, which is fired by the Place Search Element when an item is clicked. This function will find the associated place's location and smoothly pan the map to it.

For this to work, make sure the selectable attribute is present on your gmp-place-search component in 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> 

Update the JavaScript file

Update your script.js file with the following complete code. This version includes the new handleResultClick method and the updated logic in attachEventListeners and 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 
 (); 
 }); 
 

Check your work

Save your script.js file and refresh the page. The application should now be highly interactive.

Verify the following:

  • Searching by pressing "Enter" in the search box works.
  • Changing any of the filters (Price, Rating, Open Now) triggers a new search and updates the results.
  • Clicking an item in the sidebar results list now smoothly pans the map to that item's location.

In the next section, we will implement the details card that appears when a marker is clicked.

7. Implement Place Details Element

Our application is now fully interactive, but it's missing a key feature: the ability to see more information about a selected place. In this section, we will implement the Place Details Element that will appear when a user clicks a marker on the map, or selects an item in the Place Search Element.

Create a reusable details card container

The most efficient way to display place details on the map is to create a single, reusable container. We will use an AdvancedMarkerElement as this container. Its content will be the hidden gmp-place-details-compact widget that we already have in our index.html .

A new method, initDetailsPopup , will handle the creation of this reusable marker. It will be created once when the application loads and will start hidden. We will also add a listener to the main map in this method, so that clicking anywhere on the map will hide the details card.

Update the marker click behavior

Next, we need to update what happens when a user clicks a place marker. The 'click' listener inside the addMarkers method will now be responsible for showing the details card.

When a marker is clicked, the listener will:

  1. Pan the map to the marker's location.
  2. Update the details card with the information for that specific place.
  3. Position the details card at the marker's location and make it visible.

Connect the list click to the marker click

Finally, we'll update the handleResultClick method. Instead of just panning the map, it will now programmatically trigger the click event on the corresponding marker. This is a powerful pattern that allows us to reuse the exact same logic for both interactions, keeping our code clean and maintainable.

Update the JavaScript file

Update your script.js file with the following code. The new or changed sections are the initDetailsPopup method and the updated addMarkers and handleResultClick methods.

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

Check your work

Save your script.js file and refresh the page. The application should now show details on demand.

Verify the following:

  • Clicking a marker on the map now centers the map and opens a styled details card over the marker.
  • Clicking an item in the sidebar results list does the exact same thing.
  • Clicking on the map away from the card closes it.
  • Starting a new search also closes any open details card.

8. Add the final polish

Our application is now fully functional, but there are a few final touches we can add to make the user experience even better. In this final section, we will implement two key features: a dynamic header that provides better context for the search results, and automatic formatting for the user's search query.

Create a dynamic results header

At the moment, the sidebar header always says "Results." We can make this more informative by updating it to reflect the current search. For example, "Burgers near New York."

To do this, we will use the Geocoding API to convert the center coordinates of the map into a human-readable location, like a city name. A new async method, updateResultsHeader , will handle this logic. It will be called every time a search is performed.

Format the user's search query

To make sure the UI looks clean and consistent, we'll automatically format the user's search term into "Title Case" (e.g., "burger restaurant" becomes "Burger Restaurant"). A helper function, toTitleCase , will handle this transformation. The performSearch method will be updated to use this function on the user's input before performing the search and updating the header.

Update the JavaScript file

Update your script.js file with the final version of the code. This includes the new toTitleCase and updateResultsHeader methods, and the updated performSearch method that integrates them.

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

Check your work

Save your script.js file and refresh the page.

Verify the features:

  • Type pizza (all lowercase) into the search box and click search. The text in the box should change to "Pizza," and the header in the sidebar should update to say "Pizza near New York."
  • Pan the map to a different city, like Boston, and search again. The header should update to "Pizza near Boston."

9. Congratulations

You've successfully built a complete, interactive local search application that combines the simplicity of Places UI Kit with the power of the core Google Maps Platform JavaScript APIs.

What you learned

  • How to structure a mapping application using a JavaScript classto manage state and logic.
  • How to use the Places UI Kit with Google Maps JavaScript API for rapid UI development.
  • How to programmatically add and manage Advanced Markers to display custom points of interest on the map.
  • How to use the Geocoding Service to turn coordinates into human-readable addresses for a better user experience.
  • How to identify and fix common race conditions in an interactive application by using state flags and making sure component properties are updated correctly.

What's next?

  • Learn more about Customizing Advanced Markers by changing their color, scale, or even using custom HTML.
  • Explore Cloud-based Map Styling to customize the look and feel of your map to match your brand.
  • Try adding the Drawing Library to allow users to draw shapes on the map to define search areas.
  • Help us create the content that you would find most useful by answering the following survey:

What other codelabs would you like to see?

Data visualization on maps More about customizing the style of my maps Building for 3D interactions in maps

Can't find the codelab you're most interested in? Request it with a new issue here .

Design a Mobile Site
View Site in Mobile | Classic
Share by: