SaPaginator (PaginatorContent)
Vue 3 pagination component dengan DataContainer integration dan manual mode.
Versi: 0.1.2 | Changelog
Kategori: UI Component (Vue 3)
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:
import { PaginatorContent } from '@bpmlib/vue-sapaginator';Types:
import type { PaginatorProps, PaginatorEmits } from '@bpmlib/vue-sapaginator';Installation & Setup
Requirements
Peer Dependencies
Wajib:
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.0Optional (container mode):
npm install @bpmlib/utils-data-container@^0.4.0| Dependency | Versi | Status | Deskripsi |
|---|---|---|---|
vue | ^3.4.0 | Required | Vue 3 framework |
@fortawesome/fontawesome-svg-core | ^6.0.0 | Required | FontAwesome core |
@fortawesome/free-solid-svg-icons | ^6.0.0 | Required | Icon set |
@fortawesome/vue-fontawesome | ^3.0.0 | Required | Vue icon component |
@bpmlib/utils-data-container | ^0.2.0 | Optional | Untuk container mode |
IMPORTANT
@fortawesome/vue-fontawesome harus didaftarkan secara global di aplikasi sebelum menggunakan component ini.
Contoh registrasi global:
// 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
npm install @bpmlib/vue-sapaginatoryarn add @bpmlib/vue-sapaginatorpnpm add @bpmlib/vue-sapaginatorNOTE
Package ini di-publish ke private registry. Tambahkan konfigurasi registry ke .npmrc:
@bpmlib:registry=https://js.pkg.ppsdmmigas.id/Import
import { PaginatorContent } from '@bpmlib/vue-sapaginator';Quick Start
Container Mode (Direkomendasikan)
Integrasikan dengan useDataContainer dari @bpmlib/utils-data-container. State pagination dikelola otomatis.
<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.
<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
containerdi-pass. Data, pagination state, load status, search, dan error state semuanya dibaca langsung dariDataContainerObject. Event page navigation juga memanggil method container (nextPage(),prevPage(),navigatePage()). - Manual mode —
containertidak 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.
<!-- 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:
<PaginatorContent :on-click-refresh="() => {}" @click-refresh="handleRefresh" />Search Integration
Saat onSubmitSearch di-pass, search bar muncul di toolbar. Saat form di-submit:
- Di container mode:
container.submitSearch(searchValue)dipanggil — ini menyimpan nilai search dan otomatis reset halaman ke 1, lalu eventsubmitSearchdi-emit. - Di manual mode: hanya event
submitSearchyang 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.
// Signature
<PaginatorContent<TData> ... />Props
| Name | Type | Default | Description |
|---|---|---|---|
container | DataContainerObject<TData> | - | DataContainer object untuk auto mode Lihat selengkapnya |
data | TData[] | - | Array data (manual mode) |
amountShown | string | number | '0' | Jumlah item ditampilkan (manual mode) |
amountTotal | string | number | '0' | Total seluruh item (manual mode) |
currentPage | string | number | '0' | Halaman aktif (manual mode) |
maxPage | string | number | '0' | Total halaman (manual mode) |
loadStatus | number | - | Status load: -1 error, 0 loading, 1 loaded (manual mode) |
errorMessage | string | - | Pesan error saat loadStatus === -1 (manual mode) |
appliedSearch | string | - | Query search yang sedang aktif (manual mode) |
itemText | string | 'Data' | Label tipe item, tampil di info dan empty state |
listClass | string | 'grid grid-cols-1 gap-2' | Class CSS untuk grid container list |
disableNext | boolean | false | Disable tombol next |
disablePrev | boolean | false | Disable tombol prev |
withoutManualToggle | boolean | false | Sembunyikan input manual nomor halaman |
onlyInfo | boolean | false | Tampilkan hanya info section (sembunyikan kontrol paginasi) |
onlyToggle | boolean | false | Tampilkan 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:
() => voidContoh:
<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:
() => voidDi container mode, dipanggil setelah container.submitSearch() (yang otomatis reset halaman ke 1). Gunakan untuk trigger fetch ulang.
Contoh:
<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:
(page: number) => voidContoh:
<!-- Single handler untuk semua navigasi -->
<PaginatorContent :on-page-toggle="fetchPage" />
<!-- Atau via event -->
<PaginatorContent :on-page-toggle="() => {}" @page-toggle="fetchPage" />Model
| Name | Type | Default | Description |
|---|---|---|---|
v-model | string | - | Nilai input search bar |
Events
| Name | Payload | Description |
|---|---|---|
clickCreate | - | Tombol create diklik |
clickRefresh | - | Tombol refresh diklik |
clickFilter | - | Tombol filter diklik |
submitSearch | - | Search form di-submit |
prevPage | page: number | Navigasi ke halaman sebelumnya (hanya jika onPageToggle tidak di-pass) |
nextPage | page: number | Navigasi ke halaman berikutnya (hanya jika onPageToggle tidak di-pass) |
manualPage | page: number | Navigasi ke halaman spesifik (hanya jika onPageToggle tidak di-pass) |
pageToggle | page: number | Semua navigasi halaman (hanya jika onPageToggle di-pass) |
Slots
| Name | Props | Description |
|---|---|---|
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
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
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
- 2. Manual mode dengan semua props
- 3. Search dengan container
- 4. Custom item template dengan conditional styling
- 5. Custom list layout dengan slot customList
- 6. Pre-content slot untuk summary/filter chips
- 7. onlyInfo dan onlyToggle terpisah
- 8. Single page handler dengan onPageToggle
1. Container mode dengan toolbar lengkap
Setup lengkap container mode dengan create, refresh, filter, dan search.
<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.
<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.
<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 1watchpadapage.numeric.currentmenangkap reset ini dan trigger fetch
4. Custom item template dengan conditional styling
Template item yang lebih kompleks dengan status badge.
<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.
<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.
<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.
<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.
<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 (biasanyaflex 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.
Links
- Repository: repo.ppsdmmigas.id/bpmlib/js/vue-sapaginator
- Registry: js.pkg.ppsdmmigas.id