Xây dựng component chọn thành phố với tính năng gợi ý bằng JavaScript thuần

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-popupcity-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ố).

Thẻ: JavaScript frontend Web Development DOM Manipulation

Đăng vào ngày 6 tháng 9 lúc 05:44