Как сделать пагинацию используя HTML и JS и не сломать фильтр?

Всем небезразличным, доброго времени суток.

Так как не знаю JS, не получается сделать пагинацию карточек товара. Где только я не смотрел и куда только не залазил, нигде не могу найти такую реализацию, чтобы за основу был взят код HTML. С фильтром вроде разобрался, а пагинацию не могу найти.

Вот сам код

function app() {
  const buttons = document.querySelectorAll('.btn')
  const cards = document.querySelectorAll('.card')


  function filter (category, items) {
    items.forEach((item) => {
      const isItemFiltered = !item.classList.contains(category)
      const isShowAll = category === 'all'
      if (isItemFiltered && !isShowAll) {
        item.classList.add('hide-filter')
      } else {
        item.classList.remove('hide-filter')
      }
    })
  }

  buttons.forEach((button) => {
    button.addEventListener('click', () => {
      const currentCategory = button.dataset.filter
      filter(currentCategory, cards)
    })
  })
}

app()
body {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.buttons {
  margin: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 20px;
}
.wrap {
  margin-top: 10px;
  width: 1024px;
}
.product-cover {
  width: 100%;
  height: auto;
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 20px;
}

.card {
  height: 150px;
  width: calc(33.3333333% - 72px);
  border: 3px solid;
  border-color: black;
  color: white;
  text-align: center;
  line-height: 150px;
}

.gray {
  background-color: gray;
}

.red {
  background-color: red;
}

.pagination-cover {
  margin-top:94px;
}
.pagination {
  display:-webkit-box;
  display:-ms-flexbox;
  display:flex;
  -ms-flex-wrap:wrap;
  flex-wrap:wrap;
  text-decoration: none;
}
.pagination-item:not(:last-child) {
  margin-right:10px 
}
.pagination-item a {
  padding-top:1px;
  font-weight:600;
  font-size:18px;
  line-height:32px;
  width:38px;
  text-align:center;
  border:2px solid #ebedec;
  color:#535b65;
  display:block;
  border-radius:0;
  text-decoration: none;
}
.pagination-item a i {
  vertical-align:1px;
  font-weight:900;
  color:#535b65;
}
.pagination-item.active a {
  border-color:#ffd910;
  background-color:#ffd910;
}
.pagination-item.active{
  pointer-events:none;
}
.pagination-item:hover a {
  border-color:#ffd910;
}

.pagination {
  list-style-type: none;
  justify-content: center;
}

.hide-filter {
  display: none;
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Shop</title>
</head>

<body id="home">
    <div class="wrap">
    <div class="buttons">
          <button class="btn" data-filter="all">Все</button>
          <button class="btn" data-filter="gray">Серые</button>
          <button class="btn" data-filter="red">Красные</button>
    </div>
        <div class="product-cover">
            <div class="card red">Первая</div>
            <div class="card gray">Вторая</div>
            <div class="card gray">Третья</div>
            <div class="card red">Четвертая</div>
            <div class="card red">Пятая</div>
            <div class="card gray">Шестая</div>
            <div class="card red">Седьмая</div>
            <div class="card gray">Восьмая</div>    
            <div class="card gray">Девятая</div>
            <div class="card red">Десятая</div>
        </div>
        <div class="pagination-cover">
            <ul class="pagination">
                <li class="pagination-item item-prev"><a href="#"><i class="fa fa-angle-left" aria-hidden="true"></i>prev</a></li>
                <li class="pagination-item active"><a href="#">1</a></li>
                <li class="pagination-item"><a href="#">2</a></li>
                <li class="pagination-item"><a href="#">3</a></li>
                <li class="pagination-item"><a href="#">4</a></li>
                <li class="pagination-item"><a href="#">5</a></li>
                <li class="pagination-item item-next"><a href="#"><i class="fa fa-angle-right" aria-hidden="true"></i>next</a></li>
            </ul>
        </div>
    </div>
</body>
</html>

  1. Как мне правильно реализовать пагинацию таким образом, чтобы допустим на странице высвечивалось по 4 новые карточки, а остальные были скрыты?

  2. Как при этом еще и не сломать фильтр, чтобы он подтягивал нужные из скрытых, пока не заполнит страницу до 4 карточек?


Ответы (1 шт):

Автор решения: De.Minov

Вам бы для начала понять как работает пагинация.

Если вкратце - есть какой-то массив, который делится на части и пользователь может запросить каждую эту часть.

Обычно такое всегда делается на сервере, даже с фильтром.
Что есть фильтр, он из того массива берёт только нужное и так же отдаёт частями.


Вариант вывести этот массив в HTML, а после его скрывать - такое себе, особенно есть на данные в массиве потребуется вешать обработчики.

Но если же требуется, так, то как вариант:

const lengthPart = 4, // количество элементов в 1 части
      filter = document.querySelector('.buttons'),
      product = document.querySelector('.product-cover'),
      pagination = document.querySelector('.pagination');
      
let data = [...product.children]; // Тут храним все `.card`, типо аналог БД, т.е исходные данные.
let chunks = SplitParts(data); // Тут храним части. Ниже описание функции.

RenderChunks(0); // Рендерим первую часть. Описание функии ниже.
RenderPagination(); // Рендерим пагинацию. Описание тоже ниже.

// Механика работы фильта
filter.addEventListener('click', e => { // При клике в область фильтра
  let btn = e.target.closest('.btn'); // проверяем, нажата ли была кнопка .btn, это называется "делегирование событий".
  if(btn) {
    if(btn.dataset.filter !== 'all') // если не нажимаем на кнопку all
      chunks = SplitParts(data.filter(elem => elem.classList.contains(btn.dataset.filter))); // То из исходного массива получаем только элементы с классом, который равен `data-filter` у кнопки. Потом его разбиваем на части и сохраняем в массив с частями.
    else
      chunks = SplitParts(data); // Если нажат `all`, то разбиваем весь массив на части.
  }
  RenderChunks(0); // После отработки фильтра важно перерендерить части..
  RenderPagination(); // и пагинацию
});

// Механика работы пагинации
pagination.addEventListener('click', e => {
  let item = e.target.closest('.pagination-item'); // Тут тоже делегирование, как в механике выше.
  if(item) {
    let active = pagination.querySelector('.pagination-item.active'), // получим активную страницу
        part; // сюда запишем номер части, для проверок пагинации
    if(item.classList.contains('item-prev') || item.classList.contains('item-next')) { // если нажата кнопка "вперёд" или "назад"
      if(item.classList.contains('disable')) return false; // Если кнопка имеет класс `disable`, то прекращаем выполнение кода ниже
      part = +active.dataset.part; // записываем номер части активной страницы.
      part = item.classList.contains('item-prev') ? part - 1 : part + 1; // Если нажата кнопка "назад", то отнимаем единицу активной старница, если "вперёд", то прибавляем.
          
      RenderChunks(part); // Рендерим страница
      // Меняем в пагинации активную страницу
      active.classList.remove('active'); // Находим активную и удаляем класс `active`
      pagination.querySelector(`.pagination-item[data-part="${part}"]`).classList.add('active'); // находим страницу с `data-part`, который равен активной странице и добавляем ему класс `active`
    } else { // Если нажаты кнопки страницы (1, 2, 3 и т.п.)
      active.classList.remove('active'); // удаляем класс `active` у активной.
      item.classList.add('active'); // добавляем нажатой кнопке класс `active`
      part = +item.dataset.part; // получаем её номер части
      RenderChunks(part); // Рендерим страницу.
    }
    // Тут запрещаем или разрешаем использовать кнопки "вперёд" или "назад", в зависимости от того, какая часть сейчас активна.
    let prev = pagination.querySelector('.pagination-item.item-prev'),
        next = pagination.querySelector('.pagination-item.item-next');
    
    // Сначала удалим у них класс `disable`, если он есть
    if(prev.classList.contains('disable')) prev.classList.remove('disable');
    if(next.classList.contains('disable')) next.classList.remove('disable');
    if(part === 0) prev.classList.add('disable'); // Проверим является ли активная страница началом частей, если да, то запретим использовать кнопку "назад"
    if(part === chunks.length - 1) next.classList.add('disable'); // если активная является концом частей, то запрещаем "вперёд".
  }
});


// Функция которая делит массив на части.
function SplitParts(arr) { // передаём массив, который нужно разбить
  if(arr.length > lengthPart) { // проверяем, имеет ли переданный массив длину больше, чем длина части
    let chunks = [], // подготавливаем возращаемый массив с частями
        parts = Math.floor(arr.length / lengthPart); // сколько частей получится
        
    for(let i = 0; i < arr.length; i+=lengthPart) // проходим по массиву, шаг длине части
      chunks.push(arr.slice(i, i+lengthPart)); // добавляем часть в массив с частями
    
    return chunks; // возвращаем массив
  } else return arr; // если получаемый массив меньше длины части, то возвращаем его же.
}

// Функция для вывода конкретно части в HTML
function RenderChunks(part) { // передаём порядковый номер части
  if(part >= 0 && part < chunks.length) { // если номер части > 0 и < длины частей
    product.innerHTML = ''; // очищаем элемент, куда будем выводить части
    chunks[part].map(elem => product.append(elem)); // Выводим т.к. в исходном массиве уже сразу Element, то мы можем добавить его через .append
  } else return false;
}

// Функия для создания пагинации
function RenderPagination() {
  pagination.innerHTML = ''; // Очищаем блок
  if(chunks.length > 1) { // Если частей больше одной, то выводим погинацию, иначе нет смысла..
    chunks.map((elem, i) => pagination.insertAdjacentHTML('beforeend', `<li class="pagination-item${i === 0 ? ' active' : ''}" data-part="${i}"><a href="#">${i+1}</a></li>`)); // Создаём столько же "ссылок", сколько частей есть
    // Ниже добавляем кнопки "вперёд" и "назад"
    pagination.insertAdjacentHTML('afterbegin', '<li class="pagination-item item-prev disable"><a href="#"><i class="fa fa-angle-left" aria-hidden="true"></i>prev</a></li>'); // Т.к. данная функция создаёт пагинацию у которой первая страница активна, то сразу запрещаем кнопке "назад" работать.
    pagination.insertAdjacentHTML('beforeend', '<li class="pagination-item item-next"><a href="#"><i class="fa fa-angle-right" aria-hidden="true"></i>next</a></li>');
  }
}
body {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.buttons {
  margin: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 20px;
}

.wrap {
  margin-top: 10px;
  width: 1024px;
}

.product-cover {
  width: 100%;
  height: auto;
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 20px;
}

.card {
  height: 150px;
  width: calc(33.3333333% - 72px);
  border: 3px solid;
  border-color: black;
  color: white;
  text-align: center;
  line-height: 150px;
}

.gray {
  background-color: gray;
}

.red {
  background-color: red;
}

.pagination-cover {
  margin-top: 94px;
}

.pagination {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -ms-flex-wrap: wrap;
  flex-wrap: wrap;
  text-decoration: none;
}

.pagination-item:not(:last-child) {
  margin-right: 10px
}

.pagination-item a {
  padding-top: 1px;
  font-weight: 600;
  font-size: 18px;
  line-height: 32px;
  width: 38px;
  text-align: center;
  border: 2px solid #ebedec;
  color: #535b65;
  display: block;
  border-radius: 0;
  text-decoration: none;
}

.pagination-item a i {
  vertical-align: 1px;
  font-weight: 900;
  color: #535b65;
}

.pagination-item.active a {
  border-color: #ffd910;
  background-color: #ffd910;
}

.pagination-item.active {
  pointer-events: none;
}

.pagination-item:hover a {
  border-color: #ffd910;
}

.pagination-item.disable {
  opacity: .65;
  pointer-events: none;
}

.pagination {
  list-style-type: none;
  justify-content: center;
}

.hide-filter {
  display: none;
}
<div class="wrap">
  <div class="buttons">
    <button class="btn" data-filter="all">Все</button>
    <button class="btn" data-filter="gray">Серые</button>
    <button class="btn" data-filter="red">Красные</button>
  </div>
  <div class="product-cover">
    <div class="card red">Первая</div>
    <div class="card gray">Вторая</div>
    <div class="card gray">Третья</div>
    <div class="card red">Четвертая</div>
    <div class="card red">Пятая</div>
    <div class="card gray">Шестая</div>
    <div class="card red">Седьмая</div>
    <div class="card gray">Восьмая</div>
    <div class="card gray">Девятая</div>
    <div class="card red">Десятая</div>
  </div>
  <div class="pagination-cover">
    <ul class="pagination"></ul>
  </div>
</div>

Данный код использует новый стандарт, по этому лучше прогнать его через BabelJS

→ Ссылка