Day 30: Debounced Search with Filter
Fetch data once, then use a debounced input and .filter() to search results.
Requirements
- Fetch local JSON
- Render all items
- Add search input
- Debounce input by 300-500ms
- Use .filter() to create matching results
- Search across one or two fields
- Re-render filtered results
- Show empty state
- Add clear button if time allows
JavaScript focus
- fetch
- .filter()
- .includes()
- .toLowerCase()
- debounce with setTimeout / clearTimeout
- render function
MDN prep
Debounced Search with Filter
No results found.
The HTML
<section class="section search_section">
<h2 class="secondary_heading">Debounced Search with Filter</h2>
<div class="search_wrap">
<label for="search">Search</label>
<input type="text" id="search" class="search_input" placeholder="Search by name, role, or topic..." />
<button type="button" class="button search_clear">Clear</button>
</div>
<p class="search_empty" hidden>No results found.</p>
<div class="search_results transformed_cards">
<!-- render the cards here
<article class="article">
<h3 class="tertiary_heading">1) Ada Lovelace</h3>
<p>Role: Mathematician</p>
<p>Topic: Computing</p>
<p>Location: London</p>
<p class="featured">Featured</p>
</article>
-->
</div>
</section>
The JavaScript
async function initFetchFilteredData() {
try {
const response = await fetch("../data/filterd-data.json");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("There was a problem: ", error);
}
}
function initRenderFilteredUI(data) {
console.log("Original: ", data);
const root = document.querySelector(".search_section");
if (!root) return;
const input = root.querySelector(".search_input");
const clear = root.querySelector(".search_clear");
const empty = root.querySelector(".search_empty");
const cards = root.querySelector(".search_results");
function renderFilteredResults(data) {
// clear the articles before render
cards.innerHTML = "";
// render the articles
data.forEach((item) => {
// console.log(item);
const article = document.createElement("article");
article.classList.add("article");
const h3 = document.createElement("h3");
h3.classList.add("tertiary_heading");
const role = document.createElement("p");
const topic = document.createElement("p");
const location = document.createElement("p");
const featured = document.createElement("p");
featured.classList.add("featured");
h3.textContent = `${item.id}) ${item.name}`;
role.textContent = `Role: ${item.role}`;
topic.textContent = `Topic: ${item.topic}`;
location.textContent = `Location: ${item.location}`;
featured.textContent = "Featured";
if (item.featured) {
article.append(h3, role, topic, location, featured);
} else {
article.append(h3, role, topic, location);
}
cards.append(article);
});
}
// all good so far!
renderFilteredResults(data);
let timer;
input.addEventListener("input", (e) => {
clearTimeout(timer);
timer = setTimeout(() => {
const search = e.target.value.toLowerCase().trim();
const matchedItems = data.filter((item) => {
const match = item.name.toLowerCase().includes(search) || item.role.toLowerCase().includes(search) || item.topic.toLowerCase().includes(search);
return match;
});
console.log("Filtered Data: ", matchedItems);
renderFilteredResults(matchedItems);
// i think this is fine...
if (matchedItems.length === 0) {
empty.hidden = false;
} else {
empty.hidden = true;
}
}, 500);
});
clear.addEventListener("click", () => {
input.value = "";
renderFilteredResults(data);
});
}
async function init() {
const data = await initFetchFilteredData();
if (!data) return;
initRenderFilteredUI(data);
}
init();