[Vuejs]-Trying to do server side integration of data table pagination in vuejs. But it's not working properly

0👍

I had run into the same issue. I ended up having to build pagination functionality with an ASYNC function.

Everything is Commented.

I declare in this.page.filters.page the current page. It looks at the $route.query for a ‘page’ parameter (www.example.com/location?page=2). If not present, defaults it to 1.

Also declare a this.page.paginator empty object.

When the component is created, it checks to see if the $route.query contains a ‘page’ parameter, and if it doesn’t, it sets it.

We need to compute the current page number and the paginator value. See ‘paginate()’ and ‘pagenum()’ under ‘computed’ properties.

Then we need to watch the ‘pagenum’ value for any changes.

In the ‘getVideosByCategory()’ function, we have to do some checks and add a new level of history to the users browser so that they can use the back button as we switch ‘pages’. We also have to check that the user isn’t trying to go beyond the last page, and then send them back to the last page if they do.

Finally, the ‘async paginator()’ function. First, we check to see if it is less than 5. This number can be changed depending on your layout and how many pages you want to be shown. It then follows the commented logic to display the page numbers and push them into the this.page.paginator object.

Then, in the template, I model a div from ‘paginate’ and use router-link elements to generate links to the pages. The first checks to see if it is on a page greater than 1, and if so, displays an active ‘Previous’ link, else an inactive one. Then it loops through all of the pages in the ‘paginate’ object and creates a link to each page. During this, it checks for the current page number and deactivates the link for the current page.

The SCSS that is used turns the .paginator element into a flex-box element and displays the items in a row with even spacing.

<script>
import TestService from '@/services/TestService';
import _ from 'lodash';

