实现仿Stack Overflow的点击式多选标签输入框(选项来自HTTP请求)
Got it, let's build that tag input component just like Stack Overflow's! Here's a step-by-step implementation that covers all your requirements: remote option fetching, multi-select, tag display with deletion, and a clean, intuitive UI.
First, let's set up the core markup. We'll use a container to wrap everything, so we can style it like a single input field:
<div class="tag-input-container"> <!-- Container for selected tags --> <ul class="selected-tags"></ul> <!-- Input to capture user typing (we'll style the container instead of this input directly) --> <input type="text" class="tag-input" placeholder="Add tags..." /> <!-- Dropdown for remote options --> <ul class="tag-options"></ul> </div>
Let's make it look polished, matching the Stack Overflow tag vibe:
.tag-input-container { border: 1px solid #ccc; border-radius: 4px; padding: 8px; display: flex; flex-wrap: wrap; gap: 8px; min-height: 40px; align-items: center; position: relative; } .selected-tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; list-style: none; } .selected-tag { background-color: #e1ecf4; color: #39739d; padding: 4px 8px; border-radius: 3px; display: flex; align-items: center; gap: 4px; font-size: 14px; } .tag-delete-btn { cursor: pointer; font-size: 14px; line-height: 1; padding: 0; border: none; background: transparent; color: inherit; } .tag-input { border: none; outline: none; flex: 1; min-width: 120px; padding: 4px 0; font-size: 14px; } .tag-options { position: absolute; top: calc(100% + 4px); left: 8px; right: 8px; border: 1px solid #ccc; border-radius: 4px; background: white; list-style: none; padding: 0; margin: 0; max-height: 200px; overflow-y: auto; display: none; z-index: 100; } .tag-options.active { display: block; } .tag-option { padding: 8px 12px; cursor: pointer; font-size: 14px; } .tag-option:hover { background-color: #f0f0f0; }
Now let's add the functionality. We'll handle remote data fetching, selection, deletion, and UI updates:
// State management const selectedTags = []; let allOptions = []; // DOM Elements const tagContainer = document.querySelector('.tag-input-container'); const selectedTagsList = document.querySelector('.selected-tags'); const tagInput = document.querySelector('.tag-input'); const tagOptionsList = document.querySelector('.tag-options'); // Fetch options from server async function fetchTagOptions() { try { // Replace with your actual API endpoint const response = await fetch('/api/available-tags'); allOptions = await response.json(); renderTagOptions(allOptions); } catch (error) { console.error('Failed to fetch tags:', error); // Fallback with dummy tags if API fails allOptions = ['JavaScript', 'React', 'CSS', 'HTML', 'Node.js']; renderTagOptions(allOptions); } } // Render dropdown options (filters out already selected tags) function renderTagOptions(options) { tagOptionsList.innerHTML = ''; const availableOptions = options.filter(opt => !selectedTags.includes(opt)); availableOptions.forEach(option => { const li = document.createElement('li'); li.className = 'tag-option'; li.textContent = option; li.dataset.tag = option; tagOptionsList.appendChild(li); }); } // Render selected tags with delete buttons function renderSelectedTags() { selectedTagsList.innerHTML = ''; selectedTags.forEach(tag => { const li = document.createElement('li'); li.className = 'selected-tag'; li.innerHTML = `${tag} <button class="tag-delete-btn">×</button>`; li.dataset.tag = tag; selectedTagsList.appendChild(li); }); } // Add tag to selected list function addTag(tag) { if (!selectedTags.includes(tag)) { selectedTags.push(tag); renderSelectedTags(); renderTagOptions(allOptions); tagInput.value = ''; // Clear input after selection } } // Remove tag from selected list function removeTag(tag) { const index = selectedTags.indexOf(tag); if (index !== -1) { selectedTags.splice(index, 1); renderSelectedTags(); renderTagOptions(allOptions); tagInput.focus(); // Return focus to input for quick addition } } // Toggle dropdown visibility function toggleDropdown(show) { tagOptionsList.classList.toggle('active', show); } // Event Listeners // Fetch options on page load document.addEventListener('DOMContentLoaded', fetchTagOptions); // Open dropdown when input is focused tagInput.addEventListener('focus', () => toggleDropdown(true)); // Close dropdown when clicking outside the container document.addEventListener('click', (e) => { if (!tagContainer.contains(e.target)) { toggleDropdown(false); } }); // Handle option selection (event delegation for dynamic elements) tagOptionsList.addEventListener('click', (e) => { if (e.target.classList.contains('tag-option')) { const tag = e.target.dataset.tag; addTag(tag); toggleDropdown(false); } }); // Handle tag deletion (event delegation) selectedTagsList.addEventListener('click', (e) => { if (e.target.classList.contains('tag-delete-btn')) { const tag = e.target.closest('.selected-tag').dataset.tag; removeTag(tag); } }); // Optional: Filter options as user types tagInput.addEventListener('input', (e) => { const query = e.target.value.toLowerCase(); const filteredOptions = allOptions.filter(opt => opt.toLowerCase().includes(query)); renderTagOptions(filteredOptions); toggleDropdown(true); });
Key Features Explained:
- Remote Data Fetching: The
fetchTagOptionsfunction retrieves tags from your API endpoint. We added a fallback with dummy tags in case the API call fails. - Dynamic UI Updates: Two render functions keep the UI in sync with the
selectedTagsarray, ensuring tags and options update automatically when changes happen. - Event Delegation: Since tags and options are added/removed dynamically, we attach click listeners to their parent containers instead of individual elements—this avoids broken event bindings.
- User Experience: Clicking outside closes the dropdown, deleting a tag returns focus to the input, and typing filters options in real-time to help users find tags faster.
You can tweak the CSS to match your app's design system, and adjust the JavaScript logic to handle edge cases like duplicate tag prevention or loading states for the API call.
内容的提问来源于stack exchange,提问作者Gabax

