This is the Vanilla-JS implementation of the Shopping List problem. Link to the react implementation will be below.
Problem
Create a shopping list web application with search, add, check off, and delete functionality.
As the user starts typing, it should hit the endpoint and show a list of partial matches. Clicking an item should add it to the results list.
Requirements
- Entering more than two characters in the input should show a list of partially matching items (starting with the same characters)
- Clicking an item in the list of partially matching items should add it to the list
- Adding the same item multiple times is allowed
- Pressing the ‚X‘ next to an item should delete it from the list
- Pressing the ‚✓‘ next to an item should check it off (i.e. strikethrough text and partially grey out text/buttons)
- Pressing the ‚✓‘ next to a checked-off item should uncheck it again
Key Skills to be tested
- Debounce Knowledge
- Rendering lists with Unique Items
- Filtering list to remove item
- Marking item on list as complete.
Solution
A major part of this exercise is building an efficient search feature with debounce.
Debounce is a technique in javascript that delays network request by few microseconds while user is typing to prevent unnecessary network call.
Walkthrough…
Fetch Data
First, define the function to get the list of food from the demo api https://api.frontendeval.com/fake/food/:food
async function getList(value) {
if (!value) return [];
try {
const response = await fetch(
`https://api.frontendeval.com/fake/food/${value}`,
{ method: "GET" }
);
if (!response.ok) {
throw new Error(`Response error ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.log("Request Error", error.message);
return [];
}
}
Debounce Function
A debounce function uses setTimeout
to delay the network request for a short while. If while it is being delayed, another network request is made by typing, it cancels the previous request using clearTimeout
, and makes a new network request with the most recent typed values.
Basically, the debounce function cancels old requests while the user is still typing.
// debounce function
function debounce(func, delay = 1000) {
let timer;
return function (...args) {
clearTimeout(timer); // cancels older network request
timer = setTimeout(() => {
func.apply(this, args); // calls the user defined function with the args (or payload passed into it)
}, delay);
};
}
Bind the Search Field
Add event listener to the searchField such that when a user types, it uses the debounce to make the API request.
Renders the search result container as user types and remove when empty.
const searchField = document.getElementById("searchfield");
const searchResultsContainer = document.getElementById("search-results");
const selectedContainer = document.getElementById("results");
searchResultsContainer.style.display = "none";
searchField.addEventListener(
"input",
debounce(async (e) => {
const val = e.target.value;
const res = await getList(val); // makes API call to get list of foods from the backend service.
if (val.trim() === "") {
searchResultsContainer.style.display = "none";
} else if (val.length > 1) {
renderSearchResult(res);
}
}, 500)
);
Render Search Suggestions
Rendering search suggestions with the list of responses got from the network request.
Add a click
event listener to the item to generate a random id
for each of the items in the response. This helps to be able to identify the exact item when there’s need to mark an item for deletion or completion.
Then, render the selected Item in the results list.
Also, overwrite the existing search list when there’s new data with replaceChildren()
function renderSearchResult(response = []) {
searchResultsContainer.replaceChildren(); // this overwrites the existing search results list
response.forEach((item, index) => {
const p = document.createElement("p");
p.textContent = item;
p.addEventListener("click", (e) => {
const id = Math.floor(100 * Math.random()) + 1;
renderSelected({ id, item });
});
searchResultsContainer.appendChild(p);
});
searchResultsContainer.style.display = "block";
}
Rendering selected items
Render the selected item with delete and check off features.
When rendering the selected food items to the results container, add eventListeners
to mark item as completed
and delete
item from list.
function renderSelected(selected = {}) {
const listItem = document.createElement("div");
const listInfo = document.createElement("info");
const check = document.createElement("input");
const title = document.createElement("p");
const delBtn = document.createElement("button");
title.className = "food-name";
title.textContent = selected.item;
check.type = "checkbox";
listInfo.className = "info";
listInfo.appendChild(check);
listInfo.appendChild(title);
delBtn.className = "del-btn";
delBtn.textContent = "X";
listItem.appendChild(listInfo);
listItem.appendChild(delBtn);
listItem.className = "list-item";
listItem.id = selected.id;
selectedContainer.appendChild(listItem);
check.addEventListener("click", (e) => {
if (check.checked) {
listItem.style.opacity = 0.5;
title.style.textDecoration = "line-through";
} else {
listItem.style.opacity = 1;
title.style.textDecoration = "none";
}
});
delBtn.addEventListener("click", (e) => {
selectedContainer.removeChild(listItem);
});
}
Hiding the search results on click out
Finally, to hide the search-results container when a user clicks outside the container.
This is quite tricky but simple. Basically, add a click listener to the document directly, and on every click anywhere in the document, check if the clicked element is NOT inside the search results container searchResultsContainer
to remove the search result container
document.addEventListener("click", (e) => {
if (!searchResultsContainer.contains(e.target)) {
searchResultsContainer.style.display = "none";
}
});
Link to code on Github.
Link to React-JS solution walkthrough here
Leave a comment if you have any questions. Like and share if this is helpful to you in some way 🙂