DEV Community

Cover image for How to Fetch API and Implement Filtering, Sorting, and Pagination in Vue.js
Rifky Alfarez
Rifky Alfarez

Posted on

How to Fetch API and Implement Filtering, Sorting, and Pagination in Vue.js

In this article, I want to share how you can fetch data from an API and implement useful features like filtering, sorting, and pagination in a Vue.js application. These features are especially handy when working on projects like product listings or content libraries where users need an easy way to navigate through data. I'll walk you through the process step by step, from fetching the data to adding interactive controls, so you can apply these techniques in your own projects. Let’s dive in!

Fetch Data

<template>
  <main class="min-h-screen bg-neutral-200 py-4">
    <section class="w-[1280px] mx-auto">
      <ul class="mt-1">
        <li v-for="product in products" :key="product.id">
          {{ product.id }}. {{ product.title }} - ${{ product.price }} -
          {{ product.category }}
        </li>
      </ul>
      <p v-if="!loading && products.length === 0">Nothing to see here</p>
      <p v-if="loading">Loading ...</p>
      <p v-if="error">{{ error }}</p>
    </section>
  </main>
</template>

<script setup lang="ts">
import { onMounted, ref } from 'vue';

interface Product {
  id: number;
  title: string;
  price: number;
  description: string;
  category: string;
  image: string;
  rating: {
    rate: number;
    count: number;
  };
}

const BASE_URL = 'https://fakestoreapi.com/products';

const products = ref<Product[]>([]);
const loading = ref<boolean>(false);
const error = ref<string | null>(null);

const fetchProducts = async () => {
  loading.value = true;
  try {
    const response = await fetch(BASE_URL);
    if (!response.ok) {
      throw new Error(`HTTP error! status ${response.status}`);
    }
    products.value = await response.json();
  } catch (err) {
    error.value = err instanceof Error ? err.message : 'Unknown error occurred';
  } finally {
    loading.value = false;
  }
};

onMounted(() => fetchProducts());
</script>
Enter fullscreen mode Exit fullscreen mode

onMounted:
In Vue.js, onMounted is one of the lifecycle hooks that is executed after a component has been mounted to the DOM. In simpler terms, the code inside onMounted runs once the component is fully ready and displayed on the screen.

In the example above, onMounted is used to call the fetchProducts function when the component loads. This function retrieves product data from an external API and stores it in the products variable. This ensures that the data is available when users view the page. It's especially useful for initializing data, such as fetching a list of products, articles, or any dynamic information required for the page.

v-for:
v-for is a directive in Vue.js used to dynamically render elements based on data from an array or object. In the example above, v-for is used to automatically generate a list of products based on the data fetched from the API.

Implement Filtering, Sorting, and Pagination

<template>
  <main class="min-h-screen bg-neutral-200 py-4">
    <section class="w-[1280px] mx-auto">
      <div class="flex gap-x-4 items-center">
        <input v-model="searchTerm" type="search" placeholder="search..." />
        <select v-model="category">
          <option value="" selected>Category:</option>
          <option value="men's clothing">Men's Clothing</option>
          <option value="women's clothing">Women's Clothing</option>
          <option value="jewelery">Jewelery</option>
          <option value="electronics">Electronics</option>
        </select>
        <select v-model="sortBy">
          <option value="" selected>Sort By:</option>
          <option value="title">Title</option>
          <option value="price">Price</option>
        </select>
        <select v-model="orderBy">
          <option value="" selected>Order by:</option>
          <option value="asc">Ascending</option>
          <option value="desc">Descending</option>
        </select>
        <select v-model="limit">
          <option value="" selected>Limit:</option>
          <option v-for="index in 20" :value="index">{{ index }}</option>
        </select>
      </div>
      <div class="flex gap-x-4 items-center mt-4">
        <button
          :disabled="page === 1"
          @click="page > 1 && page--"
          class="disabled:cursor-not-allowed"
        >
          Prev
        </button>
        <span>Page {{ page }} of {{ totalPage }}</span>
        <button
          :disabled="page === totalPage"
          @click="page < totalPage && page++"
          class="disabled:cursor-not-allowed"
        >
          Next
        </button>
      </div>
      <ul class="mt-1">
        <li v-for="product in limitedProducts" :key="product.id">
          {{ product.id }}. {{ product.title }} - ${{ product.price }} -
          {{ product.category }}
        </li>
      </ul>
      <p v-if="!loading && searchedProducts.length === 0">
        Nothing to see here
      </p>
      <p v-if="loading">Loading ...</p>
      <p v-if="error">{{ error }}</p>
    </section>
  </main>
</template>

<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';

interface Product {
  id: number;
  title: string;
  price: number;
  description: string;
  category: string;
  image: string;
  rating: {
    rate: number;
    count: number;
  };
}

const BASE_URL = 'https://fakestoreapi.com/products';

const products = ref<Product[]>([]);
const loading = ref<boolean>(false);
const error = ref<string | null>(null);
const orderBy = ref<string>('');
const category = ref<string>('');
const sortBy = ref<string>('');
const searchTerm = ref<string>('');
const limit = ref<string>('');
const page = ref<number>(1);

