Skip to content

SaPaginator (PaginatorContent)

Vue 3 pagination component dengan DataContainer integration dan manual mode.

Versi: 0.1.2 | Changelog
Kategori: UI Component (Vue 3)

npm versionTypeScriptVueTailwind CSS

WARNING

MVP LIBRARY Library ini di-extract dari family project. Styling tightly coupled dengan parent project dan mengharuskan class definitions tertentu. Lihat Styling untuk detail.

TL;DR

Component untuk menampilkan list data berpaginasi dengan toolbar aksi (create, refresh, filter, search), loading state, error state, dan empty state. Mendukung dua mode: container mode yang terintegrasi dengan @bpmlib/utils-data-container untuk manajemen state otomatis, dan manual mode untuk kontrol penuh via props dan events.

Components:

ts
import { PaginatorContent } from '@bpmlib/vue-sapaginator';

Types:

ts
import type { PaginatorProps, PaginatorEmits } from '@bpmlib/vue-sapaginator';

Installation & Setup

Requirements

Peer Dependencies

Wajib:

bash
npm install vue@^3.4.0 @fortawesome/fontawesome-svg-core@^6.0.0 @fortawesome/free-solid-svg-icons@^6.0.0 @fortawesome/vue-fontawesome@^3.0.0

Optional (container mode):

bash
npm install @bpmlib/utils-data-container@^0.4.0
DependencyVersiStatusDeskripsi
vue^3.4.0RequiredVue 3 framework
@fortawesome/fontawesome-svg-core^6.0.0RequiredFontAwesome core
@fortawesome/free-solid-svg-icons^6.0.0RequiredIcon set
@fortawesome/vue-fontawesome^3.0.0RequiredVue icon component
@bpmlib/utils-data-container^0.2.0OptionalUntuk container mode

IMPORTANT

@fortawesome/vue-fontawesome harus didaftarkan secara global di aplikasi sebelum menggunakan component ini.

Contoh registrasi global:

ts
// main.ts
import { createApp } from 'vue';
import { library } from '@fortawesome/fontawesome-svg-core';
import { fas } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';

library.add(fas);

const app = createApp(App);
app.component('font-awesome-icon', FontAwesomeIcon);
app.mount('#app');

Package Installation

bash
npm install @bpmlib/vue-sapaginator
bash
yarn add @bpmlib/vue-sapaginator
bash
pnpm add @bpmlib/vue-sapaginator

NOTE

Package ini di-publish ke private registry. Tambahkan konfigurasi registry ke .npmrc:

@bpmlib:registry=https://js.pkg.ppsdmmigas.id/

Import

ts
import { PaginatorContent } from '@bpmlib/vue-sapaginator';

Quick Start

Container Mode (Direkomendasikan)

Integrasikan dengan useDataContainer dari @bpmlib/utils-data-container. State pagination dikelola otomatis.

vue
<script setup lang="ts">
import { PaginatorContent } from '@bpmlib/vue-sapaginator';
import { useDataContainer } from '@bpmlib/utils-data-container';

interface Produk { id: number; nama: string; harga: number; }

const container = useDataContainer<Produk>();

const fetchData = async () => {
  container.loadStatus = 0;
  const res = await fetch(`/api/produk?${container.getterUrlStringAttribute()}`);
  container.mapFromResponse(await res.json());
};

watch(() => container.page.numeric.current, fetchData, { immediate: true });
</script>

<template>
  <PaginatorContent
    :container="container"
    item-text="Produk"
    :on-click-refresh="fetchData"
  >
    <template #default="{ item }">
      <div class="card">{{ item.nama }} — Rp{{ item.harga }}</div>
    </template>
  </PaginatorContent>
</template>

Manual Mode

Kontrol semua state secara manual via props dan events.

vue
<script setup lang="ts">
import { ref } from 'vue';
import { PaginatorContent } from '@bpmlib/vue-sapaginator';

const items = ref([]);
const currentPage = ref(1);
const maxPage = ref(1);
const loadStatus = ref<-1 | 0 | 1>(0);

const fetchPage = async (page: number) => {
  loadStatus.value = 0;
  currentPage.value = page;
  const res = await fetch(`/api/items?page=${page}`);
  const data = await res.json();
  items.value = data.content;
  maxPage.value = data.max_page;
  loadStatus.value = 1;
};

fetchPage(1);
</script>

