loading...
Cover image for Vue composable - array pagination

Vue composable - array pagination

dasdaniel profile image Daniel Poda 🇨🇦 ・3 min read

Vue composable - array pagination

This is an example of a creating a pagination class for of the Vue 3 composition API.

The benefit of the composition API is that it allows to encapsulate the logic in a reusable self-enclosed non-single-file-component. This makes it easy to reuse and, compared to a mixin, is more predictable and maintainable. Because Vue 3 is not released yet, this write-up will be presented using a global bundle which will allow easier showcasing in a jsfiddle. While I have not tried it, this should be possible to apply to a Vue 2 project with the composition-api plugin with few changes.

Composable function

const { computed, ref, isRef } = Vue;

/**
 *
 * @param {Object} options
 * @param {ref|number} options.pageSize
 * @param {ref|number} options.currentIndex 0 based index
 * @param {ref|array} options.array
 */
const pagin8r = options => {
  // setup props
  const pageSize = isRef(options.pageSize)
    ? options.pageSize
    : ref(options.pageSize);
  const currentIndex = isRef(options.currentIndex)
    ? options.currentIndex
    : ref(options.currentIndex);
  const array = isRef(options.array) ? options.array : ref(options.array);

  // create computed
  const total = computed(() => array.value.length);
  const currentPageNum = computed(() => currentIndex.value + 1);
  const numPages = computed(() => Math.ceil(total.value / pageSize.value));
  const range = computed(() =>
    array.value.slice(
      currentIndex.value * pageSize.value,
      (currentIndex.value + 1) * pageSize.value
    )
  );

  // create methods
  const gotoNext = () => {
    currentIndex.value = Math.min(currentIndex.value + 1, numPages.value - 1);
  };
  const gotoPrev = () => {
    currentIndex.value = Math.max(0, currentIndex.value - 1);
  };
  const gotoFirst = () => {
    currentIndex.value = 0;
  };
  const gotoLast = () => {
    currentIndex.value = numPages.value - 1;
  };

  // return props, computed, and method variables
  return {
    array,
    pageSize,
    currentIndex,
    currentPageNum,
    total,
    numPages,
    range,
    gotoNext,
    gotoPrev,
    gotoFirst,
    gotoLast
  };
};

Few things to note.

The function has been laid out to mimic a typical non-composable, grouping props, computed, and methods. All of these have been passed to the returning object, to be available for use.

The pageSize, currentIndex, and array props check if they are being passed a ref or a value using isRef. This allows the function to accept ref and non-ref values. This makes it easier to compose functionality if you have multiple composables. Having this functionality in the Composition API makes it well suited to build a library of functional bits that can be assembled together in various configurations. For example having a simple scroll listening composable, you could calculate the page index from the scroll position and pass it to the pagination function. The Vue component itself would only worry about tying the composables together and rendering the outcome.

In the example fiddle, I use a ref for pageSize, to allow editing it in the template through a v-model. The other params are not used after creation, so a ref is not needed (but possible).

let array = [...Array(50)].map((x, n) => n);
let pageSize = ref(4);
let currentIndex = 0;

It's not complete. Edge cases have not been considered thoroughly. For example changing the page size when you're on the last page does not update the pageIndex automatically. (i.e. if you can end up on page 20 of a 10 page book.) I would probably implement through currentPageNum though

const currentPageNum = computed(() => {
  if (currentIndex.value >= numPages.value) {currentIndex.value = numPages.value - 1}
  return currentIndex.value + 1
});

I'm hoping to keep adding more examples of composables over the upcoming weeks/months.

TL;DR;

Full example: https://jsfiddle.net/dapo/kmpj38od/

photo credit: https://unsplash.com/@moonshadowpress

Discussion

markdown guide
 

Hello,
how is this example more maintainable and/or predictable than version written using mixin?

 

Ahoj,

In short, the benefits are that with the composition API, you can use multiple functionalities that use same variable names, if there's a collision, you can rename or keep it as part of an object. If you use two or more mixins, you could end up with collisions (like same data variable names intended for internal use being used by both). It is possible to setup multiple mixins, but you also have to setup merge strategies in some cases to prevent unintended interactions (think multiple lifecycle hooks, like each mixin using a created).

In addition the mixins don't do well with our IDE tools. If you use a javascript or typescript class to define a composeable, you can have type definitions for your inputs and outputs. If you're using a mixin, there isn't a way (AFAIK) to have the data, methods, etc. be available for the IDE's intellisense.

I think they also offer better testability. The functions are pure javascript and don't rely on Vue rendering, so you can test without special tools.

I think the arguments for composition over inheritance apply here too.

I will try to do a more thorough writeup of the benefits.

 

Thank you for your reply. I appreciate that you didn't just throw in the argument about composing by option vs logic parts.

I agree with the collisions, merge strategies issues and simpler testability, but I'm not so sure about the IDE support, at least when it comes to TypeScript and class-based components. When extending class with Mixins(MyMixin), IDE recognizes the content of the MyMixin (don't know if it works with JavaScript).

I'm not a big fan of mixins myself, I prefer composing code by logic using components. My main issue with the composition API is that I haven't seen the use case yet, where it would make a big difference using composition API vs current features (components, mixins etc.)