export default {
  name: 'videos',
  data() {
    return {
      videos: [],
      page: {
        data: [],
        ippOptions: {
          4: 4,
          6: 6,
          8: 8,
          10: 10,
          12: 12,
          14: 14,
          18: 18,
        },
        filters: {
          // Check to see if an Items Per Page setting was set in the URL
          // If not, default to 12 items per page
          ipp: (this.$route.query.hasOwnProperty('ipp'))?this.$route.query.ipp:12,
          page: (this.$route.query.hasOwnProperty('page'))?this.$route.query.page:1,
          product: (this.$route.query.hasOwnProperty('product'))?this.$route.query.product:0,
        },
        paginator: [],
      },
    };
  },
  created() {
    // When the component is created, check to see if there is an Items Per Page
    // setting or a page number in the URL, if not, default those $route parameters
    // manually as follows.
    if (!('ipp' in this.$route.query)) {
      // Set the $route Items per Page to the this.page.filters.ipp value
      this.$route.query.ipp = this.page.filters.ipp;
    }
    if (!('page' in this.$route.query)) {
      // Default to page 1, if page is not defined, then it is the first page of results
      this.$route.query.page = 1;
    }
    if (!('product' in this.$route.query)) {
      // Default to page 1, if page is not defined, then it is the first page of results
      this.$route.query.product = 0;
    }
  },
  mounted() {
    // Get the category/name from the end of the path
    const type = this.$route.params.category;
    // Set Video Type Attributes
    const types = {
      products: {
        id: 'products',
        name: 'Corporate Products',
      },
      android: {
        id: 'android',
        name: 'Android',
      },
      ios: {
        id: 'ios',
        name: 'iOS',
      },
      windows: {
        id: 'windows',
        name: 'Windows',
      },
      'cross-device': {
        id: 'cross-device',
        name: 'Cross-Device',
      },
      'processes-and-tools': {
        id: 'Processes and Tools',
        name: 'Processes and Tools',
      },
      all: {
        id: 'all',
        name: 'All Videos',
      },
    };
    // Check if the supplied value is in the allowed values
    if (type in types) {
      // Put the Page information into a DOM accessible array
      this.page.data = types[type];
      // If they need all Videos, call getRecentVideos
      if (type === 'all') {
        this.getRecentVideos(this.page.filters.ipp, this.$route.query.page, this.page.filters.product);
      } else {
        // Gets the videos by category (type) and amount
        this.getVideosByCategory(this.page.data.id, this.page.filters.ipp, this.$route.query.page, this.page.filters.product);
      }
    } else {
      // If it's not a valid category, send an error 404 message
      window.location = 'https://clpstaging.mcafee.com/error';
    }
  },
  watch: {
    // Watch for the Items Per Page selection to change
    ipp(val) {
      // If they need all Videos, call getRecentVideos
      if (this.page.data.id === 'all') {
        this.getRecentVideos(val, this.$route.query.page, this.page.filters.product);
      } else {
        // Reload the component
        this.$route.query.ipp = val;
        this.getVideosByCategory(this.page.data.id, val, this.$route.query.page, this.page.filters.product);
      }
    },
    // Watch for changes to the page number
    pagenum(val) {
      // If they need all Videos, call getRecentVideos
      if (this.page.data.id === 'all') {
        this.getRecentVideos(this.page.filters.ipp, val, this.page.filters.product);
      } else {
        // Reload the component
        this.$route.query.page = val;
        this.getVideosByCategory(this.page.data.id, this.$route.query.ipp, val, this.page.filters.product);
      }
    },
    // Watch for changes to the product filter
    productFilter(val) {
      // If they need all Videos, call getRecentVideos
      if (this.page.data.id === 'all') {
        this.getRecentVideos(this.page.filters.ipp, this.$route.query.page, val);
      } else {
        // Reload the component
        this.$route.query.product = val;
        this.getVideosByCategory(this.page.data.id, this.$route.query.ipp, this.$route.query.page, val);
      }
    }
  },
  computed: {
    // Define the function that chunks the results to build a
    // responsive layout.
    videoChunks() {
      return _.chunk(this.videos, 2);
    },
    // Define the Items-Per-Page value for the Watcher
    // This ensures the data is updated when a user makes a change
    // to Items-Per-Page filter.
    ipp() {
      return this.page.filters.ipp;
    },
    // Define the paginator value
    paginate() {
      return this.page.paginator;
    },
    // Define the pagenum value for the Watcher.
    // This ensures that the component is reloaded
    // when the users makes a selection from the paginator.
    pagenum() {
      return (this.$route.query.page === 'undefined')?1:this.$route.query.page;
    },
    productFilter() {
      return this.page.filters.product;
    },
  },
  methods: {
    // Methods that get called upon mount. See Vue component lifecycle.
    async getVideosByCategory(category, amount, pagenumber, productId) {
      // Build filters
      await this.getProducts();
      // Wait for the API to send back a response. await keyword only works in async functions
      const response = await TestService.fetchVideosByCategory(category, amount, pagenumber, productId);
      // Total number of results
      this.page.data.total = response.data[1][0].total;
      // Set total number of pages based on Items per Page
      this.page.data.pages = Math.ceil(response.data[1][0].total / this.page.filters.ipp);
      // Set the videos object to the first object (the array of videos we want)
      // of the JSON response from the API
      this.videos = response.data[0];
      // Add a new level of history without reloading the page with the udpated filters
      // And set the props to match
      // If the current url in the address bar is different from the query that
      // would generate the data on the page
      if (`${this.$route.path}?ipp=${amount}&page=${pagenumber}&product=${productId}` != this.$route.fullPath) {
        history.pushState(this.page, 'Example Site', `${this.$route.path}?ipp=${amount}&page=${pagenumber}&product=${productId}`);
        this.$route.query.page = pagenumber;
        this.page.filters.page = pagenumber;
        this.$route.query.ipp = amount;
        this.page.filters.ipp = amount;
        this.$route.query.product = productId;
        this.page.filters.product = productId;
      }
      // Add a new level of history without reloading the page with the udpated filters
      // And set the props to match
      // If the selected filters result in no results found
      if (this.page.data.pages === 0) {
        history.pushState(this.page, 'Example Site', `${this.$route.path}?ipp=${amount}&page=1&product=${productId}`);
        this.$route.query.page = 1;
        this.page.filters.page = 1;
        this.$route.query.ipp = amount;
        this.page.filters.ipp = amount;
        this.$route.query.product = productId;
        this.page.filters.product = productId;
      }
      // Check to make sure that the user isn't beyond the last page of results.
      // If they are, redirect them to the last page of results.
      // When updating filters, it's possible for a user to set a filter while on page 10,
      // But the results would only fill 7 pages. The user would be redirected to page 7 with
      // the same filters.
      if (this.page.data.pages < this.$route.query.page && this.page.data.pages != 0) {
        let redir = '';
        for (const key in this.page.filters) {
          redir = `${redir}&${key}=${this.page.filters[key]}`;
        }
        redir = redir.replace(/^&/g, '?').replace(/page=[0-9]*/g, `page=${this.page.data.pages}`);
        window.location = redir;
      }
      // build paginator
      await this.paginator(this.page.data.total, this.page.data.pages, this.$route.query.page);
    },
    async getRecentVideos(amount, pagenumber, productId) {
      // Build filters
      await this.getProducts();
      // Wait for the API to send back a response. await keyword only works in async functions
      const response = await TestService.fetchVideos(amount, pagenumber, productId);
      // Total number of results
      this.page.data.total = response.data[1][0].total;
      // Set total number of pages based on Items per Page
      this.page.data.pages = Math.ceil(response.data[1][0].total / this.page.filters.ipp);
      // Set the videos object to the first object (the array of videos we want)
      // of the JSON response from the API
      this.videos = response.data[0];
      // Add a new level of history without reloading the page with the udpated filters
      // And set the props to match
      // If the current url in the address bar is different from the query that
      // would generate the data on the page
      if (`${this.$route.path}?ipp=${amount}&page=${pagenumber}&product=${productId}` != this.$route.fullPath) {
        history.pushState(this.page, 'Example Site', `${this.$route.path}?ipp=${amount}&page=${pagenumber}&product=${productId}`);
        this.$route.query.page = pagenumber;
        this.page.filters.page = pagenumber;
        this.$route.query.ipp = amount;
        this.page.filters.ipp = amount;
        this.$route.query.product = productId;
        this.page.filters.product = productId;
      }
      // Add a new level of history without reloading the page with the udpated filters
      // And set the props to match
      // If the selected filters result in no results found
      if (this.page.data.pages === 0) {
        history.pushState(this.page, 'Example Site', `${this.$route.path}?ipp=${amount}&page=1&product=${productId}`);
        this.$route.query.page = 1;
        this.page.filters.page = 1;
        this.$route.query.ipp = amount;
        this.page.filters.ipp = amount;
        this.$route.query.product = productId;
        this.page.filters.product = productId;
      }
      // Check to make sure that the user isn't beyond the last page of results.
      // If they are, redirect them to the last page of results.
      // When updating filters, it's possible for a user to set a filter while on page 10,
      // But the results would only fill 7 pages. The user would be redirected to page 7 with
      // the same filters.
      if (this.page.data.pages < this.$route.query.page && this.page.data.pages != 0) {
        let redir = '';
        for (const key in this.page.filters) {
          redir = `${redir}&${key}=${this.page.filters[key]}`;
        }
        redir = redir.replace(/^&/g, '?').replace(/page=[0-9]* /g, `page=${this.page.data.pages}`);
        window.location = redir;
      }
      // build paginator
      await this.paginator(this.page.data.total, this.page.data.pages, this.$route.query.page);
    },
    async getProducts() {
      // Fetch the list of products
      const response = await TestService.fetchProducts();
      // Set the response to a variable
      let out = response.data;
      // Unshift the array and add a default "All" products option to the top
      out.unshift({product_name: 'All',tid: 0,});
      this.page.data.products = out;
    },
    async paginator(total, pages, thisPage) {
      // Find out where to start the Paginator at.
      // Set the start variable
      var start = 0;
      if (Number(thisPage) <= 5) {
        // If the user is looking at page 5 or less, start the count at 1
        start = 1;
      } else {
        // If the user is looking at a page higher than 5, check to see if the
        // user is on a page 5 or less away from the last page.
        if (Number(thisPage)+5 > pages) {
          // If the current page is 5 or less away from the last page,
          // we need to make sure that the navigation stays visually similar
          // by making sure that the same number of pages remain in the paginator.
          // Do this by the following equation where:
          // c = current page Number
          // t = total number of pages
          // c - ( 10 - ( t - c ) )
          start = Number(thisPage)-(10-(pages-Number(thisPage)));
        } else {
          // If the current page is more than 5 away from the last page,
          // just start at 5 before the current page.
          start = Number(thisPage)-5;
        }
      }
      // set the end variable
      var end = 0;
      // Check if the current page is 5 or less away from the last page.
      if (Number(thisPage)+5 > pages) {
        // If the current page number is 5 or less away from the last page,
        // set it to to the number of the last page.
        end = pages;
      } else {
        // If the current page is more than 5 away from the last page,
        // check to see if the current page is 5 or less away from the
        // first page.
        if (Number(thisPage) <= 5) {
          // If the current page is 5 away or closer to the first page,
          // we need to make sure that the navigation stays visually similar
          // by making sure that the same number of pages remain in the paginator.
          // Do this by the following equation where:
          // c = current page Number
          // c + ( 10 - c )
          end = Number(thisPage)+(11-Number(thisPage));
        } else {
          // If the current page is more than 5 away from the first page,
          // end 5 after current page. It's already been checked that
          // the current page is more than 5 away from the last page.
          end = Number(thisPage)+5;
        }
      }
      // Set an array to hold the page numbers
      var pageArr = [];
      // Count the line numbers and push them into the array
      for (var count = start; count <= end; count++) {
        pageArr.push(count);
      }
      // Set the paginator numbers into the DOM
      this.page.paginator = pageArr;
    },
  },
};
</script>
<style lang="scss">
@import '../assets/scss/_colors.scss';

