Moduł10 - Zajęcia 20 - Paginacja

Paginacja

Baza danych może przechowywać kolekcje setek milionów rekordów. Dlatego zwracanie całej kolekcji dla każdego żądania GET wymaga zbyt dużych zasobów. Rozmiar treści odpowiedzi będzie zbyt duży, a czas żądania będzie ciągnął się o kilkadziesiąt sekund, a nawet minut - im więcej danych w odpowiedzi z backendu, tym dłużej zajmuje ona sieć.

Ponadto musisz pomyśleć o użytkownikach - nie potrzebują wszystkich milionów rekordów naraz. Przetworzenie tak dużej ilości danych w odpowiedzi i renderowanie interfejsu będzie wymagało ogromnych zasobów urządzenia, na którym przeglądana jest strona internetowa. Według statystyk użytkownicy znajdują interesujące ich informacje na pierwszych kilku ekranach.

Załóżmy, że nasz backend my-api.com przechowuje bardzo dużą kolekcję postów w zasobie /posts, którą reprezentujemy na ilustracji za pomocą dwunastu elementów.

Dla każdego żądania GET backend będzie musiał zwrócić całą kolekcję, a my zmierzymy się z opisanymi wcześniej problemami. Aby je rozwiązać, istnieje paginacja - technika, w której nie cała kolekcja jest zwracana do pierwszego i każdego kolejnego żądania GET, ale pewna jej część. Paginacja jest zaimplementowana na backendzie i używana na frontendzie za pomocą specjalnych parametrów żądania.

Liczba elementów odpowiedzi

Pierwszy parametr określa liczbę elementów w odpowiedzi z backendu. Niech w naszym przypadku nazywa się to per_page. Nie ma standardu nazewnictwa parametrów paginacji, więc ich nazwy zależą od programisty Backend.

W takim żądaniu GET backend nie zwróci całej kolekcji z dwunastu elementów, ale tylko pierwsze cztery. Jeśli przekażesz wartość ujemną lub wartość większą niż liczba elementów w kolekcji, backend może zareagować inaczej - zignoruje je lub zwróci błąd 400 (BAD REQUEST), w zależności od jego implementacji.

Publiczny JSONPlaceholder API również obsługuje paginację - liczba elementów w odpowiedzi jest kontrolowana przez parametr _limit. Łącznie w kolekcji /posts znajduje się 100 pozycji. Zmień wartość parametru _limit w przykładzie i sprawdź odpowiedź backendu w interfejsie oraz w karcie Network.

Przykład -------------------------------

const fetchPostsBtn_s1a2 = document.querySelector('button#btn-s1a2');
const postList_s1a2 = document.querySelector('ul#user-list-s1a2');
let inputValue = 0;
fetchPostsBtn_s1a2.addEventListener('click', async () => {
  const input_s1a2 = document.querySelector('input#input-s1a2');
  inputValue = input_s1a2.value;
  try {
    fetchPostsBtn_s1a2.setAttribute('disabled', 'disabled');
    const posts = await fetchPosts_s1a2();
    renderPosts_s1a2(posts);
  } catch (error) {
    Notify.failure(`${error}`, optionsNotify);
  } finally {
    fetchPostsBtn_s1a2.removeAttribute('disabled');
    console.log(`inputValue ${inputValue}`);
  }
});

async function fetchPosts_s1a2() {
  const searchParams_s1a2 = new URLSearchParams({
    _limit: inputValue,
  });
  const url_s1a2 = `https://jsonplaceholder.typicode.com/posts?${searchParams_s1a2}`;
  // Change the number of items in the group here
  const response = await axios.get(url_s1a2);
  console.log(`adres ${url_s1a2}`);
  return response.data;
}

