Zum Inhalt springen

Shopping List – A Must Know Vanilla Javascript problem for any Frontend Interview (Part 1)

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

shopping-list-info

shopping-list-info2

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.

Without debounce,
without-debounce

With debounce,
with debounce

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 🙂

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert