Việc xây dựng một bộ chọn thành phố (City Picker) chuyên nghiệp yêu cầu xử lý tốt các tương tác người dùng và quản lý dữ liệu hiệu quả. Dưới đây là cách triển khai component này bằng Vanilla JavaScript với các tính năng: chuyển đổi vùng miền, phân loại theo bảng chữ cái và gợi ý tìm kiếm thời gian thực.
Yêu cầu chức năng
- Chuyển đổi giữa danh sách thành phố trong nước và quốc tế.
- Phân loại thành phố theo chữ cái đầu hoặc khu vực địa lý.
- Tự động hiển thị danh sách gợi ý khi người dùng nhập liệu.
Cấu trúc HTML
Sử dụng thẻ <dialog> để làm popup giúp tối ưu hóa việc quản lý cửa sổ hiển thị trên trình duyệt.
<div class="search-container">
<div class="input-group">
<label>Điểm khởi hành</label>
<input type="text" id="departureField" data-type="origin" oninput="onSearchInput(event)" onfocus="showCityPicker(event)" placeholder="Chọn thành phố">
</div>
<div class="input-group">
<label>Điểm đến</label>
<input type="text" id="arrivalField" data-type="destination" oninput="onSearchInput(event)" onfocus="showCityPicker(event)" placeholder="Chọn thành phố">
</div>
</div>
<!-- Popup chọn thành phố -->
<dialog id="cityDialog" class="city-dialog">
<div class="tab-header">
<button class="tab-item active" onclick="switchRegion(0)">Trong nước</button>
<button class="tab-item" onclick="switchRegion(1)">Quốc tế</button>
</div>
<div class="filter-index" id="alphaIndex">
<!-- Các ký tự index như A, B, C, D... -->
</div>
<div id="cityContainer" class="city-grid"></div>
</dialog>
<!-- Danh sách gợi ý tìm kiếm -->
<div id="suggestionBox" class="suggestion-popup" style="display:none"></div>
Xử lý logic JavaScript
1. Tìm kiếm và gợi ý thời gian thực
Dữ liệu thành phố được lưu trữ dưới dạng chuỗi định dạng pinyin|tên|viết tắt|mã để tối ưu tốc độ tìm kiếm bằng phương thức indexOf hoặc filter.
let currentInputType = 'origin';
function onSearchInput(e) {
const keyword = e.target.value.trim().toLowerCase();
const suggestEl = document.getElementById("suggestionBox");
const dialogEl = document.getElementById("cityDialog");
if (!keyword) {
suggestEl.style.display = "none";
return;
}
// Đóng popup chính khi đang tìm kiếm
dialogEl.close();
// Giả định sourceData là mảng dữ liệu ["hanoi|Hà Nội|hn|HAN", ...]
const matches = sourceData.filter(item => item.toLowerCase().includes(keyword));
renderSuggestions(matches, suggestEl);
}
function renderSuggestions(data, container) {
container.innerHTML = "";
if (data.length === 0) {
container.style.display = "none";
return;
}
const ul = document.createElement("ul");
data.forEach(item => {
const [pinyin, name, short] = item.split('|');
const li = document.createElement("li");
li.textContent = `${name} (${short.toUpperCase()})`;
li.onclick = () => selectCity(name);
ul.appendChild(li);
});
container.appendChild(ul);
container.style.display = "block";
}
2. Quản lý hiển thị Popup
Điều khiển việc mở/đóng popup và xác định vị trí dựa trên input đang được focus.
function showCityPicker(e) {
const dialog = document.getElementById("cityDialog");
currentInputType = e.target.dataset.type;
// Tính toán vị trí dựa trên phần tử kích hoạt
const rect = e.target.getBoundingClientRect();
dialog.style.top = (rect.bottom + window.scrollY) + "px";
dialog.style.left = rect.left + "px";
dialog.show();
loadCityGroup("HOT"); // Mặc định hiển thị các thành phố phổ biến
}
// Đóng popup khi click ra ngoài
document.addEventListener("click", function(e) {
const dialog = document.getElementById("cityDialog");
const isInput = e.target.hasAttribute('data-type');
if (!dialog.contains(e.target) && !isInput) {
dialog.close();
document.getElementById("suggestionBox").style.display = "none";
}
});
3. Phân loại và lọc thành phố
Hàm xử lý việc render danh sách thành phố dựa trên tab (Trong nước/Quốc tế) và các nhóm chữ cái.
let isInternational = false;
function loadCityGroup(groupKey) {
const container = document.getElementById("cityContainer");
container.innerHTML = "";
let filtered = [];
if (groupKey === "HOT") {
filtered = isInternational ? hotGlobalCities : hotDomesticCities;
} else {
// Lọc theo ký tự đầu của Pinyin/Tên
filtered = allCities.filter(city => {
const firstChar = city.split('|')[0].charAt(0).toUpperCase();
return groupKey.includes(firstChar);
});
}
const wrapper = document.createElement("div");
wrapper.className = "city-list-wrapper";
filtered.forEach(cityStr => {
const name = cityStr.split('|')[1];
const span = document.createElement("span");
span.className = "city-item";
span.textContent = name;
span.onclick = () => selectCity(name);
wrapper.appendChild(span);
});
container.appendChild(wrapper);
}
function selectCity(cityName) {
const targetId = currentInputType === 'origin' ? "departureField" : "arrivalField";
document.getElementById(targetId).value = cityName;
document.getElementById("cityDialog").close();
document.getElementById("suggestionBox").style.display = "none";
}
4. Chuyển đổi vùng miền
Cập nhật trạng thái giao diện khi người dùng chuyển giữa các tab danh mục.
function switchRegion(index) {
const tabs = document.querySelectorAll(".tab-item");
tabs.forEach((tab, i) => {
if (i === index) {
tab.classList.add("active");
} else {
tab.classList.remove("active");
}
});
isInternational = (index === 1);
loadCityGroup("HOT");
// Cập nhật lại thanh index chữ cái tương ứng với vùng miền
renderIndexBar(isInternational);
}
Để hoàn thiện component, bạn cần bổ sung CSS cho các class như city-dialog, suggestion-popup và city-grid để đảm bảo giao diện hiển thị đúng vị trí (thường là position: absolute cho suggestion và sử dụng flexbox/grid cho danh sách thành phố).