바닐라 JS
로 검색 리스트를 만들어 보고 싶어서 하드 코딩을 해봤다. 아무것도 참고 안 하고 내 머릿속에 들어 있는 정보로만 시도해 본 결과물이다.
HTML
HTML
은 간단하게 구성했다.
<body>
<input type="search">
<ul hidden></ul>
<div id="result"></div>
</body>
내가 생각한 흐름은 아래와 같다.
result
추가 ➡ 리스트 숨김input
입력 ➡ 입력 단어 포함한 요소 노출, 나머지 숨김result
추가 ➡ 리스트 숨김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);
});
평범한 리스트 생성이다.
input.addEventListener("focusin", () => {
ul.hidden = false;
});
/*
input.addEventListener("focusout", () => {
ul.hidden = true;
});
*/
초기에 input
에 포커스가 생기면 리스트를 보여주고, 벗어나면 숨기는 쪽으로 구현했다. 이 정도라면 크게 상관없지만, 리스트 요소를 클릭했을 때가 문제였다. 클릭하는 순간 input
의 포커스가 사라져 li
의 클릭 이벤트가 적용되지 않았다. 때문에 focuseout
이벤트는 삭제했다.
input.addEventListener("input", (e) => {
ul.childNodes.forEach((li) => {
li.hidden = li.textContent.includes(e.target.value) ? false : true;
});
});
input
에 입력값이 들어오면 ul
의 자식 요소 중 입력값을 포함한 요소만 노출하고 나머지는 숨겼다.
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
에 선택한 요소값이 추가되면서 리스트는 다시 숨는다. 검색한 요소를 제외한 나머지 요소의 hidden
이 true
인 상태여서 removeAttribute
로 모든 요소의 hidden
을 제거했다. 마지막으로 마우스가 벗어난 요소의 색상을 배경색과 동일하게 변경했다.
완성했다 싶었더니 문제가 있었다. 초기 리스트에는 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. 클릭으로 결과 추가 코드와 여기 코드가 비슷해 함수로 뺐다.
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";
});
}