PaginatorContent (@bpmlib/vue-sapaginator)
Vue 3 pagination component dengan DataContainer support dan flexible rendering options
Versi: 0.1.0
Kategori: UI Component (Vue 3)
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
Vue 3 pagination component yang mendukung dua mode operasi: manual props untuk kontrol penuh, atau DataContainer integration untuk state management otomatis. Menyediakan built-in loading states, error handling, search bar, action buttons, dan flexible rendering dengan slots.
Components:
import { PaginatorContent } from '@bpmlib/vue-sapaginator';Types:
import type {
PaginatorProps,
PaginatorEmits
} from '@bpmlib/vue-sapaginator';Installation & Setup
Requirements
Peer Dependencies
Library ini memerlukan peer dependencies berikut:
Wajib:
npm install vue@^3.4.0
npm install @fortawesome/fontawesome-svg-core@^6.0.0
npm install @fortawesome/free-solid-svg-icons@^6.0.0
npm install @fortawesome/vue-fontawesome@^3.0.0
npm install floating-vue@^5.0.0Optional:
npm install @bpmlib/utils-data-container@^0.1.1| 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 | FontAwesome icons |
@fortawesome/vue-fontawesome | ^3.0.0 | Required | FontAwesome Vue component |
floating-vue | ^5.0.0 | Required | Tooltip library |
@bpmlib/utils-data-container | ^0.1.1 | Optional | Untuk DataContainer mode |
tailwindcss | ^3.0.0 | Optional | Untuk styling lengkap |
Package Installation
npm install @bpmlib/vue-sapaginatoryarn add @bpmlib/vue-sapaginatorpnpm add @bpmlib/vue-sapaginatorbun install @bpmlib/vue-sapaginatorImport
Component Import:
import { PaginatorContent } from '@bpmlib/vue-sapaginator';Type Import:
import type {
PaginatorProps,
PaginatorEmits,
DataContainerObject
} from '@bpmlib/vue-sapaginator';Quick Start
Basic Usage - Manual Mode
Mode manual memberikan kontrol penuh untuk semua state dan events:
<script setup lang="ts">
import { ref } from 'vue';
import { PaginatorContent } from '@bpmlib/vue-sapaginator';
interface Product {
id: number;
name: string;
price: number;
}
const products = ref<Product[]>([
{ id: 1, name: 'Product A', price: 100 },
{ id: 2, name: 'Product B', price: 200 },
]);
const currentPage = ref(1);
const maxPage = ref(10);
const totalItems = ref(95);
const loadStatus = ref(1); // -1: error, 0: loading, 1: loaded
const handlePageChange = async (page: number) => {
currentPage.value = page;
loadStatus.value = 0;
try {
// Fetch data from API
const response = await fetch(`/api/products?page=${page}`);
const data = await response.json();
products.value = data.items;
totalItems.value = data.total;
maxPage.value = data.maxPage;
loadStatus.value = 1;
} catch (error) {
loadStatus.value = -1;
}
};
const handleRefresh = () => {
handlePageChange(currentPage.value);
};
</script>
<template>
<PaginatorContent
:data="products"
:current-page="currentPage"
:max-page="maxPage"
:amount-shown="products.length"
:amount-total="totalItems"
:load-status="loadStatus"
:onPageToggle="handlePageChange"
@click-refresh="handleRefresh"
item-text="Produk"
>
<template #default="{ item }">
<div class="card p-4">
<h3>{{ item.name }}</h3>
<p>Rp {{ item.price.toLocaleString() }}</p>
</div>
</template>
</PaginatorContent>
</template>Key Points:
- Kontrol penuh atas pagination state
- Manual event handling untuk page changes
- Load status management: -1 (error), 0 (loading), 1 (loaded)
Comprehensive Example - DataContainer Mode
Mode DataContainer menyediakan state management otomatis:
<script setup lang="ts">
import { PaginatorContent } from '@bpmlib/vue-sapaginator';
import { useDataContainer } from '@bpmlib/utils-data-container';
interface Product {
id: number;
name: string;
price: number;
category: string;
}
interface ProductFilter {
category: string;
minPrice: number;
}
// Initialize DataContainer
const container = useDataContainer<Product, ProductFilter>(
{ category: '', minPrice: 0 }, // initial filter
{
pagination: {
mode: 'lengthaware',
perPage: 20
},
sort: {
by: 'name',
dir: 'asc'
}
}
);
// Fetch function
const fetchProducts = async () => {
container.loadStatus = 0;
try {
const params = container.getterUrlStringAttribute();
const response = await fetch(`/api/products?${params}`);
const data = await response.json();
// Auto-map response to container
container.mapFromResponse(data);
} catch (error) {
container.setError(error.message, 500);
}
};
// Search handling
const searchValue = ref('');
const handleSearch = async () => {
container.search = searchValue.value;
await fetchProducts();
};
// Initial load
fetchProducts();
</script>
<template>
<PaginatorContent
:container="container"
v-model="searchValue"
@click-refresh="fetchProducts"
@submit-search="handleSearch"
item-text="Produk"
>
<template #default="{ item }">
<div class="card p-4">
<h3>{{ item.name }}</h3>
<p class="text-sm text-subtitle">{{ item.category }}</p>
<p class="font-bold">Rp {{ item.price.toLocaleString() }}</p>
</div>
</template>
</PaginatorContent>
</template>Key Points:
- DataContainer auto-manages pagination, sort, filter, dan search state
mapFromResponse()otomatis parse API responsegetterUrlStringAttribute()generate query string dari state- Tidak perlu manual page event handling
Core Concepts
Dual Mode Operation
Component mendukung dua mode operasi yang bisa dipilih sesuai kebutuhan project.
Manual Mode:
- Kontrol penuh atas state dan events via props
- Cocok untuk simple pagination atau custom state management
- Developer handle semua state changes dan API calls
- Semua props dikelola manual (
currentPage,maxPage,data,loadStatus)
DataContainer Mode:
- State management otomatis via
@bpmlib/utils-data-container - Container centralize semua pagination state (page, sort, filter, search)
- Auto-sync state changes ke component rendering
- Built-in API response mapping via
mapFromResponse() - Auto URL generation via
getterUrlStringAttribute() - Minimal boilerplate code
Mode Detection Logic:
Component internally check props.container untuk determine mode:
// Internal logic (simplified)
const computedAmountShown = computed(() => {
if (props.container) {
// DataContainer mode: extract dari container
return props.container.data.value.length;
}
// Manual mode: use manual prop
return Number(props.amountShown) || 0;
});Ketika container prop provided, semua manual props diabaikan. Component prioritize container state untuk consistency.
Mode Examples:
// Manual mode - provide individual props
<PaginatorContent
:data="items"
:current-page="currentPage"
:max-page="maxPage"
:load-status="loadStatus"
@prevPage="handlePrev"
@nextPage="handleNext"
/>
// DataContainer mode - provide container only
<PaginatorContent :container="container" />Di DataContainer mode, tidak perlu handle page events karena container auto-update internal state saat user click pagination buttons.
Loading States & Error Handling
Component memiliki 3 built-in loading states yang otomatis render UI berbeda berdasarkan loadStatus value.
State Flow:
Initial/Loading (0) → Success (1) → Display Data
↓
Error (-1) → Show Error Card with RetryState Values:
-1= Error state (menampilkan error message dengan retry button)0= Loading state (menampilkan loading card dengan skeleton animation)1= Loaded state (menampilkan data normal dengan pagination)
Internal Rendering Logic:
<template>
<LoadingCard v-if="computedLoadStatus === 0" />
<NotificationCard v-else-if="computedLoadStatus === -1">
<!-- Error UI dengan retry button -->
<BasicButton @click="triggerClickRefresh">Coba Lagi</BasicButton>
</NotificationCard>
<div v-else>
<!-- Data display dengan pagination controls -->
</div>
</template>Manual Mode Implementation:
Developer manually control state transitions:
const loadStatus = ref(0); // Start loading
const errorMessage = ref('');
try {
const data = await fetchData();
products.value = data.items;
loadStatus.value = 1; // Success
} catch (error) {
loadStatus.value = -1; // Error
errorMessage.value = error.message;
}DataContainer Mode Implementation:
Container auto-manages transitions via internal _loadStatus ref dengan getter/setter:
const fetchData = async () => {
// Set loading (triggers LoadingCard)
container.loadStatus = 0;
try {
const response = await api.get('/products');
// mapFromResponse() auto set loadStatus = 1
container.mapFromResponse(response.data);
} catch (error) {
// setError() auto set loadStatus = -1
container.setError(error.message, error.code);
}
};Breaking Change di v0.1.1:
DataContainer v0.1.1 changed loadStatus dari Ref ke getter/setter pattern:
// v0.1.0 (old) - TIDAK BEKERJA LAGI
container.loadStatus.value = 0;
if (container.loadStatus.value === 1) { }
// v0.1.1 (current) - Direct access
container.loadStatus = 0;
if (container.loadStatus === 1) { }Component automatically listen ke loadStatus changes dan trigger re-render dengan UI yang sesuai.
Pagination Modes
Component mendukung 4 pagination modes yang determine tampilan dan behavior pagination controls. Mode detection berdasarkan container.page.paginationMode atau maxPage prop presence.
Internal Mode Detection:
const isLengthAwarePagination = computed(() => {
if (props.container) {
return props.container.page.paginationMode === 'lengthaware';
}
// Manual mode: check if maxPage provided
return props.maxPage !== '0' && props.maxPage !== 0 && props.maxPage !== undefined;
});1. Length-Aware Pagination (lengthaware)
Default mode dengan complete pagination info. API response harus provide:
current_page- Current page numbermax_page- Total pagestotal- Total items countper_page- Items per page
Tampilan:
- Page number buttons (sliding window, max 5 visible)
- First/Last page buttons
- Prev/Next buttons
- Manual page input dropdown
- Info: "Menampilkan X dari Total Y Data"
DataContainer Setup:
const container = useDataContainer(filter, {
pagination: {
mode: 'lengthaware',
perPage: 20
}
});
// API response expected format
{
success: true,
content: [...], // Array of items
current_page: 1,
max_page: 10,
total: 195,
per_page: 20
}2. Cursor Pagination (cursor)
Untuk API yang menggunakan cursor tokens instead of page numbers.
Tampilan:
- Prev/Next buttons only (no page numbers)
- Info: "X Data dimuat" (no total)
DataContainer Setup:
const container = useDataContainer(filter, {
pagination: {
mode: 'cursor',
perPage: 20
}
});
// API response expected format
{
success: true,
content: [...],
next_cursor: "eyJpZCI6MTAwfQ==",
previous_cursor: "eyJpZCI6MjB9",
per_page: 20,
has_more: true
}Container manage cursor state di page.cursor.next dan page.cursor.prev.
3. Simple Pagination (simple)
Load-more style pagination tanpa total info.
Tampilan:
- Next button only (jika
hasMore: true) - Info: "X Data dimuat"
DataContainer Setup:
const container = useDataContainer(filter, {
pagination: {
mode: 'simple',
perPage: 20
}
});
// API response expected format
{
success: true,
content: [...],
has_more: true,
per_page: 20
}4. None (none)
Tanpa pagination controls, hanya info section.
Tampilan:
- Info section only
- No pagination buttons
Useful untuk data yang tidak perlu pagination atau client-side filtered data.
Conditional Rendering:
Component internally check mode untuk conditional render pagination controls:
<div v-if="isLengthAwarePagination" class="mt-2 flex justify-end">
<basic-card v-if="!onlyInfo || onlyToggle">
<!-- Page number buttons, prev/next, etc -->
</basic-card>
</div>Hanya length-aware mode yang render full pagination controls dengan page numbers.
Search Integration
Component menyediakan built-in search bar dengan responsive design dan dual binding mechanism.
Search State Binding:
Component menggunakan v-model untuk two-way binding search input value:
<template>
<PaginatorContent
v-model="searchValue"
:onSubmitSearch="handleSearch"
/>
</template>Internally, component define model:
const searchVal = defineModel<string>();Search Bar Visibility Logic:
Search bar hanya muncul jika parent provide @submit-search event listener:
<div class="w-72 hidden md:block">
<form v-if="onSubmitSearch" @submit.prevent="emits('submitSearch')">
<FormInput v-model="searchVal" label="Cari" />
<BasicButton icon="paper-plane" tooltip="Mulai Cari" />
</form>
</div>Ketika parent provide @submit-search="handleSearch", Vue automatically create onSubmitSearch prop yang truthy, sehingga search bar akan render.
Responsive Behavior:
Component implement dua display modes via reactive state:
const searchBar = reactive({
display: false, // Toggle untuk mobile mode
});
const openSearch = () => {
searchBar.display = true;
searchInput.value?.triggerFocus(); // Auto-focus input
};Desktop Mode (md: breakpoint):
- Search bar always visible di sebelah kanan info section
- Full width input dengan submit button
- CSS:
class="w-72 hidden md:block"
Mobile Mode (<md breakpoint):
- Search button muncul di action buttons
- Click search button toggle search bar via
searchBar.display - Search bar replace info section saat active
- Back button untuk close search bar
<template>
<!-- Mobile: Search button -->
<BasicButton
v-if="onSubmitSearch"
icon="magnifying-glass"
class="block md:hidden"
@click="openSearch()"
/>
<!-- Info section dengan conditional display -->
<div v-if="!searchBar.display">
<!-- Info content -->
</div>
<div v-else>
<!-- Search form (mobile) -->
<FormInput v-model="searchVal" />
<BasicButton icon="reply" @click="searchBar.display = false" />
</div>
</template>Applied Search Display:
Component menampilkan active search query di info section:
<p class="text-xs">
<template v-if="computedAppliedSearch">
<template v-if="isLengthAwarePagination">
{{ computedAmountTotal }} Hasil Pencarian "{{ computedAppliedSearch }}"
</template>
<template v-else>
Hasil Pencarian "{{ computedAppliedSearch }}"
</template>
</template>
<template v-else>
Total {{ computedAmountTotal }} {{ itemText }}
</template>
</p>DataContainer Integration:
Search value disimpan di container.search property dan automatically included di URL generation:
const searchValue = ref('');
const handleSearch = async () => {
// Update container search state
container.search = searchValue.value;
// Reset to page 1
container.setPage({ numeric: { current: 1, max: null } });
// getterUrlStringAttribute() auto include search
const params = container.getterUrlStringAttribute();
// Result: "page=1&perPage=20&q=laptop"
await fetchData();
};<template>
<PaginatorContent
:container="container"
v-model="searchValue"
@submit-search="handleSearch"
/>
</template>Applied search value di-extract dari container.search untuk display, ensuring UI always show current active search.
API Reference
Components
PaginatorContent
Komponen utama untuk rendering pagination dengan data list.
Cara Penggunaan:
Component menerima generic type untuk data items:
<PaginatorContent<Product> :container="container">
<template #default="{ item }">
<!-- item is typed as Product -->
</template>
</PaginatorContent>Props
| Name | Type | Default | Description |
|---|---|---|---|
container | DataContainerObject<TData> | - | DataContainer object untuk auto state management Lihat selengkapnya |
amountShown | string | number | '0' | Jumlah items yang ditampilkan (manual mode) |
amountTotal | string | number | '0' | Total items available (manual mode) |
currentPage | string | number | '0' | Current page number (manual mode) |
maxPage | string | number | '0' | Maximum page number (manual mode) |
loadStatus | number | 0 | Loading status: -1 = error, 0 = loading, 1 = loaded (manual mode) |
errorMessage | string | - | Error message saat loadStatus = -1 (manual mode) |
data | TData[] | - | Array of data items untuk display (manual mode) |
itemText | string | 'Data' | Label untuk item type (e.g., "Produk", "User") |
disableNext | boolean | false | Disable next page button |
disablePrev | boolean | false | Disable previous page button |
withoutManualToggle | boolean | false | Hide manual page number input dropdown |
onlyInfo | boolean | false | Show only info section, hide pagination controls |
onlyToggle | boolean | false | Show only pagination controls, hide info section |
appliedSearch | string | - | Currently applied search query (manual mode) |
listClass | string | 'grid grid-cols-1 gap-2' | CSS class untuk list container layout |
onClickCreate | () => void | - | Show create button (listen via @click-create event) |
onClickFilter | () => void | - | Show filter button (listen via @click-filter event) |
onClickRefresh | () => void | - | Show refresh button (listen via @click-refresh event) |
onSubmitSearch | () => void | - | Show search bar (listen via @submit-search event) |
onPageToggle | (page: number) => void | - | Callback untuk override default page change behavior Lihat selengkapnya |
container
DataContainer object dari @bpmlib/utils-data-container untuk automatic state management.
Type: DataContainerObject<TData>
Behavior: Ketika container provided, component auto-extract semua state dari container dan ignore manual props (amountShown, currentPage, dll). Container harus di-initialize dengan useDataContainer().
Contoh:
import { useDataContainer } from '@bpmlib/utils-data-container';
const container = useDataContainer<Product, ProductFilter>(
{ category: '' },
{
pagination: { mode: 'lengthaware', perPage: 20 }
}
);<PaginatorContent :container="container" />Use Case: Gunakan DataContainer mode ketika:
- Butuh pagination, filter, sort, dan search state management
- Integrate dengan REST API (auto URL generation)
- Ingin minimal boilerplate code
- Need URL-driven state (deep linking support di v0.1.1+)
ACTION BUTTON PROPS
Props onClickCreate, onClickFilter, onClickRefresh, onSubmitSearch digunakan untuk conditional rendering (show/hide button).
Automatic prop creation: Ketika parent provide event listener (e.g., @click-create="handler"), Vue automatically create corresponding prop onClickCreate yang truthy, sehingga button akan muncul.
Usage:
<!-- ✅ Correct: Event listener automatically shows button -->
<PaginatorContent @click-create="handleCreate" />
<!-- ❌ Wrong: Redundant prop -->
<PaginatorContent
:onClickCreate="handleCreate"
@click-create="handleCreate"
/>Lihat Events section untuk detail event payloads.
onPageToggle
Callback function untuk override default page change behavior.
Signature:
onPageToggle: (page: number) => voidParameters:
page- Target page number yang akan diaktifkan
Contoh:
<script setup>
const handlePageToggle = async (page: number) => {
console.log('Navigating to page:', page);
// Custom logic before page change
analytics.track('page_change', { page });
// Update container or manual state
await fetchData(page);
};
</script>
<template>
<PaginatorContent
:container="container"
:onPageToggle="handlePageToggle"
/>
</template>Use Case:
- Add analytics tracking
- Implement custom page change logic
- Validate before page change
- Override default DataContainer page handling
Catatan:
- Jika provided, component emit
pageToggleevent instead ofprevPage,nextPage,manualPage - DataContainer mode: callback override auto page management
- Manual mode: callback required untuk handle page changes
Model
| Name | Type | Default | Description |
|---|---|---|---|
v-model | string | '' | Two-way binding untuk search input value |
Contoh:
<script setup>
const searchQuery = ref('');
</script>
<template>
<PaginatorContent
:container="container"
v-model="searchQuery"
:onSubmitSearch="handleSearch"
/>
<!-- searchQuery automatically updated -->
<p>Current search: {{ searchQuery }}</p>
</template>Events
| Name | Payload | Description |
|---|---|---|
clickCreate | [] | Emitted saat create button clicked |
clickRefresh | [] | Emitted saat refresh button clicked |
clickFilter | [] | Emitted saat filter button clicked |
prevPage | [page: number] | Emitted saat prev/first page button clicked (jika no onPageToggle) Lihat selengkapnya |
nextPage | [page: number] | Emitted saat next/last page button clicked (jika no onPageToggle) Lihat selengkapnya |
manualPage | [page: number] | Emitted saat page number clicked atau manual input submitted (jika no onPageToggle) Lihat selengkapnya |
pageToggle | [page: number] | Emitted untuk semua page changes jika onPageToggle prop provided Lihat selengkapnya |
submitSearch | [] | Emitted saat search form submitted |
prevPage
Event yang di-emit saat user click previous page atau first page button.
Payload:
page: number // Target page numberContoh:
<script setup>
const handlePrevPage = (page: number) => {
console.log('Going to page:', page);
currentPage.value = page;
fetchData(page);
};
</script>
<template>
<PaginatorContent
:current-page="currentPage"
:max-page="maxPage"
@prevPage="handlePrevPage"
/>
</template>Use Case: Manual mode pagination handling untuk previous page navigation.
Catatan: Event ini TIDAK di-emit jika onPageToggle prop provided. Use pageToggle event instead.
nextPage
Event yang di-emit saat user click next page atau last page button.
Payload:
page: number // Target page numberContoh:
<script setup>
const handleNextPage = (page: number) => {
console.log('Going to page:', page);
currentPage.value = page;
fetchData(page);
};
</script>
<template>
<PaginatorContent
:current-page="currentPage"
:max-page="maxPage"
@nextPage="handleNextPage"
/>
</template>Use Case: Manual mode pagination handling untuk next page navigation.
Catatan: Event ini TIDAK di-emit jika onPageToggle prop provided. Use pageToggle event instead.
manualPage
Event yang di-emit saat user click page number button atau submit manual page input.
Payload:
page: number // Target page numberContoh:
<script setup>
const handleManualPage = (page: number) => {
console.log('Jumping to page:', page);
currentPage.value = page;
fetchData(page);
};
</script>
<template>
<PaginatorContent
:current-page="currentPage"
:max-page="maxPage"
@manualPage="handleManualPage"
/>
</template>Use Case: Manual mode pagination handling untuk direct page jumps.
Catatan: Event ini TIDAK di-emit jika onPageToggle prop provided. Use pageToggle event instead.
pageToggle
Event yang di-emit untuk SEMUA page changes jika onPageToggle prop provided.
Payload:
page: number // Target page numberContoh:
<script setup>
const handlePageToggle = (page: number) => {
// Single handler untuk semua page changes
console.log('Page changed to:', page);
currentPage.value = page;
fetchData(page);
};
</script>
<template>
<PaginatorContent
:current-page="currentPage"
:max-page="maxPage"
:onPageToggle="handlePageToggle"
@pageToggle="handlePageToggle"
/>
</template>Use Case:
- Unified page change handling
- Simplify event handling (one handler instead of three)
- Override DataContainer auto page management
Catatan:
- Event ini HANYA di-emit jika
onPageToggleprop provided - Replace
prevPage,nextPage, danmanualPageevents - Recommended pattern untuk cleaner code
Slots
| Name | Props | Description |
|---|---|---|
default | { item: TData, index: number } | Scoped slot untuk render individual data item |
customList | { data: TData[] } | Scoped slot untuk completely custom list layout |
preContent | - | Slot untuk render content sebelum data list (after info section) |
Default Slot:
Render individual item dengan custom template:
<PaginatorContent :container="container">
<template #default="{ item, index }">
<div class="card p-4">
<span class="text-xs text-subtitle">#{{ index + 1 }}</span>
<h3>{{ item.name }}</h3>
</div>
</template>
</PaginatorContent>Custom List Slot:
Override entire list layout:
<PaginatorContent :container="container">
<template #customList="{ data }">
<div class="flex flex-col gap-2">
<div v-for="item in data" :key="item.id" class="custom-card">
{{ item.name }}
</div>
</div>
</template>
</PaginatorContent>Pre-Content Slot:
Add content before data list (e.g., filters, summaries):
<PaginatorContent :container="container">
<template #preContent>
<div class="card p-4 mb-2">
<p>Active filters: {{ activeFiltersCount }}</p>
</div>
</template>
<template #default="{ item }">
<!-- Item template -->
</template>
</PaginatorContent>Types
TYPE REFERENCE
Types berikut exported untuk TypeScript support. Untuk DataContainer types, lihat DataContainer Documentation.
PaginatorProps
Props interface untuk PaginatorContent component.
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;
}PaginatorEmits
Events interface untuk PaginatorContent component.
interface PaginatorEmits {
clickCreate: [];
clickRefresh: [];
clickFilter: [];
prevPage: [page: number];
nextPage: [page: number];
manualPage: [page: number];
pageToggle: [page: number];
submitSearch: [];
}Examples
Contains:
- 1. Custom List Layouts
- 2. Search Integration
- 3. Action Buttons & Callbacks
- 4. Display Mode Variations
- 5. Pagination State Management
- 6. Error Handling Patterns
1. Custom List Layouts
Demonstrasi custom rendering dengan customList slot dan custom listClass.
<script setup lang="ts">
import { PaginatorContent } from '@bpmlib/vue-sapaginator';
import { useDataContainer } from '@bpmlib/utils-data-container';
interface Product {
id: number;
name: string;
price: number;
image: string;
}
const container = useDataContainer<Product>({ /* ... */ });
// Grid layout (default)
const gridClass = 'grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4';
// List layout
const listClass = 'flex flex-col gap-2';
</script>
<template>
<!-- Default slot dengan custom grid -->
<PaginatorContent
:container="container"
:list-class="gridClass"
>
<template #default="{ item, index }">
<div class="card p-4">
<img :src="item.image" class="w-full h-32 object-cover mb-2" />
<span class="text-xs text-subtitle">#{{ index + 1 }}</span>
<h3 class="font-bold">{{ item.name }}</h3>
<p>Rp {{ item.price.toLocaleString() }}</p>
</div>
</template>
</PaginatorContent>
<!-- Custom list slot - complete override -->
<PaginatorContent :container="container">
<template #customList="{ data }">
<div class="flex flex-col gap-2">
<div
v-for="item in data"
:key="item.id"
class="card p-4 flex items-center gap-4"
>
<img :src="item.image" class="w-16 h-16 rounded" />
<div class="flex-1">
<h3>{{ item.name }}</h3>
<p class="text-sm text-subtitle">ID: {{ item.id }}</p>
</div>
<p class="font-bold">Rp {{ item.price.toLocaleString() }}</p>
</div>
</div>
</template>
</PaginatorContent>
</template>Key Takeaways:
listClassprop untuk custom grid/flex layout- Default slot untuk item template dengan existing layout
customListslot untuk completely custom list structure- Both approaches support responsive design
2. Search Integration
Search dengan DataContainer integration dan manual mode.
<script setup lang="ts">
import { ref } from 'vue';
import { PaginatorContent } from '@bpmlib/vue-sapaginator';
import { useDataContainer } from '@bpmlib/utils-data-container';
interface Product {
id: number;
name: string;
sku: string;
}
// DataContainer mode
const container = useDataContainer<Product>(
{ /* filter */ },
{
pagination: { mode: 'lengthaware', perPage: 20 },
search: { param: 'q', value: '' }
}
);
const searchValue = ref('');
const fetchProducts = async () => {
container.loadStatus = 0;
try {
// getterUrlStringAttribute() auto-include search di URL
const params = container.getterUrlStringAttribute();
const response = await fetch(`/api/products?${params}`);
const data = await response.json();
container.mapFromResponse(data);
} catch (error) {
container.setError(error.message, 500);
}
};
const handleSearch = async () => {
// Update container.search property
container.search = searchValue.value;
// Reset to page 1 via setPage()
container.setPage({ numeric: { current: 1, max: null } });
// Fetch with new search
await fetchProducts();
};
// Manual mode alternative
const manualSearchValue = ref('');
const manualAppliedSearch = ref('');
const products = ref<Product[]>([]);
const handleManualSearch = async () => {
manualAppliedSearch.value = manualSearchValue.value;
const response = await fetch(`/api/products?q=${manualSearchValue.value}`);
const data = await response.json();
products.value = data.items;
};
</script>
<template>
<!-- DataContainer mode -->
<PaginatorContent
:container="container"
v-model="searchValue"
@submit-search="handleSearch"
item-text="Produk"
>
<template #default="{ item }">
<div class="card p-4">
<h3>{{ item.name }}</h3>
<p class="text-xs">SKU: {{ item.sku }}</p>
</div>
</template>
</PaginatorContent>
<!-- Manual mode -->
<PaginatorContent
:data="products"
:applied-search="manualAppliedSearch"
v-model="manualSearchValue"
@submit-search="handleManualSearch"
item-text="Produk"
>
<template #default="{ item }">
<div class="card p-4">
<h3>{{ item.name }}</h3>
</div>
</template>
</PaginatorContent>
</template>Key Takeaways:
getterUrlStringAttribute()auto-include search di URL- Update
container.searchuntuk trigger search state setPage()untuk reset pagination ke page 1v-modeluntuk bind search input valueappliedSearchprop display current active search- Provide
@submit-searchevent listener untuk show search bar - Search bar responsive: button on mobile, input on desktop
3. Action Buttons & Callbacks
Demonstrasi create, filter, refresh buttons dengan proper callbacks.
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { PaginatorContent } from '@bpmlib/vue-sapaginator';
import { useDataContainer } from '@bpmlib/utils-data-container';
interface Product {
id: number;
name: string;
category: string;
}
interface ProductFilter {
category: string;
minPrice: number;
maxPrice: number;
}
const router = useRouter();
const showFilterModal = ref(false);
const container = useDataContainer<Product, ProductFilter>(
{ category: '', minPrice: 0, maxPrice: 0 },
{ pagination: { mode: 'lengthaware', perPage: 20 } }
);
const fetchProducts = async () => {
container.loadStatus = 0;
try {
const params = container.getterUrlStringAttribute();
const response = await fetch(`/api/products?${params}`);
const data = await response.json();
container.mapFromResponse(data);
} catch (error) {
container.setError(error.message, 500);
}
};
// Create handler
const handleCreate = () => {
router.push('/products/create');
};
// Filter handler
const handleFilter = () => {
showFilterModal.value = true;
};
// Refresh handler
const handleRefresh = async () => {
await fetchProducts();
};
// Apply filter from modal
const applyFilter = async (newFilter: ProductFilter) => {
// Update filter via setFilter()
container.setFilter('category', newFilter.category);
container.setFilter('minPrice', newFilter.minPrice);
container.setFilter('maxPrice', newFilter.maxPrice);
// Apply filter changes
container.applyFilter();
showFilterModal.value = false;
await fetchProducts();
};
// Initial load
fetchProducts();
</script>
<template>
<PaginatorContent
:container="container"
@click-create="handleCreate"
@click-filter="handleFilter"
@click-refresh="handleRefresh"
item-text="Produk"
>
<template #default="{ item }">
<div class="card p-4">
<h3>{{ item.name }}</h3>
<p class="text-sm text-subtitle">{{ item.category }}</p>
</div>
</template>
</PaginatorContent>
<!-- Filter Modal -->
<FilterModal
v-model="showFilterModal"
:filter="container.filter"
@apply="applyFilter"
/>
</template>Key Takeaways:
- Provide event listeners to show action buttons (
@click-create,@click-filter,@click-refresh) - Vue automatically create corresponding props (
onClickCreate, etc.) for conditional rendering - No need to provide
:onClickXxxprops explicitly - Create button: navigate to create page
- Filter button: toggle filter modal/sidebar
- Refresh button: re-fetch current data
setFilter()untuk update individual filterapplyFilter()untuk commit filter changes- Error state auto-show retry button dengan refresh event handler
4. Display Mode Variations
Variations: info-only, toggle-only, without manual page input.
<script setup lang="ts">
import { PaginatorContent } from '@bpmlib/vue-sapaginator';
import { useDataContainer } from '@bpmlib/utils-data-container';
const container = useDataContainer({ /* ... */ });
</script>
<template>
<!-- Standard: info + pagination controls -->
<PaginatorContent :container="container">
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</PaginatorContent>
<!-- Info only: hide pagination controls -->
<PaginatorContent
:container="container"
only-info
>
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</PaginatorContent>
<!-- Toggle only: hide info section -->
<PaginatorContent
:container="container"
only-toggle
>
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</PaginatorContent>
<!-- Without manual page input dropdown -->
<PaginatorContent
:container="container"
without-manual-toggle
>
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</PaginatorContent>
<!-- Disable navigation buttons -->
<PaginatorContent
:container="container"
:disable-next="true"
:disable-prev="true"
>
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</PaginatorContent>
</template>Key Takeaways:
only-info- Show data count/search info onlyonly-toggle- Show page controls onlywithout-manual-toggle- Hide page number inputdisable-next/disable-prev- Disable navigation buttons- Combine props untuk custom layouts
5. Pagination State Management
Manual control dengan page events dan DataContainer auto-management.
<script setup lang="ts">
import { ref } from 'vue';
import { PaginatorContent } from '@bpmlib/vue-sapaginator';
import { useDataContainer } from '@bpmlib/utils-data-container';
interface Product {
id: number;
name: string;
}
// === Manual Mode ===
const manualProducts = ref<Product[]>([]);
const manualCurrentPage = ref(1);
const manualMaxPage = ref(10);
const fetchManualData = async (page: number) => {
const response = await fetch(`/api/products?page=${page}`);
const data = await response.json();
manualProducts.value = data.items;
manualCurrentPage.value = data.currentPage;
manualMaxPage.value = data.maxPage;
};
// Single handler untuk all page events
const handlePageChange = (page: number) => {
console.log('Page changed to:', page);
fetchManualData(page);
};
// === DataContainer Mode ===
const container = useDataContainer<Product>(
{ /* filter */ },
{ pagination: { mode: 'lengthaware', perPage: 20 } }
);
const fetchContainerData = async () => {
container.loadStatus = 0;
try {
// DataContainer auto-include current page di URL
const params = container.getterUrlStringAttribute();
const response = await fetch(`/api/products?${params}`);
const data = await response.json();
container.mapFromResponse(data);
} catch (error) {
container.setError(error.message, 500);
}
};
// Override auto page management
const handleCustomToggle = async (page: number) => {
console.log('Custom page logic:', page);
// Manual update container page
container.setPage({
numeric: { current: page, max: container.page.numeric.max }
});
// Fetch with new page
await fetchContainerData();
};
// === Event-based Manual Mode ===
const eventProducts = ref<Product[]>([]);
const eventCurrentPage = ref(1);
const handlePrevPage = (page: number) => {
console.log('Prev to:', page);
fetchManualData(page);
};
const handleNextPage = (page: number) => {
console.log('Next to:', page);
fetchManualData(page);
};
const handleManualPageInput = (page: number) => {
console.log('Jump to:', page);
fetchManualData(page);
};
</script>
<template>
<!-- Manual mode dengan onPageToggle (recommended) -->
<PaginatorContent
:data="manualProducts"
:current-page="manualCurrentPage"
:max-page="manualMaxPage"
:amount-shown="manualProducts.length"
:amount-total="95"
:onPageToggle="handlePageChange"
>
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</PaginatorContent>
<!-- Manual mode dengan separate events -->
<PaginatorContent
:data="eventProducts"
:current-page="eventCurrentPage"
:max-page="10"
@prevPage="handlePrevPage"
@nextPage="handleNextPage"
@manualPage="handleManualPageInput"
>
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</PaginatorContent>
<!-- DataContainer auto-management -->
<PaginatorContent :container="container">
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</PaginatorContent>
<!-- DataContainer dengan custom toggle override -->
<PaginatorContent
:container="container"
:onPageToggle="handleCustomToggle"
>
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</PaginatorContent>
</template>Key Takeaways:
- Manual mode: use
onPageToggleuntuk single handler (cleaner) - Manual mode alternative: use separate
@prevPage,@nextPage,@manualPageevents - DataContainer: auto page management (no event handling needed)
- DataContainer override: use
onPageToggleuntuk custom logic onPageToggleprop suppress default events
6. Error Handling Patterns
Error states dengan DataContainer dan manual mode.
<script setup lang="ts">
import { ref } from 'vue';
import { PaginatorContent } from '@bpmlib/vue-sapaginator';
import { useDataContainer } from '@bpmlib/utils-data-container';
interface Product {
id: number;
name: string;
}
// === DataContainer Mode ===
const container = useDataContainer<Product>(
{ /* filter */ },
{ pagination: { mode: 'lengthaware', perPage: 20 } }
);
const fetchData = async () => {
// Set loading state
container.loadStatus = 0;
try {
const params = container.getterUrlStringAttribute();
const response = await fetch(`/api/products?${params}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// mapFromResponse() auto set loadStatus to 1
container.mapFromResponse(data);
} catch (error) {
// setError() auto set loadStatus to -1
container.setError(
error.message || 'Terjadi kesalahan saat memuat data',
error.status || 500
);
}
};
// Clear error manually
const clearError = () => {
container.clearError(); // Set error to null, loadStatus to 0
};
// === Manual Mode ===
const manualProducts = ref<Product[]>([]);
const manualLoadStatus = ref<number>(0); // -1: error, 0: loading, 1: loaded
const manualError = ref<string>('');
const fetchManual = async () => {
manualLoadStatus.value = 0;
try {
const response = await fetch('/api/products');
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.statusText}`);
}
const data = await response.json();
manualProducts.value = data.items;
manualLoadStatus.value = 1;
} catch (error) {
manualLoadStatus.value = -1;
manualError.value = error.message;
}
};
// Retry handler
const retryFetch = async () => {
manualError.value = '';
await fetchManual();
};
// Initial load
fetchData();
</script>
<template>
<!-- DataContainer mode - auto error handling -->
<PaginatorContent
:container="container"
@click-refresh="fetchData"
item-text="Produk"
>
<template #default="{ item }">
<div class="card p-4">
<h3>{{ item.name }}</h3>
</div>
</template>
</PaginatorContent>
<!-- Manual mode - explicit error props -->
<PaginatorContent
:data="manualProducts"
:load-status="manualLoadStatus"
:error-message="manualError"
@click-refresh="retryFetch"
item-text="Produk"
>
<template #default="{ item }">
<div class="card p-4">
<h3>{{ item.name }}</h3>
</div>
</template>
</PaginatorContent>
<!-- Custom error handling -->
<div v-if="container.hasError">
<p class="text-red-500">{{ container.error?.message }}</p>
<button @click="clearError">Clear Error</button>
</div>
</template>Key Takeaways:
- DataContainer auto-manages error state via
setError() container.loadStatusdirect access (NEW v0.1.1: no.value!)- Load status: -1 = error, 0 = loading, 1 = loaded
- Provide
@click-refreshevent listener untuk show refresh button - Error state auto-show retry button yang trigger same event handler
- Manual mode requires explicit
loadStatusdanerrorMessageprops clearError()untuk reset error state- Component auto-render loading card, error card, atau data berdasarkan loadStatus
Styling
Import CSS
Library menyediakan minimal CSS untuk animations dan transitions:
// main.ts atau app.ts
import '@bpmlib/vue-sapaginator/style.css';Disediakan:
- Transition animations (fade, slide)
- Focus state animations
- Visibility toggles
- Loading skeleton animations
TIDAK Disediakan:
- Colors, spacing, typography
- Layout structure
- Component appearance (cards, buttons, inputs)
WARNING
Styling terbatas. Tailwind CSS + parent project classes diperlukan untuk tampilan lengkap.
Expected Classes dari Parent Project
Component menggunakan class names berikut yang harus didefinisikan di parent project:
Interactive Elements
.btn- Base button styling.btn-group- Button group container (flex gap).card- Card/panel wrapper dengan background dan border
Form Elements
.input- Form input base styling (used by internal FormInput)- Form input states managed internally
Layout
.all-center- Flex center alignment utility (justify-center items-center).text-ld- Light/dark adaptive text color.text-subtitle- Subtitle/muted text color.grid,.flex- Standard Tailwind utilities
Internal Components
Component menggunakan internal sub-components dengan expected classes:
BasicButton- Expects.btn, color variantsBasicCard- Expects.cardFormInput- Expects.input, label stylingPageButton- Custom button untuk page numbers
NON-FAMILY PROJECT
Jika menggunakan di luar family project, definisikan class-class ini dengan styling project kamu sendiri. Component hanya butuh class names ada, tidak peduli implementasinya.
PENTING
Tanpa class definitions ini, component akan render tanpa styling (unstyled). Minimal berikan basic styling untuk class-class yang digunakan component.
Minimum Required Classes:
.card { /* background, border, padding */ }
.btn { /* button base styles */ }
.btn-group { /* flex gap for buttons */ }
.input { /* input field styles */ }
.all-center { /* flex center utility */ }
.text-subtitle { /* muted text color */ }