# vue-use-paginator

Vue 3 composition API hook function to reactively paginate data and arrange paginator buttons. Completely renderless.

## Demo

* [Live demo](https://demo-vue-use-paginator.netlify.app/)
* [Demo code](https://github.com/Sun0fABeach/demo-vue-use-paginator/blob/master/src/components/Demo.vue)

## Installation

```shell
$ npm install vue-use-paginator
```

## Basic Setup

```html
<script setup lang="ts">
import usePaginator from 'vue-use-paginator'

const {
  page,
  pageSize,
  numPages,
  numItems,
  numButtons,
  slice,
  buttons,
  hasPrev,
  hasNext,
  goStart,
  goPrev,
  goNext,
  goEnd,
} = usePaginator({
  pageSize: 10,
  numItems: 70,
  numButtons: 5,
})
</script>
```

## Details

### Hook API

Function `usePaginator` returns an object of type [`IPagination`](<#user-content-definitions> "IPagination") which contains the following reactive data plus some convenience functions. You can initialize `page`, `pageSize`, `numItems` and `numButtons` by passing an object of type [`IOptions`](<#user-content-definitions> "IOptions"). Otherwise, default initial values will be used.

| Name           | Type                               | Description
| -------------- | ---------------------------------- | -----------
| `page`         | `Ref<number>`                     | Current active page (default initial value: 1)
| `pageSize`     | `Ref<number>`                     | Size of each page (default initial value: 5)
| `numItems`     | `Ref<number>`                     | Total number of items to paginate (default initial value: 0)
| `numButtons`   | `Ref<number>`                     | Number of paginator buttons to display (default initial value: 5)
| `numPages`     | `ComputedRef<number>`             | Total number of pages
| `slice`        | `ComputedRef<[number, number]>`    | Tuple containing start and end index delimiting the currently active page (0 based, end index is exclusive). For example, given you are on page 2, `pageSize` is 10 and `numItems` is greater than or equal to 20, `slice` will be `[10, 20]`.
| `buttons`      | `ComputedRef<IPaginatorButton[]>` | Array of objects usable for displaying paginator buttons (details below)
| `hasPrev`      | `ComputedRef<boolean>`            | Whether the currently active page is the first page
| `hasNext`      | `ComputedRef<boolean>`            | Whether the currently active page is the last page
| `goPrev`       | `() => number`                     | Go to next page
| `goNext`       | `() => number`                     | Go to previous page
| `goStart`      | `() => 1`                          | Go to first page
| `goEnd`        | `() => number`                     | Go to last page

* `page`, `pageSize`, `numItems`, as well as `numButtons` are writable refs that will cause the other reactive properties to adjust when changed. Once you know how many items to paginate, you need to set `numItems` at least.
* You can move to another page by either calling `goPrev`, `goNext`, `goStart`, `goEnd`, or by assigning to `page` directly. Note that assigning a value outside of 1 and `numPages` will be ignored.

### Paginator buttons

When displaying paginator buttons, you often don't have enough slots for each page and therefore need to use a placeholder like '...'. Calculating the positions of these placeholders is not trivial. Fortunately, `buttons` is an array of [`IPaginatorButton`](<#user-content-definitions> "IPaginatorButton") where each item contains the following properties to help you out.

| Name       | Type       | Description
| ---------- | ---------- | -----------
| `page`     | `number`   | The page to navigate to when clicking this button
| `active`   | `boolean`  | Whether this button shows the currently active page
| `ellipsis` | `boolean`  | Whether this button is a placeholder for omitted pages

#### Template example

Here is a minimal example how you could write your paginator markup.

```vue
<ol>
  <li :class="{ disabled: !hasPrev }" @click="goStart">
    {{ '<<' }}
  </li>
  <li :class="{ disabled: !hasPrev }" @click="goPrev">
    {{ '<' }}
  </li>
  <li
    v-for="button in buttons"
    :key="button.page"
    :class="{ 'current-page': button.active }"
    @click="page = button.page"
  >
    {{ button.ellipsis ? '...' : button.page }}
  </li>
  <li :class="{ disabled: !hasNext }" @click="goNext">
    {{ '>' }}
  </li>
  <li :class="{ disabled: !hasNext }" @click="goEnd">
    {{ '>>' }}
  </li>
</ol>
```

Notice how certain classes and texts are set based on the values of `hasPrev`, `hasNext`, `button.active` and `button.ellipsis`. How you style these elements is completely up to you.

### API integration

This library doesn't make any assumptions about whether you paginate in the frontend or call a paginated endpoint.

If you paginate in the frontend, just assign the length of your collection to  `numItems` and use the values from `slice` to slice out the items to display for your current page.

If you call a paginated endpoint, it needs to tell you the total number of items in order to set `numItems`. You can then use `slice` or `page`/`pageSize` to fetch a page. You will normally want to set up a watcher in order to fetch a new page when `page` or `pageSize` changes.

```typescript
watch([page, pageSize], ([newPage, newPageSize]) => {
  fetchPage(newPage, newPageSize);
})
```

### Types

#### Definitions

```typescript
interface IOptions {
  page?: number | Ref<number>
  pageSize?: number | Ref<number>
  numItems?: number | Ref<number>
  numButtons?: number | Ref<number>
}

interface IPagination {
  page: Ref<number>
  pageSize: Ref<number>
  numItems: Ref<number>
  numButtons: Ref<number>
  numPages: ComputedRef<number>
  slice: ComputedRef<[number, number]>
  buttons: ComputedRef<IPaginatorButton[]>
  hasPrev: ComputedRef<boolean>
  hasNext: ComputedRef<boolean>
  goPrev: () => number
  goNext: () => number
  goStart: () => 1
  goEnd: () => number
}

interface IButton {
  page: number
}

interface IPageButton extends IButton {
  active: boolean
  ellipsis: false
}

interface IEllipsisButton extends IButton {
  active: false
  ellipsis: true
}

type IPaginatorButton = IPageButton | IEllipsisButton
```

#### Importing Types

```typescript
import type { IOptions, IPagination, IPaginatorButton } from 'vue-use-paginator'
```