<template>
  <PaginatorContent
    :data="items"
    :current-page="currentPage"
    :max-page="maxPage"
    :load-status="loadStatus"
    item-text="Item"
    @prev-page="fetchPage"
    @next-page="fetchPage"
    @manual-page="fetchPage"
  >
    <template #default="{ item }">
      <div>{{ item.name }}</div>
    </template>
  </PaginatorContent>
</template>

Core Concepts

Container Mode vs Manual Mode

Component membaca state dari dua sumber berdasarkan ada tidaknya prop container:

  • Container mode — prop container di-pass. Data, pagination state, load status, search, dan error state semuanya dibaca langsung dari DataContainerObject. Event page navigation juga memanggil method container (nextPage(), prevPage(), navigatePage()).
  • Manual modecontainer tidak di-pass. Semua state dikontrol via props individual: data, currentPage, maxPage, loadStatus, errorMessage, amountShown, amountTotal. Events digunakan untuk notify perubahan halaman ke parent.

Kedua mode tetap emit events untuk backward compatibility. Di container mode, events tetap di-emit setelah method container dipanggil.

Visibility Callback Props

Prop onClickCreate, onClickFilter, onClickRefresh, dan onSubmitSearch bersifat dual-purpose: kehadiran prop menentukan apakah tombol/elemen tersebut di-render, dan nilai prop adalah handler yang dipanggil saat interaksi.

vue
<!-- Tombol refresh TIDAK muncul -->
<PaginatorContent :container="c" />

<!-- Tombol refresh MUNCUL dan memanggil fetchData saat diklik -->
<PaginatorContent :container="c" :on-click-refresh="fetchData" />

Jika ingin tombol muncul tapi handle via event (bukan prop), pass fungsi kosong sebagai prop dan dengarkan eventnya:

vue
<PaginatorContent :on-click-refresh="() => {}" @click-refresh="handleRefresh" />

Search Integration

Saat onSubmitSearch di-pass, search bar muncul di toolbar. Saat form di-submit:

  1. Di container mode: container.submitSearch(searchValue) dipanggil — ini menyimpan nilai search dan otomatis reset halaman ke 1, lalu event submitSearch di-emit.
  2. Di manual mode: hanya event submitSearch yang di-emit. Parent bertanggung jawab reset halaman.

Nilai input search terikat via v-model pada component.

Pagination Controls Visibility

Kontrol navigasi halaman ditampilkan berdasarkan mode pagination:

  • lengthAware — kontrol penuh: tombol prev/next, page number buttons, ellipsis, dan manual page input.
  • simple / regular / cursor — hanya tombol prev/next dan label halaman saat ini.
  • none — tidak ada kontrol navigasi sama sekali.

Di container mode, mode ditentukan via page.setup?.mode (di-set melalui declarePagination() atau QueryIntent). Di manual mode, kontrol muncul saat prop max-page di-pass dengan nilai lebih dari 0 (otomatis dianggap lengthAware).


API Reference

Components

PaginatorContent

Component utama. Generic terhadap tipe data TData.

ts
// Signature
<PaginatorContent<TData> ... />

Props

NameTypeDefaultDescription
containerDataContainerObject<TData>-DataContainer object untuk auto mode Lihat selengkapnya
dataTData[]-Array data (manual mode)
amountShownstring | number'0'Jumlah item ditampilkan (manual mode)
amountTotalstring | number'0'Total seluruh item (manual mode)
currentPagestring | number'0'Halaman aktif (manual mode)
maxPagestring | number'0'Total halaman (manual mode)
loadStatusnumber-Status load: -1 error, 0 loading, 1 loaded (manual mode)
errorMessagestring-Pesan error saat loadStatus === -1 (manual mode)
appliedSearchstring-Query search yang sedang aktif (manual mode)
itemTextstring'Data'Label tipe item, tampil di info dan empty state
listClassstring'grid grid-cols-1 gap-2'Class CSS untuk grid container list
disableNextbooleanfalseDisable tombol next
disablePrevbooleanfalseDisable tombol prev
withoutManualTogglebooleanfalseSembunyikan input manual nomor halaman
onlyInfobooleanfalseTampilkan hanya info section (sembunyikan kontrol paginasi)
onlyTogglebooleanfalseTampilkan hanya kontrol paginasi (sembunyikan info section)
onClickCreate() => void-Tampilkan tombol create; handler dipanggil saat klik Lihat selengkapnya
onClickRefresh() => void-Tampilkan tombol refresh; handler dipanggil saat klik Lihat selengkapnya
onClickFilter() => void-Tampilkan tombol filter; handler dipanggil saat klik Lihat selengkapnya
onSubmitSearch() => void-Tampilkan search bar; handler dipanggil saat submit Lihat selengkapnya
onPageToggle(page: number) => void-Jika di-pass, semua event page navigation di-emit sebagai pageToggle Lihat selengkapnya
container

