1. ก่อนเริ่มต้น
Codelab นี้จะสอนวิธีสร้างแอปพลิเคชันการค้นหาในพื้นที่แบบอินเทอร์แอกทีฟเต็มรูปแบบโดยใช้ชุดเครื่องมือ UI ของ Places ใน Google Maps Platform
ข้อกำหนดเบื้องต้น
- โปรเจ็กต์ Google Cloud ที่กำหนดค่า API และข้อมูลเข้าสู่ระบบที่จำเป็น
- มีความรู้พื้นฐานเกี่ยวกับ HTML และ CSS
- ความเข้าใจเกี่ยวกับ JavaScript ที่ทันสมัย
- เว็บเบราว์เซอร์รุ่นใหม่ เช่น Chrome เวอร์ชันล่าสุด
- โปรแกรมแก้ไขข้อความที่คุณเลือก
สิ่งที่คุณต้องดำเนินการ
- สร้างแอปพลิเคชันการแมปโดยใช้คลาส JavaScript
- ใช้ Web Components เพื่อแสดงแผนที่
- ใช้องค์ประกอบการค้นหาสถานที่เพื่อดำเนินการและแสดงผลการค้นหาข้อความ
- สร้างและจัดการเครื่องหมายแผนที่
AdvancedMarkerElement
ที่กำหนดเองโดยใช้โปรแกรม - แสดงองค์ประกอบรายละเอียดสถานที่เมื่อผู้ใช้เลือกสถานที่
- ใช้ Geocoding API เพื่อสร้างอินเทอร์เฟซแบบไดนามิกและใช้งานง่าย
สิ่งที่คุณต้องมี
- โปรเจ็กต์ Google Cloud ที่เปิดใช้การเรียกเก็บเงิน
- คีย์ API ของ Google Maps Platform
- รหัสแผนที่
- เปิดใช้ API ต่อไปนี้
- Maps JavaScript API
- Places UI Kit
- Geocoding API
2. ตั้งค่า
สำหรับขั้นตอนการเปิดใช้ต่อไปนี้ คุณจะต้องเปิดใช้ Maps JavaScript API, Places UI Kit และ Geocoding API
ตั้งค่า Google Maps Platform
หากยังไม่มีบัญชี Google Cloud Platform และโปรเจ็กต์ที่เปิดใช้การเรียกเก็บเงิน โปรดดูคู่มือเริ่มต้นใช้งาน Google Maps Platform เพื่อสร้างบัญชีสำหรับการเรียกเก็บเงินและโปรเจ็กต์
- ใน Cloud Console ให้คลิกเมนูแบบเลื่อนลงของโปรเจ็กต์ แล้วเลือกโปรเจ็กต์ที่ต้องการใช้สำหรับ Codelab นี้
- เปิดใช้ Google Maps Platform APIs และ SDK ที่จำเป็นสำหรับ Codelab นี้ใน Google Cloud Marketplace โดยทำตามขั้นตอนในวิดีโอนี้หรือเอกสารประกอบนี้
- สร้างคีย์ API ในหน้าข้อมูลเข้าสู่ระบบของ Cloud Console คุณสามารถทำตามขั้นตอนในวิดีโอนี้หรือเอกสารประกอบนี้ คำขอทั้งหมดไปยัง Google Maps Platform ต้องใช้คีย์ API
3. Application Shell และแผนที่การทำงาน
ในขั้นตอนแรกนี้ เราจะสร้างเลย์เอาต์ภาพที่สมบูรณ์สำหรับแอปพลิเคชันและสร้างโครงสร้างที่สะอาดและอิงตามคลาสสำหรับ JavaScript ซึ่งช่วยให้เรามีรากฐานที่มั่นคงในการต่อยอด เมื่อสิ้นสุดส่วนนี้ คุณจะมีหน้าเว็บที่จัดรูปแบบซึ่งแสดงแผนที่แบบอินเทอร์แอกทีฟ
สร้างไฟล์ HTML
ก่อนอื่นให้สร้างไฟล์ชื่อ index.html
ไฟล์นี้จะมีโครงสร้างทั้งหมดของแอปพลิเคชัน ซึ่งรวมถึงส่วนหัว ตัวกรองการค้นหา แถบด้านข้าง คอนเทนเนอร์แผนที่ และคอมโพเนนต์เว็บที่จำเป็น
คัดลอกโค้ดต่อไปนี้ลงใน index.html
อย่าลืมแทนที่ YOUR_API_KEY_HERE
ด้วยคีย์ API ของ Google Maps Platform ของคุณเอง และ DEMO_MAP_ID
ด้วยรหัสแผนที่ 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>
สร้างไฟล์ CSS
จากนั้นสร้างไฟล์ชื่อ style.css
ตอนนี้เราจะเพิ่มสไตล์ที่จำเป็นทั้งหมดเพื่อสร้างลุคที่สะอาดตาและทันสมัยตั้งแต่เริ่มต้น CSS นี้จะจัดการเลย์เอาต์โดยรวม สี แบบอักษร และลักษณะที่ปรากฏขององค์ประกอบ UI ทั้งหมด
คัดลอกโค้ดต่อไปนี้ลงใน style.css
/* style.css */
:root {
--primary-color: #1a73e8;
--text-color: #202124;
--text-color-light: #5f6368;
--background-color: #f8f9fa;
--panel-background: #ffffff;
--border-color: #dadce0;
--shadow-color: rgba(0, 0, 0, 0.1);
}
body {
font-family: 'Roboto', sans-serif;
margin: 0;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
background-color: var(--background-color);
color: var(--text-color);
}
.hidden {
display: none !important;
}
.top-header {
display: flex;
align-items: center;
padding: 12px 24px;
border-bottom: 1px solid var(--border-color);
background-color: var(--panel-background);
gap: 24px;
flex-shrink: 0;
}
.logo {
display: flex;
align-items: center;
gap: 8px;
font-size: 22px;
font-weight: 700;
color: var(--primary-color);
}
.search-container {
display: flex;
flex-grow: 1;
max-width: 720px;
}
.search-container input {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--border-color);
border-radius: 8px 0 0 8px;
font-size: 16px;
transition: box-shadow 0.2s ease;
}
.search-container input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
}
.search-container button {
padding: 0 20px;
border: 1px solid var(--primary-color);
border-radius: 0 8px 8px 0;
background-color: var(--primary-color);
color: white;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: background-color 0.2s ease;
}
.search-container button:hover {
background-color: #185abc;
}
.filter-container {
display: flex;
gap: 12px;
align-items: center;
}
.filter-container select, .open-now-label {
padding: 10px 14px;
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: var(--panel-background);
font-size: 14px;
cursor: pointer;
transition: border-color 0.2s ease;
}
.filter-container select:hover, .open-now-label:hover {
border-color: #c0c2c5;
}
.open-now-label {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
.app-container {
display: flex;
flex-grow: 1;
overflow: hidden;
}
.sidebar {
width: 35%;
min-width: 380px;
max-width: 480px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border-color);
background-color: var(--panel-background);
overflow: hidden;
}
.results-header {
padding: 16px 24px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.results-header h2 {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.results-container {
flex-grow: 1;
position: relative;
overflow-y: auto;
overflow-x: hidden;
}
.placeholder {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 2rem;
box-sizing: border-box;
}
.placeholder p {
color: var(--text-color-light);
font-size: 1.1rem;
}
gmp-place-search {
width: 100%;
}
.map-container {
flex-grow: 1;
position: relative;
}
gmp-map {
width: 100%;
height: 100%;
}
.spinner-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
.spinner-overlay.visible {
opacity: 1;
visibility: visible;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid #e0e0e0;
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
gmp-place-details-compact {
width: 350px;
display: none;
border: none;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
gmp-place-details-compact::after {
content: '';
position: absolute;
bottom: -12px;
left: 50%;
transform: translateX(-50%);
width: 24px;
height: 12px;
background-color: var(--panel-background);
clip-path: polygon(50% 100%, 0 0, 100% 0);
}
สร้างคลาสแอปพลิเคชัน JavaScript
สุดท้าย ให้สร้างไฟล์ชื่อ script.js
เราจะจัดโครงสร้างแอปพลิเคชันภายในคลาส JavaScript ที่ชื่อ PlaceFinderApp
ซึ่งจะช่วยให้โค้ดของเราเป็นระเบียบและจัดการสถานะได้อย่างสะอาด
โค้ดเริ่มต้นนี้จะกำหนดคลาส ค้นหาองค์ประกอบ HTML ทั้งหมดใน constructor
และสร้างเมธอด init()
เพื่อโหลดไลบรารี Google Maps Platform
คัดลอกโค้ดต่อไปนี้ลงใน script.js
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
// We will add more initialization logic here in later steps.
}
}
// Wait for the DOM to be ready, then create an instance of our app.
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
ข้อจำกัดของคีย์ API
คุณอาจต้องเพิ่มข้อจำกัดใหม่ลงในคีย์ API เพื่อให้ Codelab นี้ทำงานได้ ดูข้อมูลเพิ่มเติมและคำแนะนำเกี่ยวกับวิธีดำเนินการนี้ได้ที่จำกัดคีย์ API
ตรวจสอบงาน
เปิดไฟล์ index.html
ในเว็บเบราว์เซอร์ คุณควรเห็นหน้าเว็บที่มีส่วนหัวซึ่งมีแถบค้นหาและตัวกรอง แถบด้านข้างที่มีข้อความ "ผลการค้นหาจะปรากฏที่นี่" และแผนที่ขนาดใหญ่ที่อยู่ตรงกลางนิวยอร์กซิตี้ ในขั้นตอนนี้ ตัวควบคุมการค้นหายังใช้งานไม่ได้
4. ใช้ฟังก์ชันค้นหา
ในส่วนนี้ เราจะทำให้แอปพลิเคชันของเราใช้งานได้จริงโดยการติดตั้งใช้งานฟังก์ชันการค้นหาหลัก เราจะเขียนโค้ดที่จะทำงานเมื่อผู้ใช้คลิกปุ่ม "ค้นหา" เราจะสร้างฟังก์ชันนี้ตามแนวทางปฏิบัติแนะนำตั้งแต่เริ่มต้นเพื่อจัดการการโต้ตอบของผู้ใช้อย่างราบรื่นและป้องกันข้อบกพร่องที่พบบ่อย เช่น สภาวะการแข่งขัน
เมื่อสิ้นสุดขั้นตอนนี้ คุณจะคลิกปุ่มค้นหาและเห็นวงกลมหมุนแสดงการโหลดขณะที่แอปพลิเคชันดึงข้อมูลในเบื้องหลังได้
สร้างวิธีการค้นหา
ก่อนอื่น ให้กำหนดperformSearch
เมธอดภายในคลาส PlaceFinderApp
ฟังก์ชันนี้จะเป็นหัวใจสำคัญของตรรกะการค้นหา นอกจากนี้ เราจะเปิดตัวตัวแปรอินสแตนซ์ isSearchInProgress
เพื่อทำหน้าที่เป็น "ผู้ดูแล" การดำเนินการนี้จะป้องกันไม่ให้ผู้ใช้เริ่มการค้นหาใหม่ในขณะที่การค้นหาหนึ่งกำลังดำเนินการอยู่ ซึ่งอาจทำให้เกิดข้อผิดพลาดได้
ตรรกะภายใน performSearch
อาจดูซับซ้อน เราจึงจะอธิบายให้เข้าใจง่ายขึ้น
- โดยจะตรวจสอบก่อนว่ามีการค้นหาที่กำลังดำเนินการอยู่หรือไม่ หากเป็นเช่นนั้น ระบบจะไม่ดำเนินการใดๆ
- โดยจะตั้งค่า Flag
isSearchInProgress
เป็นtrue
เพื่อ "ล็อก" ฟังก์ชัน - โดยจะแสดงวงกลมการโหลดและเตรียม UI สำหรับผลลัพธ์ใหม่
- โดยจะตั้งค่าพร็อพเพอร์ตี้
textQuery
ของคำขอค้นหาเป็นnull
นี่เป็นขั้นตอนสำคัญที่บังคับให้คอมโพเนนต์เว็บรับรู้ว่ามีคำขอใหม่เข้ามา - โดยใช้
setTimeout
ที่มีความล่าช้า0
เทคนิค JavaScript มาตรฐานนี้จะกำหนดเวลาให้โค้ดที่เหลือทำงานในงานของเบราว์เซอร์ถัดไป เพื่อให้มั่นใจว่าคอมโพเนนต์ได้ประมวลผลค่าnull
ก่อน แม้ว่าผู้ใช้จะค้นหาสิ่งเดียวกัน 2 ครั้ง ระบบก็จะทริกเกอร์การค้นหาใหม่เสมอ
เพิ่ม Listener เหตุการณ์
จากนั้นเราต้องเรียกใช้เมธอด performSearch
เมื่อผู้ใช้โต้ตอบกับแอป เราจะสร้างเมธอดใหม่ attachEventListeners
เพื่อเก็บโค้ดการจัดการเหตุการณ์ทั้งหมดไว้ในที่เดียว ตอนนี้เราจะเพิ่ม Listener สำหรับเหตุการณ์ click
ของปุ่มค้นหา นอกจากนี้ เราจะเพิ่มตัวยึดตําแหน่งสําหรับเหตุการณ์อื่น gmp-load
ซึ่งเราจะใช้ในขั้นตอนถัดไป
อัปเดตไฟล์ JavaScript
อัปเดตไฟล์ script.js
ด้วยโค้ดต่อไปนี้ ส่วนใหม่หรือส่วนที่เปลี่ยนแปลงคือเมธอด attachEventListeners
และเมธอด performSearch
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
// Call the new method to set up listeners
this.attachEventListeners();
}
// NEW: Method to set up all event listeners
attachEventListeners() {
this.searchButton.addEventListener('click', this.performSearch.bind(this));
// We will add the gmp-load listener in the next step
}
// NEW: Core search method
async performSearch() {
// Exit if a search is already in progress
if (this.isSearchInProgress) {
return;
}
// Set the lock
this.isSearchInProgress = true;
// Show the placeholder and spinner
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
// Force a state change by clearing the query first.
this.searchRequest.textQuery = null;
// Defer setting the real properties to the next event loop cycle.
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
// If the query is empty, release the lock and hide the spinner
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
// For now, we just set the textQuery. We'll add filters later.
this.searchRequest.textQuery = rawQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
}, 0);
}
// NEW: Helper method to show/hide the spinner
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
}
// Wait for the DOM to be ready, then create an instance of our app.
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
ตรวจสอบงาน
บันทึกไฟล์ script.js
แล้วรีเฟรช index.html
ในเบราว์เซอร์ หน้าเว็บควรมีลักษณะเหมือนเดิม ตอนนี้ให้คลิกปุ่ม "ค้นหา" ในส่วนหัว
คุณควรเห็นสิ่งต่อไปนี้เกิดขึ้น
- ข้อความตัวยึดตำแหน่ง "ผลการค้นหาจะปรากฏที่นี่" จะหายไป
- วงล้อแสดงการโหลดจะปรากฏขึ้นและหมุนต่อไป
วงกลมหมุนจะหมุนไปเรื่อยๆ เนื่องจากเรายังไม่ได้บอกให้หยุด เราจะดำเนินการดังกล่าวในส่วนถัดไปเมื่อแสดงผลลัพธ์ ซึ่งเป็นการยืนยันว่าฟังก์ชันการค้นหาของเราทํางานได้อย่างถูกต้อง
5. แสดงผลการค้นหาและเพิ่มเครื่องหมาย
ตอนนี้ทริกเกอร์การค้นหาใช้งานได้แล้ว งานต่อไปคือการแสดงผลลัพธ์บนหน้าจอ โค้ดในส่วนนี้จะเชื่อมต่อตรรกะการค้นหากับ UI เมื่อองค์ประกอบการค้นหาสถานที่โหลดข้อมูลเสร็จแล้ว องค์ประกอบจะยกเลิก "การล็อก" การค้นหา ซ่อนวงกลมหมุนที่กำลังโหลด และแสดงเครื่องหมายบนแผนที่สำหรับผลการค้นหาแต่ละรายการ
ฟังการค้นหาที่เสร็จสมบูรณ์
Place Search Element จะทริกเกอร์เหตุการณ์ gmp-load
เมื่อดึงข้อมูลสำเร็จ ซึ่งเป็นสัญญาณที่เหมาะสมที่สุดสำหรับเราในการประมวลผลผลลัพธ์
ก่อนอื่น ให้เพิ่ม Listener เหตุการณ์สําหรับเหตุการณ์นี้ในเมธอด attachEventListeners
สร้างเมธอดการจัดการเครื่องหมาย
จากนั้นเราจะสร้างเมธอดตัวช่วยใหม่ 2 รายการ ได้แก่ clearMarkers
และ addMarkers
clearMarkers()
จะนำเครื่องหมายทั้งหมดจากการค้นหาก่อนหน้าออกaddMarkers()
จะถูกเรียกใช้โดยgmp-load
Listener โดยจะวนซ้ำในรายการสถานที่ที่ได้จากการค้นหาและสร้างAdvancedMarkerElement
ใหม่สำหรับแต่ละสถานที่ นอกจากนี้ เราจะซ่อนวงกลมการโหลดและปลดล็อกisSearchInProgress
ในส่วนนี้ด้วย ซึ่งเป็นการสิ้นสุดวงจรการค้นหา
โปรดสังเกตว่าเราจัดเก็บเครื่องหมายในออบเจ็กต์ (this.markers
) โดยใช้รหัสสถานที่เป็นคีย์ ซึ่งเป็นวิธีจัดการเครื่องหมายและจะช่วยให้เราค้นหาเครื่องหมายที่เฉพาะเจาะจงได้ในภายหลัง
สุดท้าย เราต้องเรียกใช้ clearMarkers()
เมื่อเริ่มการค้นหาใหม่ทุกครั้ง ซึ่งคุณสามารถทำได้ใน performSearch
อัปเดตไฟล์ JavaScript
อัปเดตไฟล์ script.js
ด้วยวิธีการใหม่และการเปลี่ยนแปลงใน attachEventListeners
และ performSearch
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
this.attachEventListeners();
}
attachEventListeners() {
this.searchButton.addEventListener('click', this.performSearch.bind(this));
// NEW: Listen for when the search component has loaded results
this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
}
// NEW: Method to clear markers from a previous search
clearMarkers() {
for (const marker of Object.values(this.markers)) {
marker.map = null;
}
this.markers = {};
}
// NEW: Method to add markers for new search results
addMarkers() {
// Release the lock and hide the spinner
this.isSearchInProgress = false;
this.showLoading(false);
const places = this.placeSearch.places;
if (!places || places.length === 0) return;
// Create a new marker for each place result
for (const place of places) {
if (!place.location || !place.id) continue;
const marker = new this.AdvancedMarkerElement({
map: this.map,
position: place.location,
title: place.displayName,
});
// Store marker by its place ID for access later
this.markers[place.id] = marker;
}
}
async performSearch() {
if (this.isSearchInProgress) {
return;
}
this.isSearchInProgress = true;
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
// NEW: Clear old markers before starting a new search
this.clearMarkers();
this.searchRequest.textQuery = null;
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
this.searchRequest.textQuery = rawQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
}, 0);
}
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
}
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
ตรวจสอบงาน
บันทึกไฟล์และรีเฟรชหน้าเว็บในเบราว์เซอร์ คลิกปุ่ม "ค้นหา"
ตอนนี้ไอคอนหมุนของการโหลดควรจะปรากฏขึ้นชั่วครู่แล้วหายไป แถบด้านข้างจะแสดงรายการสถานที่ที่เกี่ยวข้องกับข้อความค้นหา และคุณควรเห็นเครื่องหมายที่เกี่ยวข้องปรากฏบนแผนที่ เครื่องหมายจะยังไม่ทําอะไรเมื่อคลิก แต่เราจะเพิ่มการโต้ตอบนั้นในส่วนถัดไป
6. เปิดใช้งานตัวกรองการค้นหาและการโต้ตอบในรายการ
ตอนนี้แอปพลิเคชันของเราแสดงผลการค้นหาได้แล้ว แต่ยังไม่โต้ตอบได้ ในส่วนนี้ เราจะทำให้การควบคุมของผู้ใช้ทั้งหมดเป็นจริง เราจะเปิดใช้งานตัวกรอง อนุญาตให้ค้นหาด้วยปุ่ม "Enter" และเชื่อมต่อรายการในรายการผลลัพธ์กับตำแหน่งที่เกี่ยวข้องบนแผนที่
เมื่อสิ้นสุดขั้นตอนนี้ แอปพลิเคชันจะตอบสนองต่อข้อมูลที่ผู้ใช้ป้อนได้อย่างเต็มที่
เปิดใช้งานตัวกรองการค้นหา
ก่อนอื่น ระบบจะอัปเดตเมธอด performSearch
เพื่ออ่านค่าจากตัวควบคุมตัวกรองทั้งหมดในส่วนหัว สำหรับตัวกรองแต่ละรายการ (ราคา คะแนน และ "เปิดอยู่") ระบบจะตั้งค่าพร็อพเพอร์ตี้ที่เกี่ยวข้องในออบเจ็กต์ searchRequest
ก่อนที่จะดำเนินการค้นหา
เพิ่ม Listener เหตุการณ์สำหรับตัวควบคุมทั้งหมด
จากนั้นเราจะขยายattachEventListeners
เราจะเพิ่ม Listener สำหรับเหตุการณ์ change
ในตัวควบคุมตัวกรองแต่ละรายการ รวมถึง Listener keydown
ในช่องป้อนข้อมูลการค้นหาเพื่อตรวจหาเมื่อผู้ใช้กดปุ่ม "Enter" ผู้ฟังใหม่ทั้งหมดนี้จะเรียกใช้เมธอด performSearch
เชื่อมต่อรายการผลลัพธ์กับแผนที่
การคลิกรายการในรายการผลการค้นหาของแถบด้านข้างควรโฟกัสแผนที่ไปยังสถานที่นั้นๆ เพื่อสร้างประสบการณ์การใช้งานที่ราบรื่น
เมธอดใหม่ handleResultClick
จะรอรับฟังเหตุการณ์ gmp-select
ซึ่งจะเริ่มทำงานโดยองค์ประกอบการค้นหาสถานที่เมื่อมีการคลิกรายการ ฟังก์ชันนี้จะค้นหาตำแหน่งของสถานที่ที่เชื่อมโยงและเลื่อนแผนที่ไปยังตำแหน่งนั้นอย่างราบรื่น
หากต้องการให้ฟีเจอร์นี้ทำงานได้ โปรดตรวจสอบว่าแอตทริบิวต์ selectable
อยู่ในคอมโพเนนต์ gmp-place-search
ใน index.html
<gmp-place-search id="place-search-list" class="hidden" selectable>
<gmp-place-all-content></gmp-place-all-content>
<gmp-place-text-search-request></gmp-place-text-search-request>
</gmp-place-search>
อัปเดตไฟล์ JavaScript
อัปเดตไฟล์ script.js
ด้วยโค้ดที่สมบูรณ์ต่อไปนี้ เวอร์ชันนี้มีhandleResultClick
วิธีใหม่และตรรกะที่อัปเดตแล้วใน attachEventListeners
และ performSearch
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
this.attachEventListeners();
}
// UPDATED: All event listeners are now attached
attachEventListeners() {
// Listen for the 'Enter' key press in the search input
this.queryInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.performSearch();
}
});
// Listen for a sidebar result click
this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
this.searchButton.addEventListener('click', this.performSearch.bind(this));
this.priceFilter.addEventListener('change', this.performSearch.bind(this));
this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
}
clearMarkers() {
for (const marker of Object.values(this.markers)) {
marker.map = null;
}
this.markers = {};
}
addMarkers() {
this.isSearchInProgress = false;
this.showLoading(false);
const places = this.placeSearch.places;
if (!places || places.length === 0) return;
for (const place of places) {
if (!place.location || !place.id) continue;
const marker = new this.AdvancedMarkerElement({
map: this.map,
position: place.location,
title: place.displayName,
});
this.markers[place.id] = marker;
}
}
// NEW: Function to handle clicks on the results list
handleResultClick(event) {
const place = event.place;
if (!place || !place.location) return;
// Pan the map to the selected place
this.map.panTo(place.location);
}
// UPDATED: Search function now includes all filters
async performSearch() {
if (this.isSearchInProgress) {
return;
}
this.isSearchInProgress = true;
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
this.clearMarkers();
this.searchRequest.textQuery = null;
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
this.searchRequest.textQuery = rawQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
// Add filter values to the request
const selectedPrice = this.priceFilter.value;
let priceLevels = [];
switch (selectedPrice) {
case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
default: priceLevels = null; break;
}
this.searchRequest.priceLevels = priceLevels;
const selectedRating = parseFloat(this.ratingFilter.value);
this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
}, 0);
}
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
}
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
ตรวจสอบงาน
บันทึกไฟล์ script.js
แล้วรีเฟรชหน้าเว็บ ตอนนี้แอปพลิเคชันควรมีการโต้ตอบสูง
โปรดตรวจสอบสิ่งต่อไปนี้
- การค้นหาโดยกด "Enter" ในช่องค้นหาใช้ได้
- การเปลี่ยนตัวกรองใดๆ (ราคา คะแนน เปิดอยู่ตอนนี้) จะทริกเกอร์การค้นหาใหม่และอัปเดตผลการค้นหา
- ตอนนี้การคลิกรายการในรายการผลการค้นหาของแถบด้านข้างจะเลื่อนแผนที่ไปยังตำแหน่งของรายการนั้นอย่างราบรื่น
ในส่วนถัดไป เราจะใช้การ์ดรายละเอียดที่จะปรากฏขึ้นเมื่อคลิกเครื่องหมาย
7. ใช้คอมโพเนนต์รายละเอียดสถานที่
ตอนนี้แอปพลิเคชันของเราโต้ตอบได้อย่างเต็มที่แล้ว แต่ยังขาดฟีเจอร์สำคัญไป นั่นคือความสามารถในการดูข้อมูลเพิ่มเติมเกี่ยวกับสถานที่ที่เลือก ในส่วนนี้ เราจะใช้ Place Details Element ซึ่งจะปรากฏขึ้นเมื่อผู้ใช้คลิกเครื่องหมายบนแผนที่ หรือเลือกรายการใน Place Search Element
สร้างคอนเทนเนอร์การ์ดรายละเอียดที่นำกลับมาใช้ใหม่ได้
วิธีที่มีประสิทธิภาพมากที่สุดในการแสดงรายละเอียดสถานที่บนแผนที่คือการสร้างคอนเทนเนอร์เดียวที่นำกลับมาใช้ใหม่ได้ เราจะใช้ AdvancedMarkerElement
เป็นคอนเทนเนอร์นี้ เนื้อหาของวิดเจ็ตนี้จะเป็นวิดเจ็ต gmp-place-details-compact
ที่ซ่อนอยู่ซึ่งเรามีอยู่แล้วใน index.html
initDetailsPopup
ซึ่งเป็นวิธีใหม่จะจัดการการสร้างเครื่องหมายที่ใช้ซ้ำได้นี้ ระบบจะสร้างครั้งเดียวเมื่อแอปพลิเคชันโหลดและจะเริ่มต้นโดยซ่อนไว้ นอกจากนี้ เราจะเพิ่มตัวฟังลงในแผนที่หลักด้วยวิธีนี้ เพื่อให้การคลิกที่ใดก็ได้บนแผนที่จะซ่อนการ์ดรายละเอียด
อัปเดตลักษณะการคลิกเครื่องหมาย
จากนั้นเราต้องอัปเดตสิ่งที่เกิดขึ้นเมื่อผู้ใช้คลิกเครื่องหมายสถานที่ ตอนนี้ 'click'
listener ภายในเมธอด addMarkers
จะมีหน้าที่แสดงการ์ดรายละเอียด
เมื่อมีการคลิกเครื่องหมาย ผู้ฟังจะทำสิ่งต่อไปนี้
- เลื่อนแผนที่ไปยังตำแหน่งของเครื่องหมาย
- อัปเดตการ์ดรายละเอียดด้วยข้อมูลของสถานที่นั้นๆ
- วางการ์ดรายละเอียดที่ตำแหน่งของเครื่องหมายและทำให้มองเห็นได้
เชื่อมต่อการคลิกลิสต์กับการคลิกเครื่องหมาย
สุดท้าย เราจะอัปเดตhandleResultClick
แทนที่จะแค่เลื่อนแผนที่ ตอนนี้ระบบจะทริกเกอร์เหตุการณ์ click
ในเครื่องหมายที่เกี่ยวข้องโดยอัตโนมัติ ซึ่งเป็นรูปแบบที่มีประสิทธิภาพที่ช่วยให้เรานำตรรกะเดียวกันมาใช้ซ้ำได้สำหรับการโต้ตอบทั้ง 2 แบบ ทำให้โค้ดของเราสะอาดและบำรุงรักษาได้
อัปเดตไฟล์ JavaScript
อัปเดตไฟล์ script.js
ด้วยโค้ดต่อไปนี้ ส่วนใหม่หรือส่วนที่มีการเปลี่ยนแปลงคือเมธอด initDetailsPopup
และเมธอด addMarkers
และ handleResultClick
ที่อัปเดตแล้ว
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
// NEW: Call the method to initialize the details card
this.initDetailsPopup();
this.attachEventListeners();
}
attachEventListeners() {
this.queryInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.performSearch();
}
});
this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
this.searchButton.addEventListener('click', this.performSearch.bind(this));
this.priceFilter.addEventListener('change', this.performSearch.bind(this));
this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
}
// NEW: Method to set up the reusable details card
initDetailsPopup() {
this.detailsPopup = new this.AdvancedMarkerElement({
content: this.placeDetailsWidget,
map: null,
zIndex: 100
});
this.map.addListener('click', () => { this.detailsPopup.map = null; });
}
clearMarkers() {
for (const marker of Object.values(this.markers)) {
marker.map = null;
}
this.markers = {};
}
// UPDATED: The marker's click listener now shows the details card
addMarkers() {
this.isSearchInProgress = false;
this.showLoading(false);
const places = this.placeSearch.places;
if (!places || places.length === 0) return;
for (const place of places) {
if (!place.location || !place.id) continue;
const marker = new this.AdvancedMarkerElement({
map: this.map,
position: place.location,
title: place.displayName,
});
// Add the click listener to show the details card
marker.addListener('click', (event) => {
event.stop();
this.map.panTo(place.location);
this.placeDetailsRequest.place = place;
this.placeDetailsWidget.style.display = 'block';
this.detailsPopup.position = place.location;
this.detailsPopup.map = this.map;
});
this.markers[place.id] = marker;
}
}
// UPDATED: This now triggers the marker's click event
handleResultClick(event) {
const place = event.place;
if (!place || !place.id) return;
const marker = this.markers[place.id];
if (marker) {
// Programmatically trigger the marker's click event
marker.click();
}
}
async performSearch() {
if (this.isSearchInProgress) return;
this.isSearchInProgress = true;
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
this.clearMarkers();
// Hide the details card when a new search starts
if (this.detailsPopup) this.detailsPopup.map = null;
this.searchRequest.textQuery = null;
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
this.searchRequest.textQuery = rawQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
const selectedPrice = this.priceFilter.value;
let priceLevels = [];
switch (selectedPrice) {
case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
default: priceLevels = null; break;
}
this.searchRequest.priceLevels = priceLevels;
const selectedRating = parseFloat(this.ratingFilter.value);
this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
}, 0);
}
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
}
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
ตรวจสอบงาน
บันทึกไฟล์ script.js
แล้วรีเฟรชหน้าเว็บ ตอนนี้แอปพลิเคชันควรแสดงรายละเอียดตามต้องการแล้ว
โปรดตรวจสอบสิ่งต่อไปนี้
- ตอนนี้การคลิกเครื่องหมายบนแผนที่จะทําให้แผนที่อยู่ตรงกลางและเปิดการ์ดรายละเอียดที่มีสไตล์เหนือเครื่องหมาย
- การคลิกรายการในรายการผลการค้นหาของแถบด้านข้างจะทําให้เกิดผลลัพธ์เดียวกัน
- การคลิกบนแผนที่ที่ไม่ได้อยู่ในการ์ดจะปิดการ์ด
- การเริ่มการค้นหาใหม่จะปิดการ์ดรายละเอียดที่เปิดอยู่ด้วย
8. เพิ่มความสมบูรณ์ขั้นสุดท้าย
ตอนนี้แอปพลิเคชันของเราทำงานได้อย่างเต็มที่แล้ว แต่เรายังสามารถเพิ่มการปรับแต่งขั้นสุดท้ายอีกเล็กน้อยเพื่อทำให้ประสบการณ์ของผู้ใช้ดียิ่งขึ้น ในส่วนสุดท้ายนี้ เราจะใช้ฟีเจอร์หลัก 2 อย่าง ได้แก่ ส่วนหัวแบบไดนามิกที่ให้บริบทที่ดีขึ้นสำหรับผลการค้นหา และการจัดรูปแบบอัตโนมัติสำหรับคำค้นหาของผู้ใช้
สร้างส่วนหัวผลลัพธ์แบบไดนามิก
ปัจจุบันส่วนหัวของแถบด้านข้างจะแสดงคำว่า "ผลลัพธ์" เสมอ เราสามารถทำให้ข้อมูลนี้มีประโยชน์มากขึ้นได้โดยการอัปเดตให้สอดคล้องกับการค้นหาปัจจุบัน เช่น "เบอร์เกอร์ใกล้นิวยอร์ก"
โดยเราจะใช้ Geocoding API เพื่อแปลงพิกัดกึ่งกลางของแผนที่เป็นตำแหน่งที่มนุษย์อ่านได้ เช่น ชื่อเมือง วิธี async
ใหม่ updateResultsHeader
จะจัดการตรรกะนี้ ระบบจะเรียกใช้ฟังก์ชันนี้ทุกครั้งที่มีการค้นหา
จัดรูปแบบคำค้นหาของผู้ใช้
เพื่อให้ UI ดูสะอาดตาและสอดคล้องกัน เราจะจัดรูปแบบคำค้นหาของผู้ใช้เป็น "Title Case" โดยอัตโนมัติ (เช่น "ร้านเบอร์เกอร์" จะกลายเป็น "ร้านเบอร์เกอร์") ฟังก์ชันตัวช่วย toTitleCase
จะจัดการการเปลี่ยนรูปแบบนี้ เราจะอัปเดตวิธีการ performSearch
เพื่อใช้ฟังก์ชันนี้กับอินพุตของผู้ใช้ก่อนทำการค้นหาและอัปเดตส่วนหัว
อัปเดตไฟล์ JavaScript
อัปเดตไฟล์ script.js
ด้วยโค้ดเวอร์ชันสุดท้าย ซึ่งรวมถึงวิธี toTitleCase
และ updateResultsHeader
ใหม่ รวมถึงวิธี performSearch
ที่อัปเดตแล้วซึ่งผสานรวมวิธีดังกล่าว
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
this.initDetailsPopup();
this.attachEventListeners();
}
attachEventListeners() {
this.queryInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.performSearch();
}
});
this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
this.searchButton.addEventListener('click', this.performSearch.bind(this));
this.priceFilter.addEventListener('change', this.performSearch.bind(this));
this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
}
initDetailsPopup() {
this.detailsPopup = new this.AdvancedMarkerElement({
content: this.placeDetailsWidget,
map: null,
zIndex: 100
});
this.map.addListener('click', () => { this.detailsPopup.map = null; });
}
// NEW: Helper function to format text to Title Case
toTitleCase(str) {
if (!str) return '';
return str.toLowerCase().split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
}
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
clearMarkers() {
for (const marker of Object.values(this.markers)) { marker.map = null; }
this.markers = {};
}
addMarkers() {
this.isSearchInProgress = false;
this.showLoading(false);
const places = this.placeSearch.places;
if (!places || places.length === 0) return;
for (const place of places) {
if (!place.location || !place.id) continue;
const marker = new this.AdvancedMarkerElement({
map: this.map,
position: place.location,
title: place.displayName,
});
marker.addListener('click', (event) => {
event.stop();
this.map.panTo(place.location);
this.placeDetailsRequest.place = place;
this.placeDetailsWidget.style.display = 'block';
this.detailsPopup.position = place.location;
this.detailsPopup.map = this.map;
});
this.markers[place.id] = marker;
}
}
handleResultClick(event) {
const place = event.place;
if (!place || !place.id) return;
const marker = this.markers[place.id];
if (marker) {
marker.click();
}
}
// UPDATED: Now integrates formatting and the dynamic header
async performSearch() {
if (this.isSearchInProgress) return;
this.isSearchInProgress = true;
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
this.clearMarkers();
if (this.detailsPopup) this.detailsPopup.map = null;
this.searchRequest.textQuery = null;
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
// Format the query and update the input box value
const formattedQuery = this.toTitleCase(rawQuery);
this.queryInput.value = formattedQuery;
// Update the header with the new query and location
await this.updateResultsHeader(formattedQuery);
// Pass the formatted query to the search request
this.searchRequest.textQuery = formattedQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
const selectedPrice = this.priceFilter.value;
let priceLevels = [];
switch (selectedPrice) {
case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
default: priceLevels = null; break;
}
this.searchRequest.priceLevels = priceLevels;
const selectedRating = parseFloat(this.ratingFilter.value);
this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
}, 0);
}
// NEW: Method to update the sidebar header with geocoded location
async updateResultsHeader(query) {
try {
const response = await this.geocoder.geocode({ location: this.map.getCenter() });
if (response.results && response.results.length > 0) {
const cityResult = response.results.find(r => r.types.includes('locality')) || response.results[0];
const city = cityResult.address_components[0].long_name;
this.resultsHeaderText.textContent = `${query} near ${city}`;
} else {
this.resultsHeaderText.textContent = `${query} near current map area`;
}
} catch (error) {
console.error("Geocoding failed:", error);
this.resultsHeaderText.textContent = `Results for ${query}`;
}
}
}
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
ตรวจสอบงาน
บันทึกไฟล์ script.js
แล้วรีเฟรชหน้าเว็บ
ตรวจสอบฟีเจอร์
- พิมพ์
pizza
(ตัวพิมพ์เล็กทั้งหมด) ลงในช่องค้นหา แล้วคลิกค้นหา ข้อความในช่องควรเปลี่ยนเป็น "พิซซ่า" และส่วนหัวในแถบด้านข้างควรได้รับการอัปเดตเป็น "พิซซ่าใกล้นิวยอร์ก" - เลื่อนแผนที่ไปยังเมืองอื่น เช่น บอสตัน แล้วค้นหาอีกครั้ง ส่วนหัวควรเปลี่ยนเป็น "พิซซ่าใกล้บอสตัน"
9. ขอแสดงความยินดี
คุณได้สร้างแอปพลิเคชันการค้นหาในพื้นที่แบบอินเทอร์แอกทีฟที่สมบูรณ์เรียบร้อยแล้ว ซึ่งผสานความเรียบง่ายของ Places UI Kit เข้ากับประสิทธิภาพของ JavaScript API หลักของ Google Maps Platform
สิ่งที่คุณได้เรียนรู้
- วิธีสร้างแอปพลิเคชันการแมปโดยใช้คลาส JavaScript เพื่อจัดการสถานะและตรรกะ
- วิธีใช้ Places UI Kit กับ Google Maps JavaScript API เพื่อการพัฒนา UI อย่างรวดเร็ว
- วิธีเพิ่มและจัดการเครื่องหมายขั้นสูงโดยอัตโนมัติเพื่อแสดงจุดที่น่าสนใจที่กำหนดเองบนแผนที่
- วิธีใช้บริการ Geocoding เพื่อเปลี่ยนพิกัดเป็นที่อยู่ที่มนุษย์อ่านได้เพื่อประสบการณ์ของผู้ใช้ที่ดียิ่งขึ้น
- วิธีระบุและแก้ไข Race Condition ทั่วไปในแอปพลิเคชันแบบอินเทอร์แอกทีฟโดยใช้แฟล็กสถานะและตรวจสอบว่าพร็อพเพอร์ตี้ของคอมโพเนนต์ได้รับการอัปเดตอย่างถูกต้อง
ขั้นตอนถัดไปคือ
- ดูข้อมูลเพิ่มเติมเกี่ยวกับการปรับแต่งเครื่องหมายขั้นสูงโดยการเปลี่ยนสี ขนาด หรือแม้แต่ใช้ HTML ที่กำหนดเอง
- สำรวจการจัดรูปแบบแผนที่ในระบบคลาวด์เพื่อปรับแต่งรูปลักษณ์ของแผนที่ให้ตรงกับแบรนด์
- ลองเพิ่มไลบรารีการวาดเพื่อให้ผู้ใช้วาดรูปร่างบนแผนที่เพื่อกำหนดพื้นที่ค้นหา
- ช่วยเราสร้างเนื้อหาที่เป็นประโยชน์ต่อคุณมากที่สุดโดยตอบแบบสำรวจต่อไปนี้
คุณอยากเห็น Codelab อื่นๆ แบบไหน
หากไม่พบโค้ดแล็บที่คุณสนใจมากที่สุด ขอได้โดยแจ้งปัญหาใหม่ที่นี่