const totalPage = computed(() => {
  const itemsPerPage = limit.value ? Number(limit.value) : 10;
  return Math.ceil(searchedProducts.value.length / itemsPerPage);
});

const fetchProducts = async () => {
  loading.value = true;
  try {
    const response = await fetch(BASE_URL);
    if (!response.ok) {
      throw new Error(`HTTP error! status ${response.status}`);
    }
    products.value = await response.json();
  } catch (err) {
    error.value = err instanceof Error ? err.message : 'Unknown error occurred';
  } finally {
    loading.value = false;
  }
};

const orderByProducts = computed(() => {
  const sortedProducts = [...products.value];

  if (sortBy.value === 'title') {
    return orderBy.value === 'desc'
      ? sortedProducts.sort((a, b) => b.title.localeCompare(a.title))
      : sortedProducts.sort((a, b) => a.title.localeCompare(b.title));
  } else if (sortBy.value === 'price') {
    return orderBy.value === 'desc'
      ? sortedProducts.sort((a, b) => b.price - a.price)
      : sortedProducts.sort((a, b) => a.price - b.price);
  } else if (orderBy.value === 'desc') {
    return sortedProducts.sort((a, b) => b.id - a.id);
  }

  return sortedProducts;
});

const filteredProducts = computed(() => {
  if (category.value) {
    return orderByProducts.value.filter(
      (product) => product.category === category.value
    );
  }

  return orderByProducts.value;
});

const searchedProducts = computed(() => {
  if (searchTerm.value) {
    return filteredProducts.value.filter((product) =>
      product.title
        .toLocaleLowerCase()
        .includes(searchTerm.value.toLocaleLowerCase())
    );
  }

  return filteredProducts.value;
});

const limitedProducts = computed(() => {
  const itemsPerPage = limit.value ? Number(limit.value) : 10;
  return searchedProducts.value.slice(
    (page.value - 1) * itemsPerPage,
    page.value * itemsPerPage
  );
});

onMounted(() => fetchProducts());
</script>
Enter fullscreen mode Exit fullscreen mode

v-model:
The v-model directive in Vue enables two-way data binding between form inputs and their associated reactive variables. This means that any change in the input field immediately updates the linked variable, and changes to the variable programmatically reflect in the input field. For instance, in this implementation, v-model is used with the searchTerm, category, sortBy, orderBy, and limit inputs, ensuring that the user’s selections or entries dynamically update the application state.

computed:
The computed properties are used to perform reactive calculations based on the state of the application. They allow for efficient updates as they are only re-evaluated when their dependencies change. In this implementation, computed properties like orderByProducts, filteredProducts, searchedProducts, and limitedProducts enable seamless filtering, sorting, and pagination of the product list. Each computed property builds on the result of the previous one, ensuring that all operations are applied consistently and dynamically as the state changes.

Filtering:

const filteredProducts = computed(() => {
  if (category.value) {
    return orderByProducts.value.filter(
      (product) => product.category === category.value
    );
  }
  return orderByProducts.value;
});
Enter fullscreen mode Exit fullscreen mode

The filtering process checks if a category is selected. If it is, only products belonging to that category are included.

Sorting:

const orderByProducts = computed(() => {
  const sortedProducts = [...products.value];
  if (sortBy.value === 'title') {
    return orderBy.value === 'desc'
      ? sortedProducts.sort((a, b) => b.title.localeCompare(a.title))
      : sortedProducts.sort((a, b) => a.title.localeCompare(b.title));
  } else if (sortBy.value === 'price') {
    return orderBy.value === 'desc'
      ? sortedProducts.sort((a, b) => b.price - a.price)
      : sortedProducts.sort((a, b) => a.price - b.price);
  }
  return sortedProducts;
});
Enter fullscreen mode Exit fullscreen mode

The sorting logic uses JavaScript's sort method:

  • For title, it uses localeCompare to handle string comparison.
  • For price, it performs numerical sorting.

Pagination:

const limitedProducts = computed(() => {
  const itemsPerPage = limit.value ? Number(limit.value) : 10;
  return searchedProducts.value.slice(
    (page.value - 1) * itemsPerPage,
    page.value * itemsPerPage
  );
});
Enter fullscreen mode Exit fullscreen mode

The slice method determines which products are shown based on the current page and selected limit. Example: If limit = 5 and page = 2, it displays products with indices 5–9.

Conclusion

Thank you for taking the time to read this article. I hope the explanation provided a clear understanding of how filtering, sorting, and pagination can be implemented effectively in Vue.js. By combining the power of v-model for data binding and computed properties for reactive updates, this approach ensures an efficient and dynamic user experience.

If you have any suggestions, questions, or feedback, feel free to share them! I’m always open to discussions and happy to help.
GitHub: https://github.com/rfkyalf/vue-api-handling.git

I’m always excited to connect and collaborate on web development projects. You can learn more about my work and past projects by visiting my portfolio website: https://www.rifkyalfarez.my.id.

Top comments (0)