DataContainerObject dari @bpmlib/utils-data-container. Saat di-pass, component membaca semua state (data, pagination, loadStatus, search, error) dari object ini.

Use Case: Gunakan saat mengintegrasikan dengan composable useDataContainer. Lihat dokumentasi @bpmlib/utils-data-container untuk detail interface-nya.


onClickCreate, onClickRefresh, onClickFilter

Callback yang juga menentukan visibilitas tombol di toolbar. Tidak di-pass → tombol tidak render.

Signature:

ts
() => void

Contoh:

vue
<PaginatorContent
  :on-click-create="() => router.push('/produk/baru')"
  :on-click-refresh="fetchData"
  :on-click-filter="() => showFilterModal = true"
/>

onSubmitSearch

Callback yang juga menentukan visibilitas search bar. Tidak di-pass → search bar tidak render.

Signature:

ts
() => void

Di container mode, dipanggil setelah container.submitSearch() (yang otomatis reset halaman ke 1). Gunakan untuk trigger fetch ulang.

Contoh:

vue
<PaginatorContent
  v-model="searchQuery"
  :container="container"
  :on-submit-search="fetchData"
/>

onPageToggle

Jika di-pass, semua navigasi halaman (prev, next, manual) akan emit event pageToggle alih-alih event individual (prevPage, nextPage, manualPage). Berguna saat satu handler menangani semua perubahan halaman.

Signature:

ts
(page: number) => void

Contoh:

vue
<!-- Single handler untuk semua navigasi -->
<PaginatorContent :on-page-toggle="fetchPage" />

<!-- Atau via event -->
<PaginatorContent :on-page-toggle="() => {}" @page-toggle="fetchPage" />

Model

NameTypeDefaultDescription
v-modelstring-Nilai input search bar

Events

NamePayloadDescription
clickCreate-Tombol create diklik
clickRefresh-Tombol refresh diklik
clickFilter-Tombol filter diklik
submitSearch-Search form di-submit
prevPagepage: numberNavigasi ke halaman sebelumnya (hanya jika onPageToggle tidak di-pass)
nextPagepage: numberNavigasi ke halaman berikutnya (hanya jika onPageToggle tidak di-pass)
manualPagepage: numberNavigasi ke halaman spesifik (hanya jika onPageToggle tidak di-pass)
pageTogglepage: numberSemua navigasi halaman (hanya jika onPageToggle di-pass)

Slots
NamePropsDescription
default{ item: TData, index: number }Template untuk setiap item dalam list
customList{ data: TData[] }Gantikan seluruh list dengan rendering custom
preContent-Konten yang ditampilkan antara toolbar dan list
loadingView-Override tampilan loading state (default: LoadingCard)
errorView{ message: string, retry: () => void }Override tampilan error state (default: NotificationCard dengan error message + tombol retry)
emptyView{ itemText: string, retry: () => void }Override tampilan empty state / data kosong (default: NotificationCard dengan ghost icon + tombol retry)

Types

PaginatorProps

ts
interface PaginatorProps<TData = unknown> {
  container?: DataContainerObject<TData>;
  amountShown?: string | number;
  amountTotal?: string | number;
  currentPage?: string | number;
  maxPage?: string | number;
  loadStatus?: number;
  errorMessage?: string;
  data?: TData[];
  itemText?: string;
  disableNext?: boolean;
  disablePrev?: boolean;
  withoutManualToggle?: boolean;
  onlyInfo?: boolean;
  onlyToggle?: boolean;
  appliedSearch?: string;
  listClass?: string;
  onClickCreate?: () => void;
  onClickFilter?: () => void;
  onClickRefresh?: () => void;
  onSubmitSearch?: () => void;
  onPageToggle?: (page: number) => void;
}

Interface untuk semua props PaginatorContent. Gunakan saat membuat wrapper component.


PaginatorEmits

ts
interface PaginatorEmits {
  clickCreate: [];
  clickRefresh: [];
  clickFilter: [];
  prevPage: [page: number];
  nextPage: [page: number];
  manualPage: [page: number];
  pageToggle: [page: number];
  submitSearch: [];
}

Examples

Contains:

1. Container mode dengan toolbar lengkap

