[JavaScript] 조잡한 검색 리스트 만들기

찐새·2022년 12월 9일
0

Javascript

목록 보기
6/11
post-thumbnail

바닐라 JS로 검색 리스트를 만들어 보고 싶어서 하드 코딩을 해봤다. 아무것도 참고 안 하고 내 머릿속에 들어 있는 정보로만 시도해 본 결과물이다.

기본 HTML

HTML은 간단하게 구성했다.

<body>
  <input type="search">
  <ul hidden></ul>
  <div id="result"></div>
</body>

내가 생각한 흐름은 아래와 같다.

  1. 더미 배열로 리스트 생성 ➡ 요소 클릭 시 result 추가 ➡ 리스트 숨김
  2. input 입력 ➡ 입력 단어 포함한 요소 노출, 나머지 숨김
  3. 검색 결과 요소 클릭 시 result 추가 ➡ 리스트 숨김

1. 리스트 생성

const input = document.querySelector("input");
const ul = document.querySelector("ul");
const result = document.getElementById("result");
const fruits = ["apple","banana","orange","lemon","lime","pure","peach","berry"];

fruits.forEach((fruit) => {
  const li = document.createElement("li");
  li.innerText = fruit;
  ul.appendChild(li);
});

평범한 리스트 생성이다.

2. 리스트 노출 및 숨김

input.addEventListener("focusin", () => {
  ul.hidden = false;
});

/*
input.addEventListener("focusout", () => {
  ul.hidden = true;
});
*/

초기에 input에 포커스가 생기면 리스트를 보여주고, 벗어나면 숨기는 쪽으로 구현했다. 이 정도라면 크게 상관없지만, 리스트 요소를 클릭했을 때가 문제였다. 클릭하는 순간 input의 포커스가 사라져 li의 클릭 이벤트가 적용되지 않았다. 때문에 focuseout 이벤트는 삭제했다.

3. 검색

input.addEventListener("input", (e) => {
  ul.childNodes.forEach((li) => {
    li.hidden = li.textContent.includes(e.target.value) ? false : true;
  });
});

input에 입력값이 들어오면 ul의 자식 요소 중 입력값을 포함한 요소만 노출하고 나머지는 숨겼다.

4. 클릭으로 결과 추가

input.addEventListener("input", (e) => {
  ul.childNodes.forEach((li) => {
    li.hidden = li.textContent.includes(e.target.value) ? false : true;
    li.addEventListener("mouseover", () => {
      li.style.cursor = "pointer";
      li.style.background = "pink";
      li.addEventListener("click", (e) => {
        result.innerText = e.target.textContent;
        ul.hidden = true;
        input.value = "";
        ul.childNodes.forEach((li) => li.removeAttribute("hidden"));
      });
    });
    li.addEventListener("mouseleave", () => {
      li.style.background = "transparent";
    });
  });
});

3. 검색 코드 내에 추가했다. li에 마우스을 올리면 해당 요소가 강조된다. 클릭하면 result에 선택한 요소값이 추가되면서 리스트는 다시 숨는다. 검색한 요소를 제외한 나머지 요소의 hiddentrue인 상태여서 removeAttribute로 모든 요소의 hidden을 제거했다. 마지막으로 마우스가 벗어난 요소의 색상을 배경색과 동일하게 변경했다.

5. 초기 리스트에도 클릭 적용

완성했다 싶었더니 문제가 있었다. 초기 리스트에는 click이벤트가 적용되지 않았다. input내에서만 동작하도록 작성한 까닭이었다. 4. 클릭으로 결과 추가 내부의 코드를 조금만 수정해서 초기 리스트 생성 코드에 붙여넣어 해결했다.

fruits.forEach((fruit) => {
  const li = document.createElement("li");
  li.innerText = fruit;
  ul.appendChild(li);
  li.addEventListener("mouseover", () => {
    li.style.cursor = "pointer";
    li.style.background = "pink";
    li.addEventListener("click", (e) => {
      result.innerText = e.target.textContent;
      ul.hidden = true;
      ul.childNodes.forEach((li) => li.removeAttribute("hidden"));
    });
  });
  li.addEventListener("mouseleave", () => {
    li.style.background = "transparent";
  });
});

검색 요소 포함 여부 코드와 input 초기화 코드를 제외한 나머지 코드를 붙여넣었다. 이렇게 보니 4. 클릭으로 결과 추가 코드와 여기 코드가 비슷해 함수로 뺐다.