function renderPosts_s1a2(posts) {
  postList_s1a2.innerHTML = null;
  const markup = posts
    .map(({ id, title, body, userId }, index) => {
      return `<li>
          <h2 class="post-title">${index + 1}. ${title.slice(0, 30)}</h2>
          <p><b>Post id</b>: ${id}</p>
          <p><b>Author id</b>: ${userId}</p>
          <p>${body}</p>
        </li>`;
    })
    .join('');
  postList_s1a2.innerHTML = markup;
}
            

    PrzykładEND -------------------------------

    Numer grupy elementów

    Określając w odpowiedzi żądaną liczbę elementów, zawsze otrzymamy ten sam wynik - pierwsze per_page elementów kolekcji, tzw. pierwsza grupa lub „strona". Drugi parametr paginacji steruje przesunięciem w obrębie kolekcji - numer grupy elementów, którą chcemy uzyskać. Jeśli backend implementuje paginację, to domyślną wartością tego parametru jest jeden - pierwsza grupa lub „strona" elementów. Niech w naszym przypadku będzie się nazywać page.

    Zmieniając wartość parametru page, określamy backendowi, jaką kolejną grupę elementów chcemy otrzymać i tak dalej, aż w kolekcji zabraknie elementów. Jeśli ustawisz wartość ujemną lub więcej niż grup w kolekcji, odpowiedź backendu będzie zależeć od jego implementacji.

    W JSONPlaceholder API parametr kontrolujący grupę elementów nazywa się _page. Zmień jego wartość w przykładzie i sprawdź odpowiedź backendu w interfejsie oraz w karcie Network.

    Przykład -------------------------------

    const fetchPostsBtn_s1a3 = document.querySelector('button#btn-s1a3');
    const postList_s1a3 = document.querySelector('ul#user-list-s1a3');
    let inputValue_s1a3 = 0;
    let inputPageValue_s1a3 = 0;
    fetchPostsBtn_s1a3.addEventListener('click', async () => {
      const input_s1a3 = document.querySelector('input#input-s1a3');
      const inputPage_s1a3 = document.querySelector('input#input-page-s1a3');
      inputValue_s1a3 = input_s1a3.value;
      inputPageValue_s1a3 = inputPage_s1a3.value;
      try {
        fetchPostsBtn_s1a3.setAttribute('disabled', 'disabled');
        const posts = await fetchPosts_s1a3();
        renderPosts_s1a3(posts);
      } catch (error) {
        Notify.failure(`${error}`, optionsNotify);
      } finally {
        fetchPostsBtn_s1a3.removeAttribute('disabled');
        console.log(`limit Value ${inputValue_s1a3}`);
        console.log(`page Value ${inputPageValue_s1a3}`);
      }
    });
    
    async function fetchPosts_s1a3() {
      const searchParams_s1a3 = new URLSearchParams({
        _limit: inputValue_s1a3,
        _page: inputPageValue_s1a3,
      });
      const url_s1a3 = `https://jsonplaceholder.typicode.com/posts?${searchParams_s1a3}`;
      // Change the number of items in the group here
      const response = await axios.get(url_s1a3);
      console.log(`adres ${url_s1a3}`);
      return response.data;
    }
    
    function renderPosts_s1a3(posts) {
      postList_s1a3.innerHTML = null;
      const markup = posts
        .map(({ id, title, body, userId }, index) => {
          return `&lli>
              <h2 class="post-title">${index + 1}. ${title.slice(0, 30)}</h2>
              <p>&lb>Post id</b>: ${id}</p>
              <p><b>Author id</b>: ${userId}</p>
              <p>${body}</p>
            </li>`;
        })
        .join('');
      postList_s1a3.innerHTML = markup;
    }
                

      PrzykładEND -------------------------------

      Aby wiedzieć, kiedy w kolekcji kończą się elementy i wyświetlić użytkownikowi komunikat o tym, backend w każdej odpowiedzi zwraca nie tylko tablicę elementów, ale także metadane o dostępnej liczbie grup („stron"), w zależności od wartości parametru per_page lub po prostu o ogólnej liczbie elementów w kolekcji, wtedy obliczenie liczby grup spada na barki programisty front-end. Niestety JSONPlaceholder API nie implementuje tego funkcjonału.

      Technika „Załaduj więcej"

      Aby dynamicznie zmieniać numer grupy dla każdego kolejnego żądania, wystarczy zadeklarować jeszcze jedną zmienną globalną, nazwiemy ją page i ustawimy wartość początkową na 1 - pierwsza grupa elementów. Po każdym udanym żądaniu, w wywołaniu zwrotnym metody then() zwiększymy wartość page o jeden. Tworząc parametry żądania, posługujemy się jego wartością.

      Przykład -------------------------------

      const fetchPostsBtn_s1a4 = document.querySelector('button#btn-s1a4');
      const postList_s1a4 = document.querySelector('ul#user-list-s1a4');
      
      // Controls the group number
      let page_s1a4 = 1;
      // Controls the number of items in the group
      let perPage_s1a4 = 10;
      
      fetchPostsBtn_s1a4.addEventListener('click', async () => {
        try {
          const posts = await fetchPosts_s1a4();
          renderPosts_s1a4(posts);
          // Increase the group number
          page_s1a4 += 1;
      
          // Replace button text after first request
          if (page_s1a4 > 1) {
            fetchPostsBtn_s1a4.textContent = 'Fetch more posts';
          }
        } catch (error) {
          Notify.failure(`${error}`, optionsNotify);
        }
      });
      
      async function fetchPosts_s1a4() {
        const params = new URLSearchParams({
          _limit: perPage_s1a4,
          _page: page_s1a4,
        });
      
        const response = await axios.get(
          `https://jsonplaceholder.typicode.com/posts?${params}`
        );
        return response.data;
      }
      
      function renderPosts_s1a4(posts) {
        const markup = posts
          .map(({ id, title, body, userId }, index) => {
            return `<li>
                <h2 class="post-title">${index}. ${title.slice(0, 30)}</h2>
                <p><b>Post id</b>: ${id}</p>
                <p><b>Author id</b>: ${userId}</p>
                <p class="post-body">${body}</p>
              </li>`;
          })
          .join('');
        postList_s1a4.insertAdjacentHTML('beforeend', markup);
      }
      
                  

        PrzykładEND -------------------------------

        Po wczytaniu pierwszej grupy elementów tekst na przycisku zmieni się, a sam przycisk spadnie pod listę postów. Gdy użytkownik przewinie stronę i ponownie ją kliknie, zostanie wykonane żądanie dla drugiej grupy elementów, która zostanie dodana do już istniejących znaczników listy postów. Jeśli po kliknięciu przycisku „Fetch posts" nie ma więcej postów do pobrania, wyświetlamy alert.

        Dodaliśmy sprawdzanie końca kolekcji we frontendzie, ponieważ JSONPlaceholder API nie implementuje tego funkcjonału w backendzie. W naszym przypadku wystarczy podzielić łączną ilość elementów w kolekcji przez ilość elementów w jednej grupie. Jest to podobne do przypadku, gdy backend nie zwraca liczby dostępnych stron, ale całkowitą liczbę elementów w kolekcji.