Setup lengkap container mode dengan create, refresh, filter, dan search.

vue
<script setup lang="ts">
import { ref, watch } from 'vue';
import { PaginatorContent } from '@bpmlib/vue-sapaginator';
import { useDataContainer } from '@bpmlib/utils-data-container';

interface User { id: number; name: string; email: string; }

const container = useDataContainer<User>();
const searchVal = ref('');
const showFilter = ref(false);

const fetchData = async () => {
  container.loadStatus = 0;
  const res = await fetch(`/api/users?${container.getterUrlStringAttribute()}`);
  container.mapFromResponse(await res.json());
};

watch(
  () => container.page.numeric.current,
  fetchData,
  { immediate: true },
);
</script>

<template>
  <PaginatorContent
    v-model="searchVal"
    :container="container"
    item-text="User"
    :on-click-create="() => $router.push('/users/new')"
    :on-click-refresh="fetchData"
    :on-click-filter="() => (showFilter = true)"
    :on-submit-search="fetchData"
  >
    <template #default="{ item }">
      <div class="card">
        <strong>{{ item.name }}</strong>
        <span>{{ item.email }}</span>
      </div>
    </template>
  </PaginatorContent>
</template>

2. Manual mode dengan semua props

Kontrol penuh tanpa DataContainer — cocok untuk integrasi legacy atau API non-standar.

vue
<script setup lang="ts">
import { ref } from 'vue';
import { PaginatorContent } from '@bpmlib/vue-sapaginator';

const data = ref<{ name: string }[]>([]);
const currentPage = ref(1);
const maxPage = ref(1);
const totalData = ref(0);
const loadStatus = ref<-1 | 0 | 1>(0);
const errorMsg = ref('');

const fetchPage = async (page: number) => {
  loadStatus.value = 0;
  try {
    const res = await fetch(`/api/items?page=${page}`);
    const json = await res.json();
    data.value = json.content;
    maxPage.value = json.max_page;
    totalData.value = json.total;
    currentPage.value = page;
    loadStatus.value = 1;
  } catch (e) {
    errorMsg.value = 'Gagal memuat data';
    loadStatus.value = -1;
  }
};

fetchPage(1);
</script>

<template>
  <PaginatorContent
    :data="data"
    :current-page="currentPage"
    :max-page="maxPage"
    :amount-shown="data.length"
    :amount-total="totalData"
    :load-status="loadStatus"
    :error-message="errorMsg"
    item-text="Item"
    :on-click-refresh="() => fetchPage(currentPage)"
    @prev-page="fetchPage"
    @next-page="fetchPage"
    @manual-page="fetchPage"
  >
    <template #default="{ item }">
      <div>{{ item.name }}</div>
    </template>
  </PaginatorContent>
</template>

3. Search dengan container

Search ter-bind via v-model. Submit otomatis reset halaman ke 1 dan fetch ulang.

vue
<script setup lang="ts">
import { ref, watch } from 'vue';
import { PaginatorContent } from '@bpmlib/vue-sapaginator';
import { useDataContainer } from '@bpmlib/utils-data-container';

const container = useDataContainer<{ title: string }>();
const searchVal = ref('');

const fetchData = async () => {
  container.loadStatus = 0;
  const res = await fetch(`/api/articles?${container.getterUrlStringAttribute()}`);
  container.mapFromResponse(await res.json());
};

// Watch page changes (termasuk reset ke 1 saat search)
watch(() => container.page.numeric.current, fetchData, { immediate: true });
</script>

<template>
  <PaginatorContent
    v-model="searchVal"
    :container="container"
    item-text="Artikel"
    :on-submit-search="fetchData"
  >
    <template #default="{ item }">
      <div>{{ item.title }}</div>
    </template>
  </PaginatorContent>
</template>

Key Takeaways:

  • container.submitSearch(searchVal) dipanggil otomatis saat submit — reset page ke 1
  • watch pada page.numeric.current menangkap reset ini dan trigger fetch

4. Custom item template dengan conditional styling

Template item yang lebih kompleks dengan status badge.

vue
<template>
  <PaginatorContent :container="container" item-text="Pesanan">
    <template #default="{ item, index }">
      <div class="card flex justify-between items-center">
        <div>
          <span class="text-xs text-subtitle">#{{ index + 1 }}</span>
          <h5>{{ item.nomor }}</h5>
          <p class="text-xs">{{ item.pelanggan }}</p>
        </div>
        <span
          class="text-xs px-2 py-1 rounded"
          :class="item.status === 'selesai' ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'"
        >
          {{ item.status }}
        </span>
      </div>
    </template>
  </PaginatorContent>
