input 태그를 사용하여 자동 검색을 만들어 보도록 하겠습니다. 우선 소스 코드입니다.
javascript입니다.
const userCardContainer = document.querySelector("[data-user-cards-container]");
const userCardTemplate = document.querySelector("[data-user-template]");
const headerSearchInput = document.getElementById("headerSearch");
const portfolioSearchInput = document.getElementById("productSearch");
let company = "A";
let dataSet = "./information.json";
var companyName = document.getElementsByClassName("companyName");
let users = [];
let currentPage = 1;
const pageCount = 20;
let max = 0;
function goToPage(page)
{
currentPage = page;
updatePage();
}
function prevPage()
{
currentPage--;
if (currentPage < 1)
currentPage = 1;
updatePage();
}
function nextPage()
{
currentPage++;
if (currentPage > max)
currentPage = max;
updatePage();
}
function updatePageNumbers()
{
const pageNumbers = document.getElementById('page-numbers');
pageNumbers.innerText = '';
for (let i = 1; i <= max; i++)
{
const button = document.createElement('button');
button.textContent = i;
button.onclick = () => goToPage(i);
if (i == currentPage)
button.classList.add('active');
pageNumbers.appendChild(button);
}
}
function updatePage()
{
let index = 0;
const start = (currentPage - 1) * pageCount;
const end = start + pageCount;
users.forEach(user => {
user.element.classList.toggle("pageHide", false);
if (!user.element.classList.contains("hide"))
{
if (index < start || index >= end)
{
user.element.classList.toggle("pageHide", true);
}
index++;
}
});
updatePageNumbers();
}
function changePage(newPage, count)
{
max = Math.ceil(count / pageCount);
if (newPage < 1)
currentPage = 1;
else if (max < newPage)
currentPage = max;
else
currentPage = newPage;
updatePage();
updatePageNumbers();
}
function syncInputsAndSearch(event)
{
currentPage = 1;
let count = 0;
const value = event.target.value.toLowerCase();
headerSearchInput.value = value;
portfolioSearchInput.value = value;
if (value === "")
updateCompanyButtons();
else
clearCompanyButtons();
users.forEach(user => {
let isVisible;
if (value === "")
{
isVisible = user.company === company;
}
else
{
isVisible = user.name.toLowerCase().includes(value);
}
user.element.classList.toggle("hide", !isVisible);
if (isVisible)
count++;
});
changePage(currentPage, count);
}
function updateCompanyButtons()
{
for (let i = 0; i < companyName.length; i++)
{
companyName[i].classList.remove("clicked");
if (companyName[i].textContent === company)
{
currentPage = 1;
companyName[i].classList.add("clicked");
}
}
}
function clearCompanyButtons()
{
for (let i = 0; i < companyName.length; i++)
{
companyName[i].classList.remove("clicked");
}
}
function handleCompanyClick(event)
{
currentPage = 1;
const clickedCompany = event.target.textContent;
company = clickedCompany;
updateCompanyButtons();
updateProductDisplay();
headerSearchInput.value = '';
portfolioSearchInput.value = '';
}
function updateProductDisplay()
{
currentPage = 1;
let count = 0;
users.forEach(user => {
const isVisible = user.company === company;
if (isVisible)
count++;
user.element.classList.toggle("hide", !isVisible);
});
changePage(currentPage, count);
}
function init()
{
for (let i = 0; i < companyName.length; i++)
{
companyName[i].addEventListener("click", handleCompanyClick);
}
updateCompanyButtons();
}
headerSearchInput.addEventListener("input", syncInputsAndSearch);
portfolioSearchInput.addEventListener("input", syncInputsAndSearch);
init();
fetch(dataSet)
.then(res => res.json())
.then(data => {
users = data.map(user => {
const card = userCardTemplate.content.cloneNode(true).children[0];
const modelNameLink = card.querySelector("[model-name-link]");
const modelName = card.querySelector("[model-name]");
const productDescription = card.querySelector("[product-description]");
const productImg = card.querySelector("[product-image]");
modelNameLink.href = user.link;
modelName.textContent = user.name;
productDescription.textContent = user.explain;
productImg.src = user.img;
userCardContainer.append(card);
return { link: user.link, name: user.name, explain: user.explain, img: user.img, company: user.company, element: card };
});
updateProductDisplay();
});
조금 많이 기네요.
html입니다.
<input type="text" id="headerSearch" data-animation-scroll='true' data-target="#product" type="search" placeholder="All Product Search" autocomplete="off" data-search>
...
<div class="search-wrapper">
<input type="text" id="productSearch" type="search" placeholder="All Product Search" autocomplete="off" data-search>
</div>
<div class="navigation">
<button class="companyName">A</button>
<button class="companyName">B</button>
<button class="companyName">C</button>
<button class="companyName">D</button>
<button class="companyName">E</button>
</div>
<div class="user-cards" data-user-cards-container></div>
<template data-user-template>
<div class="card">
<p>
<a href="" target="_blank" class="modelNameLink" model-name-link>
<span class="modelName" model-name></span>
<span class="productDescription" product-description></span>
<img src="" alt="Product Image" class ="productImg" product-image>
</a>
</p>
</div>
</template>
<div class="pagination">
<div id="prev-btn" onclick="prevPage()" class="arrow">←</div>
<div class="page-numbers" id="page-numbers"></div>
<div id="next-btn" onclick="nextPage()" class="arrow">→</div>
</div>
...
html 먼저 설명을 해드리도록 하겠습니다.
HTML
<input type="text" id="headerSearch" data-animation-scroll='true' data-target="#product" type="search" placeholder="All Product Search" autocomplete="off" data-search>
...
<input type="text" id="productSearch" type="search" placeholder="All Product Search" autocomplete="off" data-search>
우선 input 태그가 2개가 있습니다. 이는 서로 연동이 되게 하기 위함입니다.
간단하게 말하면 어느 input 태그에 적어도 같은 내용이 적히게 만들기 위함입니다.
<div class="navigation">
<button class="companyName">A</button>
<button class="companyName">B</button>
<button class="companyName">C</button>
<button class="companyName">D</button>
<button class="companyName">E</button>
</div>
A 회사용 제품, B 회사용 제품, C 회사용 제품, D 회사용 제품, E 회사용 제품들로 나뉩니다.
</div class="user-cards" data-user-cards-container>
이 부분은 나중에 나오는 card라는 클래스를 담아두는 요소라고 생각하면 좋습니다.
<template data-user-template>
<div class="card">
<p>
<a href="" target="_blank" class="modelNameLink" model-name-link>
<span class="modelName" model-name></span>
<span class="productDescription" product-description></span>
<img src="" alt="Product Image" class ="productImg" product-image>
</a>
</p>
</div>
</template>
우선 template 요소에 대해 알아야 합니다. template는 콘텐츠 조각을 나중에 사용하기 위해 담아놓는 컨테이너입니다. 페이지를 불러올 때에 template부분도 읽기는 하지만 이는 렌더링이 아닌 유효성을 검사하기 위함입니다. 이는 나중에 JavaScript를 사용해 인스턴스를 생성하여 렌더링을 하게 됩니다.
필요한 정보를 template 요소 안에 잘 넣습니다.
<div class="pagination">
<div id="prev-btn" onclick="prevPage()" class="arrow">←</div>
<div class="page-numbers" id="page-numbers"></div>
<div id="next-btn" onclick="nextPage()" class="arrow">→</div>
</div>
마지막으로 pagination입니다. 원하는 화면으로 넘어갈 수 있게 숫자와 화살표를 만들어 놓습니다. 이때에 page-numbers 클래스 또한 해당되는 제품 개수가 다르기 때문에 javascript의 도움을 받아야 합니다.
javascript 부분입니다.
JavaScript
const userCardContainer = document.querySelector("[data-user-cards-container]");
const userCardTemplate = document.querySelector("[data-user-template]");
const headerSearchInput = document.getElementById("headerSearch");
const portfolioSearchInput = document.getElementById("productSearch");
let company = "A";
let dataSet = "./information.json";
var companyName = document.getElementsByClassName("companyName");
let users = [];
let currentPage = 1;
const pageCount = 20;
let max = 0;
querySelector은 특정 요소의 가장 첫 번째 것을 가져옵니다.
getElementById는 주어진 문자열과 일치하는 id 속성을 가진 요소를 찾고 이를 반환합니다. (id는 당연히 한 개이죠.)
getElementsByClassName은 클래스 이름을 갖는 모든 요소를 배열로 반환을 합니다.
function goToPage(page)
{
currentPage = page;
updatePage();
}
function prevPage()
{
currentPage--;
if (currentPage < 1)
currentPage = 1;
updatePage();
}
function nextPage()
{
currentPage++;
if (currentPage > max)
currentPage = max;
updatePage();
}
gotToPage, prevPage, nextPage는 설명을 건너뛰도록 하겠습니다.
function updatePage()
{
let index = 0;
const start = (currentPage - 1) * pageCount;
const end = start + pageCount;
users.forEach(user => {
user.element.classList.toggle("pageHide", false);
if (!user.element.classList.contains("hide"))
{
if (index < start || index >= end)
{
user.element.classList.toggle("pageHide", true);
}
index++;
}
});
updatePageNumbers();
}
위의 내용을 설명하기 전에 사용되는 함수들을 알아보도록 하겠습니다.
user.element.classList.contains(str) 함수는 user에 str클래스의 유무를 보고 판단해 줍니다.
user.element.classList.toggle(className, force) 함수는 user에 className를 force의 값에 따라 true(추가) 혹은 false(삭제)를 합니다.
이제 자세히 살펴보도록 하겠습니다.
start와 end를 사용하여 선별된 데이터에서 시작 인덱스와 끝 인덱스를 정합니다.
users.forEach는 for문이라고 생각하면 좋을 것 같습니다. users배열 안에 있는 객체들을 하나하나 user로 반환받아 처리를 하게 됩니다.
"user.element.classList.toggle("pageHide", false);"를 통하여 "pageHide" 속성을 삭제합니다. "pageHide" 속성을 포함한다면 display가 none으로 됩니다. 즉, 안 보이게 됩니다.
관련 css 코드입니다.
.pageHide { display: none;}
hide가 클래스가 없는 요소들이 이번에 보여주어야 할 data이기 때문에 "!user.element.classList.contains("hide")"를 사용하여 hide 클래스가 있는지 없는지 판단을 합니다.
start보다 작거나 end보다 크거나 같을 경우에는 pageHide 속성을 추가해 줍니다.
function updatePageNumbers()
{
const pageNumbers = document.getElementById('page-numbers');
pageNumbers.innerText = '';
for (let i = 1; i <= max; i++)
{
const button = document.createElement('button');
button.textContent = i;
button.onclick = () => goToPage(i);
if (i == currentPage)
button.classList.add('active');
pageNumbers.appendChild(button);
}
}
위의 내용을 설명하기 전에 사용되는 함수들을 알아보도록 하겠습니다.
innerText는 입력받은 문자열 그대로 초기화를 진행합니다.
document.createElement는 지정한 tageName의 HTML 요소를 만들어 반환을 합니다.
textContent는 텍스트 콘텐츠를 표현하게 해 줍니다.
onclick = () => function()은 click시 실행될 함수를 선언해 줍니다.
classList.add()는 인자값을 주어진 속성 목록에 추가를 합니다. 만약 실패할 경우에는 throw를 하기 때문에 주의가 필요합니다.
appendChilld()는 인자를 특정 부모 노드의 자식 노드 리스트 중 마지막 자식으로 붙입니다.
함수들에 대한 설명만 듣고 위의 코드를 다시 보면 이해가 되시겠지만 한 번만 풀어서 더 설명드리겠습니다.
"page-numbers" id를 가진 요소를 초기화를 해주기 위해 innerText를 사용합니다.
button요소를 만들기 위해 createElement를 사용하여 button을 만듭니다.
textContent를 사용하여 각각의 번호에 맞게 입력을 해줍니다.
클릭 시 실행될 함수도 button.onclick = () => goToPage(i) 이렇게 만들어 줍니다.
현재 페이지라면 active 요소를 추가하여 디자인도 바꾸어줍니다.
마지막으로 pageNumbers에 만들어 놓은 button을 추가해 줍니다.
function changePage(newPage, count)
{
max = Math.ceil(count / pageCount);
if (newPage < 1)
currentPage = 1;
else if (max < newPage)
currentPage = max;
else
currentPage = newPage;
updatePage();
updatePageNumbers();
}
function syncInputsAndSearch(event)
{
currentPage = 1;
let count = 0;
const value = event.target.value.toLowerCase();
headerSearchInput.value = value;
portfolioSearchInput.value = value;
if (value === "")
updateCompanyButtons();
else
clearCompanyButtons();
users.forEach(user => {
let isVisible;
if (value === "")
{
isVisible = user.company === company;
updateCompanyButtons();
}
else
{
isVisible = user.name.toLowerCase().includes(value);
clearCompanyButtons();
}
user.element.classList.toggle("hide", !isVisible);
if (isVisible)
count++;
});
changePage(currentPage, count);
}
changePage함수는 역할만 설명하고 넘어가도록 하겠습니다.
max page를 정합니다. 예를 들어 한 page당 20개의 data를 보여주고 총 data가 102개일 때에 max page는 6이 됩니다.
다음으로 syncInputsAndSearch함수입니다. 이는 "input"이벤트가 발생했을 때에 실행하는 함수입니다.
역시 필요한 계념 먼저 설명을 하겠습니다.
event.target.value.toLowerCase()에 대해 설명해 드리겠습니다. event.target은 이벤트가 발생한 대상 요소를 가리킵니다. 객체에서 value를 뽑아냅니다. 마지막으로 toLowerCase함수로 소문자로 바꿉니다.
이제 로직을 설명해 드리도록 하겠습니다.
input 이벤트가 발생했다는 것은 검색을 시작했다는 겁니다. 그러면 처음부터 보여주어야 하기 때문에 currentPage를 1로 초기화를 해줍니다.
count 같은 경우에는 이번에 보여줄 총 data의 수를 세주는 변수입니다.
검색을 진행할 때에 대소문자를 구분하지 않기 위해 소문자로 바꾸어줍니다.
다음으로 회사의 버튼을 조작해 줄 것입니다. 만약 value값이 없다면 즉, 검색창에 입력된 데이터가 없다면 원래 회사만 불이 들어오게 만들어주고 그렇지 않고 value값이 존재한다면 전체에서 검색을 하는 것이기 때문에 모든 회사의 불이 꺼져야 합니다.
다음으로 hide 속성을 toggle 해줄 기준을 정할 차례입니다. 방식은 위의 pageHide와 비슷하기 때문에 생략하도록 하겠습니다. 마지막으로 보여줄 data의 개수를 잘 세주고 changePage함수를 실행합니다.
function updateCompanyButtons()
{
for (let i = 0; i < companyName.length; i++)
{
companyName[i].classList.remove("clicked");
if (companyName[i].textContent === company)
{
currentPage = 1;
companyName[i].classList.add("clicked");
}
}
}
function clearCompanyButtons()
{
for (let i = 0; i < companyName.length; i++)
{
companyName[i].classList.remove("clicked");
}
}
function handleCompanyClick(event)
{
currentPage = 1;
const clickedCompany = event.target.textContent;
company = clickedCompany;
updateCompanyButtons();
updateProductDisplay();
headerSearchInput.value = '';
portfolioSearchInput.value = '';
}
function updateProductDisplay()
{
currentPage = 1;
let count = 0;
users.forEach(user => {
const isVisible = user.company === company;
if (isVisible)
count++;
user.element.classList.toggle("hide", !isVisible);
});
changePage(currentPage, count);
}
classList.remove(tok)에 대해서만 설명해 드리면 코드를 읽는 데에 있어서 큰 문제는 없을 것입니다. 현재 속성 목록에서 tok를 제거하는 것입니다.
이제 각각의 함수에 대해 간단하게 설명해 드리겠습니다.
updateCompanyButtons는 필요한 요소만 clicked 속성을 끼는 것입니다.
clearCompanyButtons는 모든 요소의 clicked 속성을 제거합니다.
handleCompanyClick 함수는 event함수입니다. 회사 버튼이 클릭되면 실행되는 함수입니다.
updateProductDisplay 함수는 처음에 시작할 때에 실행하는 함수입니다. 초기화해 주는 함수라고 생각하면 좋을 것 같습니다.
function init()
{
for (let i = 0; i < companyName.length; i++)
{
companyName[i].addEventListener("click", handleCompanyClick);
}
updateCompanyButtons();
}
headerSearchInput.addEventListener("input", syncInputsAndSearch);
portfolioSearchInput.addEventListener("input", syncInputsAndSearch);
init();
fetch(dataSet)
.then(res => res.json())
.then(data => {
users = data.map(user => {
const card = userCardTemplate.content.cloneNode(true).children[0];
const modelNameLink = card.querySelector("[model-name-link]");
const modelName = card.querySelector("[model-name]");
const productDescription = card.querySelector("[product-description]");
const productImg = card.querySelector("[product-image]");
modelNameLink.href = user.link;
modelName.textContent = user.name;
productDescription.textContent = user.explain;
productImg.src = user.img;
userCardContainer.append(card);
return { link: user.link, name: user.name, explain: user.explain, img: user.img, company: user.company, element: card };
});
updateProductDisplay();
});
필요 개념과 함수 설명을 같이 드리겠습니다.
addEventListener(type, function);은 특정 이벤트에 function이 발생하도록 설정을 해놓는 것이다. type에는 여러 종류가 있는데 여기서는 click 이벤트와 input 이벤트만 사용을 하였다.
fetch 함수는 네트워크 요청을 비동기적(특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고 다음 코드를 먼저 실행하는 것)으로 수행을 합니다. then은 fetch를 성공했을 때의 부분이다. then으로 res 객체를 리턴 받는다.(. then(res =>) 이를 json파일로 파싱을 하고(res.json()) 다시 then으로 data 객체를 리턴 받는다. 이때 data는 일반적으로 객체나 배열의 형태이다. 다음으로 map함수를 통하여 배열의 각 요소를 순회하며 새로운 배열을 생성을 한다. 그게 users이다.
"userCardTemplate.content.cloneNode(true).children[0]"부분은 template부분의 자식 노드까지 깊은 복사를 하여 첫 번째 자식 요소를 나타내는 것이다. 그렇게 한 후에 각각의 요소에 추가를 다한 후에 userCardContainer에 card를 추가합니다.
마지막으로 return을 하여 배열에 있는 객체를 반환을 하나하나씩 합니다.
이렇게 긴 포스팅이 끝났습니다. "자동 검색 및 pagination!" 쉽지 않은 주제로 이야기를 나누어 보았는데 긴 포스팅 읽어주셔서 너무 감사드리고 하시는 프로젝트가 잘 마무리되셨으면 좋겠습니다. 감사합니다.
'Language > javascript' 카테고리의 다른 글
html 요소 클릭시 문자열 복사하기 (0) | 2024.11.04 |
---|---|
모바일 전용 페이지 만들기 (0) | 2024.10.28 |
자연스로운 스크롤 이동 (0) | 2024.10.21 |