Skip to content

PaginatorContent (@bpmlib/vue-sapaginator)

Vue 3 pagination component dengan DataContainer support dan flexible rendering options

Versi: 0.1.0
Kategori: UI Component (Vue 3)

npm versionTypeScriptVueTailwind CSS

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:

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

Types:

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

Installation & Setup

Requirements

Peer Dependencies

Library ini memerlukan peer dependencies berikut:

Wajib:

bash
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.0

Optional:

bash
npm install @bpmlib/utils-data-container@^0.1.1
DependencyVersiStatusDeskripsi
vue^3.4.0RequiredVue 3 framework
@fortawesome/fontawesome-svg-core^6.0.0RequiredFontAwesome core
@fortawesome/free-solid-svg-icons^6.0.0RequiredFontAwesome icons
@fortawesome/vue-fontawesome^3.0.0RequiredFontAwesome Vue component
floating-vue^5.0.0RequiredTooltip library
@bpmlib/utils-data-container^0.1.1OptionalUntuk DataContainer mode
tailwindcss^3.0.0OptionalUntuk styling lengkap

Package Installation

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

Import

Component Import:

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

Type Import:

ts
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:

vue
<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:

vue
<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 response
  • getterUrlStringAttribute() 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:

Mode Detection Logic:

Component internally check props.container untuk determine mode:

ts
// 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:

ts
// 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 Retry

State 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:

vue
<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:

ts
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:

ts
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:

ts
// 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:

ts
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 number
  • max_page - Total pages
  • total - Total items count
  • per_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:

ts
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:

ts
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:

ts
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:

vue
<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:

vue
<template>
  <PaginatorContent
    v-model="searchValue"
    :onSubmitSearch="handleSearch"
  />
</template>

Internally, component define model:

ts
const searchVal = defineModel<string>();

Search Bar Visibility Logic:

Search bar hanya muncul jika parent provide @submit-search event listener:

vue
<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:

ts
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
vue
<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:

vue
<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:

ts
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();
};
vue
<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:

ts
<PaginatorContent<Product> :container="container">
  <template #default="{ item }">
    <!-- item is typed as Product -->
  </template>
</PaginatorContent>
Props
NameTypeDefaultDescription
containerDataContainerObject<TData>-DataContainer object untuk auto state management Lihat selengkapnya
amountShownstring | number'0'Jumlah items yang ditampilkan (manual mode)
amountTotalstring | number'0'Total items available (manual mode)
currentPagestring | number'0'Current page number (manual mode)
maxPagestring | number'0'Maximum page number (manual mode)
loadStatusnumber0Loading status: -1 = error, 0 = loading, 1 = loaded (manual mode)
errorMessagestring-Error message saat loadStatus = -1 (manual mode)
dataTData[]-Array of data items untuk display (manual mode)
itemTextstring'Data'Label untuk item type (e.g., "Produk", "User")
disableNextbooleanfalseDisable next page button
disablePrevbooleanfalseDisable previous page button
withoutManualTogglebooleanfalseHide manual page number input dropdown
onlyInfobooleanfalseShow only info section, hide pagination controls
onlyTogglebooleanfalseShow only pagination controls, hide info section
appliedSearchstring-Currently applied search query (manual mode)
listClassstring'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:

ts
import { useDataContainer } from '@bpmlib/utils-data-container';

const container = useDataContainer<Product, ProductFilter>(
  { category: '' },
  {
    pagination: { mode: 'lengthaware', perPage: 20 }
  }
);
vue
<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+)

See: DataContainer Overview

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:

vue
<!-- ✅ 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:

ts
onPageToggle: (page: number) => void

Parameters:

  • page - Target page number yang akan diaktifkan

Contoh:

vue
<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 pageToggle event instead of prevPage, nextPage, manualPage
  • DataContainer mode: callback override auto page management
  • Manual mode: callback required untuk handle page changes

Model
NameTypeDefaultDescription
v-modelstring''Two-way binding untuk search input value

Contoh:

vue
<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
NamePayloadDescription
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:

ts
page: number // Target page number

Contoh:

vue
<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:

ts
page: number // Target page number

Contoh:

vue
<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:

ts
page: number // Target page number

Contoh:

vue
<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:

ts
page: number // Target page number

Contoh:

vue
<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 onPageToggle prop provided
  • Replace prevPage, nextPage, dan manualPage events
  • Recommended pattern untuk cleaner code

Slots
NamePropsDescription
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:

vue
<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:

vue
<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):

vue
<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.

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;
}

PaginatorEmits

Events interface untuk PaginatorContent component.

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

Examples

Contains:


1. Custom List Layouts

Demonstrasi custom rendering dengan customList slot dan custom listClass.

vue
<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:

  • listClass prop untuk custom grid/flex layout
  • Default slot untuk item template dengan existing layout
  • customList slot untuk completely custom list structure
  • Both approaches support responsive design

2. Search Integration

Search dengan DataContainer integration dan manual mode.

vue
<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.search untuk trigger search state
  • setPage() untuk reset pagination ke page 1
  • v-model untuk bind search input value
  • appliedSearch prop display current active search
  • Provide @submit-search event 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.

vue
<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 :onClickXxx props explicitly
  • Create button: navigate to create page
  • Filter button: toggle filter modal/sidebar
  • Refresh button: re-fetch current data
  • setFilter() untuk update individual filter
  • applyFilter() 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.

vue
<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 only
  • only-toggle - Show page controls only
  • without-manual-toggle - Hide page number input
  • disable-next/disable-prev - Disable navigation buttons
  • Combine props untuk custom layouts

5. Pagination State Management

Manual control dengan page events dan DataContainer auto-management.

vue
<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 onPageToggle untuk single handler (cleaner)
  • Manual mode alternative: use separate @prevPage, @nextPage, @manualPage events
  • DataContainer: auto page management (no event handling needed)
  • DataContainer override: use onPageToggle untuk custom logic
  • onPageToggle prop suppress default events

6. Error Handling Patterns

Error states dengan DataContainer dan manual mode.

vue
<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.loadStatus direct access (NEW v0.1.1: no .value!)
  • Load status: -1 = error, 0 = loading, 1 = loaded
  • Provide @click-refresh event listener untuk show refresh button
  • Error state auto-show retry button yang trigger same event handler
  • Manual mode requires explicit loadStatus dan errorMessage props
  • 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:

ts
// 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 variants
  • BasicCard - Expects .card
  • FormInput - Expects .input, label styling
  • PageButton - 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:

css
.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 */ }