</template>

5. Custom list layout dengan slot customList

Gantikan seluruh list rendering untuk layout yang tidak bisa dicapai dengan listClass.

vue
<template>
  <PaginatorContent :container="container" item-text="Foto">
    <template #customList="{ data }">
      <div class="grid grid-cols-2 md:grid-cols-4 gap-3">
        <div v-for="item in data" :key="item.id" class="aspect-square overflow-hidden rounded">
          <img :src="item.url" :alt="item.caption" class="w-full h-full object-cover" />
        </div>
      </div>
    </template>
  </PaginatorContent>
</template>

NOTE

Saat customList slot digunakan, prop listClass tidak berlaku.


6. Pre-content slot untuk summary/filter chips

Tampilkan konten antara toolbar dan list — cocok untuk active filter chips.

vue
<template>
  <PaginatorContent :container="container" item-text="Produk">
    <template #preContent>
      <div v-if="activeFilters.length" class="flex gap-2 flex-wrap">
        <span
          v-for="f in activeFilters"
          :key="f.key"
          class="chip"
          @click="removeFilter(f.key)"
        >
          {{ f.label }} ✕
        </span>
      </div>
    </template>

    <template #default="{ item }">
      <div>{{ item.nama }}</div>
    </template>
  </PaginatorContent>
</template>

7. onlyInfo dan onlyToggle terpisah

Pisahkan info section dan kontrol paginasi ke area layout yang berbeda.

vue
<template>
  <!-- Info section di header halaman -->
  <div class="page-header">
    <PaginatorContent :container="container" item-text="Data" only-info />
  </div>

  <!-- List di tengah -->
  <PaginatorContent :container="container">
    <template #default="{ item }"><div>{{ item.name }}</div></template>
  </PaginatorContent>

  <!-- Kontrol paginasi di footer -->
  <div class="page-footer">
    <PaginatorContent :container="container" only-toggle />
  </div>
</template>

CAUTION

Gunakan instance container yang sama untuk ketiga component agar state tetap sinkron.


8. Single page handler dengan onPageToggle

Satu handler untuk semua navigasi halaman — prev, next, maupun manual input.

vue
<script setup lang="ts">
import { ref } from 'vue';
import { PaginatorContent } from '@bpmlib/vue-sapaginator';

const data = ref([]);
const currentPage = ref(1);
const maxPage = ref(5);
const loadStatus = ref<-1 | 0 | 1>(1);

const goToPage = async (page: number) => {
  loadStatus.value = 0;
  const res = await fetch(`/api/items?page=${page}`);
  const json = await res.json();
  data.value = json.content;
  currentPage.value = page;
  loadStatus.value = 1;
};
</script>

<template>
  <PaginatorContent
    :data="data"
    :current-page="currentPage"
    :max-page="maxPage"
    :load-status="loadStatus"
    :on-page-toggle="goToPage"
  >
    <template #default="{ item }"><div>{{ item.name }}</div></template>
  </PaginatorContent>
</template>

Styling

CSS Import

Component tidak menyediakan stylesheet dan tidak memerlukan CSS import tambahan.

Disediakan oleh library ini: Tidak ada — hanya component structure.

TIDAK Disediakan:

  • Colors, spacing, typography
  • Card appearance
  • Button styling
  • Layout structure

WARNING

Component memerlukan Tailwind CSS dan class definitions dari parent project untuk tampil dengan benar. Tanpa class-class ini, component akan render tanpa styling.

Expected Classes dari Parent Project

Component internal menggunakan class names berikut yang harus didefinisikan di parent project:

Card Classes

  • .card-normal — card dengan background default
  • .card-sub — card dengan background subtle
  • .card-red — card dengan indikasi error/danger
  • .card-green — card dengan indikasi success
  • .card-primary, .card-secondary, .card-ternary — card themed

Button & Group Classes

  • .btn-group — wrapper untuk sekumpulan tombol yang terhubung

Typography & Layout

  • .text-subtitle — warna teks untuk subtitle/secondary text
  • .text-ld — text variant untuk page indicator
  • .all-center — utility untuk centering (biasanya flex items-center justify-center)

TIP

NON-FAMILY PROJECT Jika menggunakan di luar family project, definisikan class-class ini sesuai design system kamu. Component hanya butuh class names tersebut ada — tidak peduli implementasinya.