Skip to content

Use paginated resources in your Vue/Vuex app with ease

Notifications You must be signed in to change notification settings

cyon/vuex-pagination

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vuex-pagination

JavaScript Style Guide build status

Note: This library only works with Vue 2. For Vue 3, take a look at vue-use-pagination.

Library that makes it magnitudes easier to integrate paginated resources from an API (REST, GraphQL, or anything else) into your Vue application.

How does it work?

Resources in vuex-pagination directly map to their corresponding resources in the API. The resource itself "lives" in the Vuex store and manages the already fetched data.

To initialize a new resource you have to provide a fetchPage function. This function receives some parameters like the current page and the page size and fetches the data from the API.

To display data in your Vue components, you can initialize instances. There can be multiple instances for every resource and all of them can have a different state (current page, page size etc).

Visualisation

Because of this approach, vuex-pagination is suited regardless of the used API. It can also work with data read directly from the file system, for example in Electron applications.

Installation

// Using npm:
npm install vuex-pagination

// Or using Yarn:
yarn add vuex-pagination

If you don't have already, make sure to also install the vuex package.

Usage

To use it in your application you'll need to install the package as a Vue plugin. This can be done as follows:

import Vue from 'vue'
import Vuex from 'vuex'
import { PaginationPlugin } from 'vuex-pagination'

Vue.use(Vuex)
Vue.use(PaginationPlugin)

Warning: Vuex is needed for the plugin to work. It needs to be initialized even if you don't use it directly yourself.


createResource(name, fetchPage, opts)

Parameter Type Required / Default Value Description
name string required Name of the resource
fetchPage function required Function to fetch a single page
opts object null Optional arguments
opts.prefetch boolean false Automatically prefetch items
opts.cacheResources number 20 How many resources should be cached

The fetchPage function receives a single opts parameter that looks like the following:

Parameter Type Description
opts.page number Page to fetch
opts.pageSize number Selected page size
opts.args object Optional arguments that can be passed from the instance

The fetchPage function has to return a Promise which fulfills after the page was fetched and should return an object:

{
  total: 33, // how many items are there in total?
  data: items // array of the items
}
Show example

The createResource is usually done in your store logic. If you didn't use Vuex directly it can also be where you're initializing your Vue application.

Here is an example of how a resource could be initialized:

import { createResource } from 'vuex-pagination'

async function fetchUsers (opts) {
let users = window.fetch(`/users?page=${opts.page}&limit=${opts.pageSize}`)
  .then((response) => response.json())
  .catch((error) => {
      this.dispatch('SHOW_GLOBAL_ERROR_MESSAGE', error.response.data.message);
      return {
          total: 0,
          data: [],
      };
  });

return {
  total: users.total,
  data: users.items
}
}

createResource('users', fetchUsers, {
prefetch: true
})

createInstance(name, opts)

Parameter Type Required / Default Value Description
name string required Name of the resource
opts object required Options
opts.page number 1 Initial page
opts.pageFrom number null Range mode: start page
opts.pageTo number null Range mode: last page
opts.pageSize number 20 Initial page size
opts.args function undefined Function that returns an object of additional arguments

For every resource there can be one or many instances. That means that this data can be shown on different parts of your application and we'll still just download everything once.

An instance can be created like this:

import { createInstance } from 'vuex-pagination'

// this is our Vue component
export default {
  computed: {
    licenses: createInstance('users', {
      page: 1,
      pageSize: 10
    })
  }
}

The instance has the following properties:

Property Type Description Reactive
page number Current page true
pageSize number Selected page size true
items array Items for the current page false
total number Total amount of items false
totalPages number Total number of pages (based on current page size) false
loading boolean Whether data is being fetched currently false

The items property is your data which can be displayed then.

See example
<template>
<li v-for="user in users.items" :key="user.id" class="user">
  <span class="name">{{user.name}}</span>
</li>
</template>

The page and pageSize attributes are reactive, it means you can use them as v-model in your component - or just set them programmatically.

See example
<select v-model="users.pageSize">
<option :value="5">5</option>
<option :value="10">10</option>
<option :value="20">20</option>
</select>

Standard vs range mode

In the standard mode you always get a single page while in the range mode you'll see a list of pages at once. This can be especially useful when you're implementing an endless scrolling feature.