.paginator {
  div {
    list-style-type:none;
    padding:0rem;
    display:flex;
    flex-flow:nowrap;
    justify-content:space-around;
    .router-link-active {
      display:inline-block;
      width:100%;
      color:$darkGray;
      padding: 0.5rem 0rem;
      border-top:solid $lightGray 0.2rem;
      font-size: 1.6rem;
      font-weight:bold;
      text-decoration:none;
      &.disabled {
        color: $lightGray;
        &:hover {
          border-top: solid $lightGray 0.2rem;
          cursor:default;
        }
      }
      &.router-link-exact-active {
        border-top:solid $mcafeeRed 0.2rem;
      }
      &:hover {
        border-top:solid $mcafeeRed 0.2rem;
      }
    }
  }
}
</style>
<template>
  <b-container>
    <b-row class="w-100">
      <b-col md="12" lg="12" xl="12" class="paginator">
        <div v-model="paginate">
          <router-link
            v-if="$route.query.page > 1"
            :to="{ name: $route.name, query: { ipp: page.filters.ipp, page: Number($route.query.page) - 1, product: page.filters.product} }"
            class="prev">&lt;&nbsp;&nbsp;&nbsp;Previous</router-link>
          <div
            v-else
            class="prev disabled router-link-active">&lt;&nbsp;&nbsp;&nbsp;Previous</div>
          <router-link v-for="pages in paginate"
            v-bind:key = "pages"
            :to="{ name: $route.name, query: { ipp: page.filters.ipp, page: pages, product: page.filters.product} }">{{ pages }}</router-link>
          <router-link
            v-if="page.data.pages > $route.query.page"
            :to="{ name: $route.name, query: { ipp: page.filters.ipp, page: Number($route.query.page) + 1, product: page.filters.product} }"
            class="next">Next&nbsp;&nbsp;&nbsp;&gt;</router-link>
          <div
            v-else
            class="next disabled router-link-active">Next&nbsp;&nbsp;&nbsp;&gt;</div>
        </div>
      </b-col>
    </b-row>
  </b-container>
</template>

Leave a comment