6. 리펙토링

function clickItem(ul, li, input, searchItem) {
  if (searchItem) {
    li.hidden = li.textContent.includes(searchItem) ? false : true;
  }
  li.addEventListener("mouseover", () => {
    li.style.cursor = "pointer";
    li.style.background = "pink";
    ul.hidden = false;
    li.addEventListener("click", (e) => {
      result.innerText = e.target.textContent;
      ul.hidden = true;
      if (input) {
        input.value = "";
      }
      ul.childNodes.forEach((li) => li.removeAttribute("hidden"));
    });
  });
  li.addEventListener("mouseleave", () => {
    li.style.background = "transparent";
  });
}

처음에는 인자를 서순 없이 입력했는데, 그랬더니 인자가 잘못 들어가는 경우가 발생했다. JavaScript에서는 인자를 주입하지 않으면 undefined로 취급하기 때문에 공통 인자(ul, li)는 앞으로, 옵션 인자(input, searchItem)는 뒤로 뺐다.

// 5번 코드 수정
fruits.forEach((fruit) => {
  const li = document.createElement("li");
  li.innerText = fruit;
  ul.appendChild(li);
  clickItem(ul, li);
});

// 4번 코드 수정
input.addEventListener("input", (e) => {
  ul.childNodes.forEach((li) => {
    clickItem(ul, li, input, e.target.value);
  });
});

아무것도 선택하지 않았을 시에 리스트가 남아 있어 다시 focusout을 추가했다. 리스트 요소 클릭 시 적용 안 되는 점은 removeEventListener로 해결했다. clickItem 함수 내에서 input 요소를 전역으로 사용하기 위해 인자와 다른 이름으로 바꿨다. input ➡ search

// 2번 코드 수정
function handleHidden() {
  ul.hidden = ul.hidden ? false : true;
}
search.addEventListener("focusin", handleHidden);
search.addEventListener("focusout", handleHidden);

// 6번 코드 수정
function clickItem(ul, li, input, searchItem) {
  if (searchItem) {
    li.hidden = li.textContent.includes(searchItem) ? false : true;
  }
  li.addEventListener("mouseover", () => {
    // 리스트에 마우스가 있으면 `focusout` 이벤트 제거
    search.removeEventListener("focusout", handleHidden);
    li.style.cursor = "pointer";
    li.style.background = "pink";
    ul.hidden = false;
    li.addEventListener("click", (e) => {
      result.innerText = e.target.textContent;
      ul.hidden = true;
      if (input) {
        input.value = "";
      }
      ul.childNodes.forEach((li) => li.removeAttribute("hidden"));
    });
  });
  li.addEventListener("mouseleave", () => {
    // 리스트를 벗어나 클릭하면 다시 `focusout` 이벤트 할당
    search.addEventListener("focusout", handleHidden);
    li.style.background = "transparent";
  });
}

결과

전체 코드

const search = document.querySelector("input");
const ul = document.querySelector("ul");
const result = document.getElementById("result");
const fruits = [
  "apple",
  "banana",
  "orange",
  "lemon",
  "lime",
  "pure",
  "peach",
  "berry",
];

fruits.forEach((fruit) => {
  const li = document.createElement("li");
  li.innerText = fruit;
  ul.appendChild(li);
  clickItem(ul, li);
});

function handleHidden() {
  ul.hidden = ul.hidden ? false : true;
}

search.addEventListener("focusin", handleHidden);

search.addEventListener("focusout", handleHidden);

search.addEventListener("input", (e) => {
  ul.childNodes.forEach((li) => {
    clickItem(ul, li, search, e.target.value);
  });
});

function clickItem(ul, li, input, searchItem) {
  if (searchItem) {
    li.hidden = li.textContent.includes(searchItem) ? false : true;
  }
  li.addEventListener("mouseover", () => {
    search.removeEventListener("focusout", handleHidden);
    li.style.cursor = "pointer";
    li.style.background = "pink";
    ul.hidden = false;
    li.addEventListener("click", (e) => {
      result.innerText = e.target.textContent;
      ul.hidden = true;
      if (input) {
        input.value = "";
      }
      ul.childNodes.forEach((li) => li.removeAttribute("hidden"));
    });
  });
  li.addEventListener("mouseleave", () => {
    search.addEventListener("focusout", handleHidden);
    li.style.background = "transparent";
  });
}
profile
프론트엔드 개발자가 되고 싶다

0개의 댓글