But be aware that the results are still paged in the background using the pageSize you specified. So if you create a new instance, set pageFrom and pageTo both to 1, then set the pageTo to 3, the fetchPage function will be called twice. Once for the second and once for the third page.

See example
<template>
<div>
  <ul>
    <li v-for="user in users.items" :key="user.id">{{ user.name }}</li>
  </ul>
  <a @click.prevent="users.pageTo += 1">Show more...</a>
</div>
</template>
<script>
import { createInstance } from 'vuex-pagination'

export default {
computed: {
  users: createInstance('users', {
    pageFrom: 1,
    pageTo: 1,
    pageSize: 30
  })
}
}
</script>

Passing arguments

Often you'll have to pass different kinds of arguments or parameters to a resource. Think filters, query parameters or different headers, just to name a few.

This can be done with the args function inside the createInstance call:

import { createInstance } from 'vuex-pagination'

export default {
  data () {
    return {
      query: ''
    }
  },
  computed: {
    licenses: createInstance('users', {
      page: 1,
      pageSize: 10,
      args () {
        return {
          query: this.query
        }
      }
    })
  }
}

Every time the return value of the args function changes, the results are being fetched again. If your arguments are not ready yet (maybe they get passed as a prop), the function should just return null and then the fetchPage function will not be called yet.


Pre-fetching and caching

Internally, fetched items are being saved in specific registries - one for every combination of arguments. Registries themselves have no concept of different pages, they are just one big list. Only the instances with their own parameters (page and pageSize) break those down into smaller chunks.

This means that data, that was fetched once, does not have to be downloaded again - it's already in the registry. Using prefetch you can also ensure that the data for the next page is already present:

import { createResource } from 'vuex-pagination'

const controller = createResource('users', fetchLicensesPage, { prefetch: true })

If you have an instance where your user navigates to the second page, we'll already calculate the third page and download all the items so the transition in the pagination is seamless.

Due to the fact that we create a new registry for every combination of arguments, there can also be a large amount of unused registries which then use up memory. Those are good to keep around, as it can happen that we'll use the same combination again and then we already have the results around but often, those are not needed anymore and we garbage collect those automatically. The amount of unused registries is 20 per default but you can set this value yourself like this:

import { createResource } from 'vuex-pagination'

const controller = createResource('users', fetchUsersPage, { cacheResources: 30 })

Usage in Vuex store

The return value of createResource is a controller that allows you to trigger certain actions on the resource itself from store actions. You can also retrieve a controller to a defined resource using the resource function:

import { resource, createResource } from 'vuex-pagination'

const resource = createResource('users', fetchUsersPage)
// ...or:
const resource = resource('users')

To re-fetch a specific resource (for example to poll changes) you can use the controller's refresh function.

If you want to work with paginated items inside your store you might want to use the fetchRange function:

import { resource } from 'vuex-pagination'

resource('users').fetchRange({
  pageFrom: 1,
  pageTo: 5,
  pageSize: 10
}).then((items) => {
  console.log(items)
})

Please note: In previous versions the resource function was called controller. This is deprecated but will continue to work in 1.x versions of this package.

Examples

We prepared a few examples for different use-cases. These can be found in the examples/ folder.

If you cloned this repository, you can run the examples by navigating to the respective folder and running npm run serve.

Testing

The library does some "magic" in the background to make everything as reactive and easy to use as it is. Therefore you need to keep a few things in mind when testing components that use vuex-pagination:

Internally, every instance in a component needs to be registered with the central Vuex store. Therefore you need to wait for the $vuexPaginationInitialized Promise to be resolved. After the that, the data from your instance is available. In a test with vue-test-utils this could look like the following:

import { createLocalVue, mount } from '@vue/test-utils'
import { PaginationPlugin } from 'vuex-pagination'
import Vuex from 'vuex'

const localVue = createLocalVue()
localVue.use(PaginationPlugin)
localVue.use(Vuex)

const store = new Vuex.Store({})

const wrapper = mount(Component, {
  localVue,
  store
})

await wrapper.vm.$vuexPaginationInitialized

// do your testing here

If you do multiple tests, you'll need to reset the vuex-pagination instance before each of them:

import { reset } from 'vuex-pagination'

// after each test run:
reset()

Tests

Tests are implemented using Jest and vue-test-utils. They can be run like this: npm test

Coding style is Standard and is tested in the test script too.