@bpmlib/utils-data-container
Vue 3 composable untuk mengelola paginated data dengan filtering, sorting, dan search capabilities
Versi: 0.1.0
Kategori: Framework Utils (Vue 3)
TL;DR
Composable untuk manage state paginated data dengan support multiple pagination modes (length-aware, cursor-based, simple), advanced filtering dengan nested paths, sorting, dan search. Menyediakan separation between draft dan applied filters, auto-detection pagination mode dari API response, dan URL parameter generation untuk seamless integration dengan backend APIs.
Composables:
import { useDataContainer } from '@bpmlib/utils-data-container';Types:
import type {
DataContainerObject,
PageConfig,
SortConfig,
SearchConfig,
ErrorInfo,
UrlParams,
PaginationMode,
} from '@bpmlib/utils-data-container';Installation & Setup
Requirements
Peer Dependencies
Library ini memerlukan peer dependencies berikut:
Wajib:
npm install vue@^3.3.0| Dependency | Versi | Status | Deskripsi |
|---|---|---|---|
vue | ^3.3.0 | Required | Vue 3 framework |
Package Installation
npm install @bpmlib/utils-data-containeryarn add @bpmlib/utils-data-containerpnpm add @bpmlib/utils-data-containerbun install @bpmlib/utils-data-containerImport
Basic Import:
import { useDataContainer } from '@bpmlib/utils-data-container';With Types:
import { useDataContainer } from '@bpmlib/utils-data-container';
import type {
DataContainerObject,
PageConfig,
PaginationMode
} from '@bpmlib/utils-data-container';Quick Start
Basic Usage
Contoh paling sederhana untuk manage paginated data:
<script setup lang="ts">
import { useDataContainer } from '@bpmlib/utils-data-container';
interface User {
id: number;
name: string;
email: string;
}
interface UserFilter {
status?: string;
role?: string;
}
const container = useDataContainer<User, UserFilter>({
status: 'active',
role: ''
});
async function fetchUsers() {
container.loadStatus.value = 0; // Set loading
const response = await fetch('/api/users?' + container.getterUrlStringAttribute());
const data = await response.json();
container.mapFromResponse(data);
}
fetchUsers();
</script>
<template>
<div>
<div v-if="container.isLoading.value">Loading...</div>
<div v-else-if="container.hasError.value">{{ container.error.value?.message }}</div>
<div v-else>
<div v-for="user in container.data.value" :key="user.id">
{{ user.name }} - {{ user.email }}
</div>
</div>
</div>
</template>Key Points:
useDataContainermengelola reactive state untuk data, filter, pagination, sort, dan searchgetterUrlStringAttribute()generate URL query string dari current statemapFromResponse()auto-detect pagination mode dan map response ke container
Comprehensive Example
Full-featured example dengan filtering, pagination, dan sorting:
<script setup lang="ts">
import { useDataContainer } from '@bpmlib/utils-data-container';
interface Product {
id: number;
name: string;
price: number;
category: string;
}
interface ProductFilter {
category?: string;
minPrice?: number;
maxPrice?: number;
}
const container = useDataContainer<Product, ProductFilter>({
category: '',
minPrice: 0,
maxPrice: 0
});
async function fetchProducts() {
container.loadStatus.value = 0;
try {
const response = await fetch('/api/products?' + container.getterUrlStringAttribute());
const data = await response.json();
container.mapFromResponse(data);
} catch (error) {
container.setError('Failed to fetch products', 'FETCH_ERROR');
}
}
function handleFilterChange() {
if (container.hasUnappliedFilterChanges()) {
container.applyFilter();
fetchProducts();
}
}
function handleSort(field: string) {
const currentDir = container.sort.sortDir;
container.setSort(field, currentDir === 'a' ? 'd' : 'a');
fetchProducts();
}
function handlePageChange(page: number) {
container.setPage({
...container.page,
numeric: { ...container.page.numeric, current: page }
});
fetchProducts();
}
fetchProducts();
</script>
<template>
<div>
<!-- Filters -->
<div class="filters">
<input v-model="container.filter.category" @change="handleFilterChange" placeholder="Category">
<input v-model.number="container.filter.minPrice" @change="handleFilterChange" placeholder="Min Price">
<input v-model.number="container.filter.maxPrice" @change="handleFilterChange" placeholder="Max Price">
<button @click="container.resetFilterToInit">Reset</button>
</div>
<!-- Search -->
<input v-model="container.search" @change="fetchProducts" placeholder="Search products...">
<!-- Sort -->
<div class="sort">
<button @click="handleSort('name')">Sort by Name</button>
<button @click="handleSort('price')">Sort by Price</button>
</div>
<!-- Data -->
<div v-if="container.isLoading.value">Loading...</div>
<div v-else-if="container.hasError.value">{{ container.error.value?.message }}</div>
<div v-else-if="container.isEmpty.value">No products found</div>
<div v-else>
<div v-for="product in container.data.value" :key="product.id">
{{ product.name }} - ${{ product.price }} ({{ product.category }})
</div>
</div>
<!-- Pagination -->
<div v-if="container.page.paginationMode === 'lengthaware'" class="pagination">
<button
v-for="page in container.page.numeric.max"
:key="page"
@click="handlePageChange(page)"
:disabled="page === container.page.numeric.current"
>
{{ page }}
</button>
<p>Total: {{ container.page.totalData }} items</p>
</div>
</div>
</template>Core Concepts
Library ini didesain dengan beberapa konsep fundamental yang mempengaruhi cara kamu menggunakannya.
Lazy State Management & Mutable Filter Pattern
Filter state di-manage dalam dua layer: draft (filter) dan applied (_appliedFilter). System membedakan antara draft filter (user sedang input) dan applied filter (yang benar-benar digunakan untuk fetch).
How it works:
filter- Mutable reactive object yang bisa langsung di-bind ke form input_appliedFilter- Readonly snapshot dari last applied filter- Perubahan pada
filtertidak otomatis trigger fetch - kamu kontrol kapan apply applyFilter()copyfilterke_appliedFilteryang digunakan untuk URL generationhasUnappliedFilterChanges()check apakah ada pending changes- Pattern ini mencegah accidental fetch setiap user ketik di form
Example:
// User input langsung update filter (tidak trigger fetch)
container.filter.status = 'active';
container.filter.role = 'admin';
// Check apakah ada perubahan yang belum di-apply
if (container.hasUnappliedFilterChanges()) {
// Apply filter dan fetch
container.applyFilter();
fetchData();
}
// Reset ke applied state (discard draft)
container.resetFilterToApplied();
// Reset ke initial state
container.resetFilterToInit();Use Case: Form dengan "Apply" button, mencegah accidental fetch, implement filter preview, atau dirty state tracking.
See: Filter Methods, Example 2
Pagination Mode Auto-Detection
Library auto-detect pagination mode berdasarkan response structure, tanpa perlu manual configuration.
How it works:
- Length-Aware: Detected jika response punya
max_page+total - Cursor: Detected jika response punya
next_cursoratauprevious_cursor - Simple: Detected jika response punya
has_more - None: Default jika tidak ada pagination metadata
Detection Logic:
// Backend response dengan max_page + total → Length-Aware
{
content: [...],
current_page: 1,
max_page: 10,
total: 95,
per_page: 10
}
// Backend response dengan cursors → Cursor-based
{
content: [...],
next_cursor: 'token123',
previous_cursor: null,
has_more: true,
per_page: 20
}
// Backend response dengan has_more only → Simple
{
content: [...],
current_page: 2,
has_more: true,
per_page: 25
}See: PageConfig Type, Example 3
URL Parameter Generation
Library generate URL query parameters dari current state untuk seamless backend integration.
How it works:
getterObjectAttribute()- Return plain object untuk axios/fetch paramsgetterUrlStringAttribute()- Return URL-encoded string untuk direct append- Nested filters di-encode sebagai
filter[key][nested]=value - Empty values otomatis di-skip (tidak masuk URL)
Generated Format:
// Container state
container.setFilter('status', 'active');
container.setFilter('nested.category', 'tech');
container.setSort('name', 'a');
container.search = 'laptop';
container.setPage({ paginationMode: 'lengthaware', numeric: { current: 2 }, perPage: 20 });
// Object output
container.getterObjectAttribute();
// {
// page: 2,
// perPage: 20,
// sortBy: 'name',
// sortDir: 'asc',
// filter: { status: 'active', nested: { category: 'tech' } },
// q: 'laptop'
// }
// String output
container.getterUrlStringAttribute();
// "page=2&perPage=20&sortBy=name&sortDir=asc&filter[status]=active&filter[nested][category]=tech&q=laptop"See: URL Generation Methods, Example 1
API Reference
Composables
useDataContainer
Composable untuk manage paginated data state dengan support filtering, sorting, search, dan multiple pagination modes.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
initialFilter | TFilter | {} | Initial filter values yang akan digunakan saat reset |
Generic Types:
TData- Type of individual data items in arrayTFilter- Type of filter object structureTAdditional- Type of additional data dari response (metadata, stats, dll)
Returns
Contains:
- Reactive State
- Computed Properties
- Data Methods
- Filter Methods
- Search Methods
- Pagination Methods
- Sort Methods
- Error Methods
- URL Generation Methods
- Utility Methods
Reactive State
data
data: Ref<TData[]>Array of data items yang di-fetch dari API. Menggunakan shallowRef untuk performance.
⚠️ Penting - Penggunaan .value:
- Di
<script>: Perlu.value→container.data.value - Di
<template>: Tidak perlu.value(auto-unwrap) →container.data
filter
filter: Reactive<TFilter>Draft filter state yang bisa langsung di-bind ke form inputs. Perubahan tidak otomatis trigger fetch.
page
page: PageConfigReactive pagination configuration object. Contains numeric (page numbers), cursor (tokens), perPage, hasMore, totalData, dan paginationMode.
See: PageConfig Type, setPage()
sort
sort: SortConfigReactive sort configuration object. Contains sortBy (field name) dan sortDir ('a' or 'd').
See: SortConfig Type, setSort()
loadStatus
loadStatus: Ref<-1 | 0 | 1>Loading status indicator: -1 (error), 0 (loading), 1 (loaded successfully).
error
error: Ref<ErrorInfo | null>Error information object dengan message dan code. Null jika tidak ada error.
See: ErrorInfo Type, setError(), clearError()
additionalData
additionalData: Ref<TAdditional | null>Additional data dari API response (metadata, statistics, dll). Menggunakan shallowRef untuk performance. Populated dari appended field di response.
search
search: stringSearch value dengan getter/setter. Tidak perlu .value untuk access. Value akan masuk URL params dengan parameter name dari searchParam (default: 'q').
See: setSearchParam(), getSearchParam()
Computed Properties
isLoading
isLoading: ComputedRef<boolean>Computed property, true ketika loadStatus === 0.
isLoaded
isLoaded: ComputedRef<boolean>Computed property, true ketika loadStatus === 1.
hasError
hasError: ComputedRef<boolean>Computed property, true ketika loadStatus === -1 atau error !== null.
isEmpty
isEmpty: ComputedRef<boolean>Computed property, true ketika data.length === 0.
Data Methods
appendData()
appendData: (item: TData) => voidAppend single item ke data array.
Use Case: Optimistic updates, manual data addition, real-time updates dari websocket.
deleteDataByIndex()
deleteDataByIndex: (index: number) => booleanDelete item dari data array by index. Returns true jika berhasil, false jika index invalid.
Use Case: Optimistic deletes, manual data removal.
dataLength()
dataLength: () => numberGet current length of data array.
Use Case: Display counts, validation, conditional logic.
Filter Methods
setFilter()
setFilter: (path: string, value: unknown) => voidSet filter value dengan support nested paths menggunakan dot notation (e.g., 'status', 'nested.category').
Use Case: Programmatic filter updates, complex filter logic, conditional filters.
See: Example 2
applyFilter()
applyFilter: () => voidCopy current filter state ke _appliedFilter dan reset page ke 1. Call ini sebelum fetch data.
Use Case: "Apply" button on filter form, filter submission, batch filter updates.
resetFilterToInit()
resetFilterToInit: () => voidReset filter ke initialFilter values yang diberikan saat initialization.
Use Case: "Reset" button, clear all filters, return to default state.
resetFilterToApplied()
resetFilterToApplied: () => voidReset filter ke last _appliedFilter state (discard draft changes).
Use Case: "Cancel" button, discard changes, revert to last applied.
hasUnappliedFilterChanges()
hasUnappliedFilterChanges: () => booleanCheck apakah ada pending changes di filter yang belum di-apply. Returns true jika ada changes.
Use Case: Enable/disable apply button, dirty state tracking, unsaved changes warning.
Search Methods
setSearchParam()
setSearchParam: (param: string) => voidSet custom search parameter name untuk URL generation (default: 'q').
Use Case: Match backend API expectations, custom search field names.
See: Example 4
getSearchParam()
getSearchParam: () => stringGet current search parameter name.
Pagination Methods
setPage()
setPage: (config: Partial<PageConfig> | null) => voidUpdate pagination configuration. Pass null untuk reset ke default.
Use Case: Page navigation, change items per page, cursor pagination handling.
See: PageConfig Type, Example 3
Sort Methods
setSort()
setSort: (sortBy: string, sortDir?: 'a' | 'd') => voidSet sort configuration. sortDir: 'a' (ascending) or 'd' (descending). Default: 'a'.
Use Case: Column sorting, sort direction toggle, default sort order.
Error Methods
setError()
setError: (message: string, code: number | string) => voidSet error state manually. Otomatis set loadStatus ke -1.
Use Case: Manual error handling, custom error scenarios, API error mapping.
See: Example 7
clearError()
clearError: () => voidClear error state (set error ke null).
Use Case: Retry logic, error dismissal, state cleanup.
URL Generation Methods
getterObjectAttribute()
getterObjectAttribute: () => UrlParamsGenerate URL parameters sebagai plain object untuk axios/fetch. Returns object containing page, perPage, sortBy, sortDir, filter, dan search params.
Use Case: Axios requests, complex param handling, debugging URLs.
getterUrlStringAttribute()
getterUrlStringAttribute: () => stringGenerate URL query string untuk direct append ke URL. Returns URL-encoded string (without leading ?).
Use Case: Direct fetch URLs, URL construction, browser navigation.
See: URL Parameter Generation, Example 1
Utility Methods
reset()
reset: () => voidReset semua state ke default values. Data di-restore ke last successful fetch, loadStatus = 0, filters reset, search cleared, pagination/sort reset, error cleared.
Use Case: Clear all filters and state, return to initial state, hard reset.
mapFromResponse()
mapFromResponse: (
response: {
success?: boolean;
message?: string;
code?: string | number;
content?: TData[] | TData | unknown;
appended?: TAdditional;
max_page?: number | null;
current_page?: number | null;
total?: number | null;
next_cursor?: string | null;
previous_cursor?: string | null;
per_page?: number | null;
has_more?: boolean | null;
},
mapper?: ((item: unknown) => TData) | null
) => voidMap API response ke container state dengan auto-detection pagination mode. success: false akan trigger error state. content bisa array atau single object. mapper untuk transform setiap item.
Automatic Pagination Detection:
- Length-Aware (has
max_page+total) →paginationMode = 'lengthaware' - Cursor (has
next_cursororprevious_cursor) →paginationMode = 'cursor' - Simple (has
has_moreonly) →paginationMode = 'simple' - None (no pagination metadata) →
paginationMode = 'none'
Use Case: Map API responses, transform data, handle pagination automatically, error handling.
See: Pagination Mode Auto-Detection, Example 5
Types
DataContainerObject
interface DataContainerObject<
TData = unknown,
TFilter extends Record<string, unknown> = Record<string, unknown>,
TAdditional = unknown
>Return type dari useDataContainer composable. Contains all reactive state, computed properties, dan methods.
Generic Parameters:
TData- Type of individual data itemsTFilter- Type of filter object structureTAdditional- Type of additional response data
See: Complete property descriptions di API Reference - useDataContainer
PageConfig
interface PageConfig {
numeric: {
current: number | null;
max: number | null;
};
cursor: {
next: string | null;
prev: string | null;
};
perPage: number | null;
hasMore: boolean | null;
totalData: number | null;
paginationMode: PaginationMode;
isFrontEndPreferCursor: boolean;
}Pagination configuration object.
Properties:
numeric.current- Current page number (length-aware/simple)numeric.max- Total pages (length-aware)cursor.next- Next page cursor token (cursor-based)cursor.prev- Previous page cursor token (cursor-based)perPage- Items per pagehasMore- More data available (cursor/simple)totalData- Total count of all items (length-aware)paginationMode- Current pagination modeisFrontEndPreferCursor- Force cursor mode (useCursor=1in URL)
See: PaginationMode Type, Pagination Mode Auto-Detection
SortConfig
interface SortConfig {
sortDir: 'a' | 'd';
sortBy: string;
}Sort configuration object.
Properties:
sortDir- Sort direction:'a'(ascending) or'd'(descending)sortBy- Field name to sort by (empty string = no sort)
SearchConfig
interface SearchConfig {
searchParam: string;
searchValue: string;
}Search configuration object (internal).
Properties:
searchParam- Parameter name untuk URL (default:'q')searchValue- Current search value
Note: Access via container.search getter/setter dan getSearchParam() method.
ErrorInfo
interface ErrorInfo {
message: string;
code: number | string;
}Error information object dengan human-readable message dan error code (numeric status atau string identifier).
UrlParams
interface UrlParams {
perPage?: number;
page?: number;
cursor?: string;
useCursor?: number;
sortBy?: string;
sortDir?: string;
filter?: Record<string, string | string[]>;
[key: string]: unknown;
}URL parameters object structure. Contains pagination, sort, filter, dan dynamic search params.
PaginationMode
type PaginationMode = 'lengthaware' | 'cursor' | 'simple' | 'none';Pagination mode types.
Values:
'lengthaware'- Classic pagination dengan total pages (hasmax_page+total)'cursor'- Cursor-based dengan tokens (hasnext_cursor/previous_cursor)'simple'- Simple dengan "has more" indicator (hashas_more)'none'- No pagination
See: Pagination Mode Auto-Detection
Examples
Contains:
- 1. Basic Data Fetching with Pagination
- 2. Advanced Filtering with Nested Paths
- 3. Multiple Pagination Modes
- 4. Search with Custom Parameter
- 5. Response Mapping with Custom Transformer
- 6. Complete CRUD Operations
- 7. Error Handling Patterns
1. Basic Data Fetching with Pagination
Complete example untuk fetch paginated data dengan length-aware pagination.
<script setup lang="ts">
import { useDataContainer } from '@bpmlib/utils-data-container';
interface Product {
id: number;
name: string;
price: number;
}
const container = useDataContainer<Product>();
async function fetchProducts() {
container.loadStatus.value = 0;
try {
const response = await fetch('/api/products?' + container.getterUrlStringAttribute());
const data = await response.json();
container.mapFromResponse(data);
} catch (error) {
container.setError('Failed to fetch products', 'FETCH_ERROR');
}
}
function goToPage(page: number) {
container.setPage({
...container.page,
numeric: { ...container.page.numeric, current: page }
});
fetchProducts();
}
fetchProducts();
</script>
<template>
<div>
<div v-if="container.isLoading.value">Loading...</div>
<div v-else-if="container.hasError.value" class="error">
{{ container.error.value?.message }}
<button @click="fetchProducts">Retry</button>
</div>
<div v-else-if="container.isEmpty.value">No products found</div>
<div v-else>
<div v-for="product in container.data.value" :key="product.id" class="product">
<h3>{{ product.name }}</h3>
<p>${{ product.price }}</p>
</div>
<!-- Pagination -->
<div v-if="container.page.paginationMode === 'lengthaware'" class="pagination">
<button
v-for="page in container.page.numeric.max"
:key="page"
@click="goToPage(page)"
:disabled="page === container.page.numeric.current"
>
{{ page }}
</button>
<p>Total: {{ container.page.totalData }} products</p>
</div>
</div>
</div>
</template>Key Takeaways:
getterUrlStringAttribute()automatically builds query string dengan pagination paramsmapFromResponse()auto-detect pagination mode dari response structure- Use
isLoading,hasError,isEmptyuntuk clean conditional rendering
2. Advanced Filtering with Nested Paths
Demonstrates filtering dengan nested objects dan apply/reset functionality.
<script setup lang="ts">
import { useDataContainer } from '@bpmlib/utils-data-container';
interface Product {
id: number;
name: string;
price: number;
}
interface ProductFilter {
status?: string;
pricing?: {
min?: number;
max?: number;
};
tags?: string[];
}
const container = useDataContainer<Product, ProductFilter>({
status: '',
pricing: { min: 0, max: 0 },
tags: []
});
async function fetchProducts() {
container.loadStatus.value = 0;
const response = await fetch('/api/products?' + container.getterUrlStringAttribute());
const data = await response.json();
container.mapFromResponse(data);
}
function handleApplyFilters() {
if (container.hasUnappliedFilterChanges()) {
container.applyFilter();
fetchProducts();
}
}
function handleResetFilters() {
container.resetFilterToInit();
container.applyFilter();
fetchProducts();
}
fetchProducts();
</script>
<template>
<div>
<!-- Filters Form -->
<div class="filters">
<select v-model="container.filter.status">
<option value="">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
<!-- Nested filters -->
<input
v-model.number="container.filter.pricing.min"
type="number"
placeholder="Min Price"
>
<input
v-model.number="container.filter.pricing.max"
type="number"
placeholder="Max Price"
>
<!-- Array filter -->
<div>
<label>
<input type="checkbox" value="sale" v-model="container.filter.tags">
On Sale
</label>
<label>
<input type="checkbox" value="new" v-model="container.filter.tags">
New Arrival
</label>
</div>
<!-- Actions -->
<button
@click="handleApplyFilters"
:disabled="!container.hasUnappliedFilterChanges()"
>
Apply Filters
</button>
<button @click="handleResetFilters">Reset</button>
<button
@click="container.resetFilterToApplied"
:disabled="!container.hasUnappliedFilterChanges()"
>
Cancel
</button>
</div>
<!-- Data display -->
<div v-if="container.isLoaded.value">
<div v-for="product in container.data.value" :key="product.id">
{{ product.name }} - ${{ product.price }}
</div>
</div>
</div>
</template>Key Takeaways:
setFilter()dengan dot notation untuk nested paths:setFilter('pricing.min', 100)hasUnappliedFilterChanges()untuk enable/disable apply buttonresetFilterToInit()vsresetFilterToApplied()untuk different reset behaviors- Filters tidak otomatis trigger fetch - harus explicit
applyFilter()danfetchData()
Catatan: URL generation otomatis handle nested filters sebagai filter[pricing][min]=100&filter[pricing][max]=500
3. Multiple Pagination Modes
Shows handling different pagination modes dengan single component logic.
<script setup lang="ts">
import { useDataContainer } from '@bpmlib/utils-data-container';
interface Item {
id: number;
name: string;
}
const container = useDataContainer<Item>();
async function fetchData() {
container.loadStatus.value = 0;
const response = await fetch('/api/items?' + container.getterUrlStringAttribute());
const data = await response.json();
// Auto-detects pagination mode from response
container.mapFromResponse(data);
}
// Length-aware pagination
function goToPage(page: number) {
container.setPage({
...container.page,
numeric: { ...container.page.numeric, current: page }
});
fetchData();
}
// Cursor pagination
function goToNextPage() {
if (container.page.cursor.next) {
container.setPage({
...container.page,
cursor: { next: container.page.cursor.next, prev: null }
});
fetchData();
}
}
function goToPrevPage() {
if (container.page.cursor.prev) {
container.setPage({
...container.page,
cursor: { next: null, prev: container.page.cursor.prev }
});
fetchData();
}
}
// Simple pagination (load more)
function loadMore() {
if (container.page.hasMore) {
const nextPage = (container.page.numeric.current || 1) + 1;
container.setPage({
...container.page,
numeric: { ...container.page.numeric, current: nextPage }
});
fetchData();
}
}
fetchData();
</script>
<template>
<div>
<!-- Data -->
<div v-for="item in container.data.value" :key="item.id">
{{ item.name }}
</div>
<!-- Length-Aware Pagination -->
<div v-if="container.page.paginationMode === 'lengthaware'">
<button
v-for="page in container.page.numeric.max"
:key="page"
@click="goToPage(page)"
:disabled="page === container.page.numeric.current"
>
{{ page }}
</button>
<p>Page {{ container.page.numeric.current }} of {{ container.page.numeric.max }}</p>
</div>
<!-- Cursor Pagination -->
<div v-else-if="container.page.paginationMode === 'cursor'">
<button
@click="goToPrevPage"
:disabled="!container.page.cursor.prev"
>
Previous
</button>
<button
@click="goToNextPage"
:disabled="!container.page.cursor.next"
>
Next
</button>
<p v-if="container.page.hasMore">More data available</p>
</div>
<!-- Simple Pagination (Load More) -->
<div v-else-if="container.page.paginationMode === 'simple'">
<button
@click="loadMore"
:disabled="!container.page.hasMore"
>
Load More
</button>
<p v-if="!container.page.hasMore">No more data</p>
</div>
<!-- No Pagination -->
<div v-else-if="container.page.paginationMode === 'none'">
<p>All data loaded</p>
</div>
</div>
</template>Key Takeaways:
- Check
container.page.paginationModeuntuk render appropriate UI - Length-aware: Use
numeric.currentdannumeric.max - Cursor: Use
cursor.nextdancursor.prev - Simple: Use
hasMoreuntuk "Load More" button - Auto-detection berdasarkan response structure - tidak perlu manual configuration
4. Search with Custom Parameter
Custom search parameter name untuk match backend API expectations.
<script setup lang="ts">
import { useDataContainer } from '@bpmlib/utils-data-container';
import { watch } from 'vue';
interface Article {
id: number;
title: string;
content: string;
}
const container = useDataContainer<Article>();
// Set custom search parameter (backend expects 'keyword' instead of 'q')
container.setSearchParam('keyword');
async function fetchArticles() {
container.loadStatus.value = 0;
const response = await fetch('/api/articles?' + container.getterUrlStringAttribute());
const data = await response.json();
container.mapFromResponse(data);
}
// Debounced search
let searchTimeout: number | undefined;
watch(() => container.search, (newValue) => {
clearTimeout(searchTimeout);
if (newValue.trim()) {
searchTimeout = setTimeout(() => {
container.applyFilter(); // Reset page to 1
fetchArticles();
}, 300) as unknown as number;
} else {
container.applyFilter();
fetchArticles();
}
});
fetchArticles();
</script>
<template>
<div>
<!-- Search Input -->
<input
v-model="container.search"
type="search"
placeholder="Search articles..."
>
<p v-if="container.search">
Searching for: "{{ container.search }}"
(param: {{ container.getSearchParam() }})
</p>
<!-- Results -->
<div v-if="container.isLoading.value">Searching...</div>
<div v-else-if="container.isEmpty.value">No articles found</div>
<div v-else>
<div v-for="article in container.data.value" :key="article.id">
<h3>{{ article.title }}</h3>
<p>{{ article.content }}</p>
</div>
</div>
</div>
</template>Key Takeaways:
setSearchParam('keyword')changes URL param dari default?q=ke?keyword=getSearchParam()returns current parameter name- Watch
container.searchuntuk implement debounced search applyFilter()resets page to 1 before search (important untuk pagination)
Catatan: Generated URL akan include: ?keyword=laptop&page=1&perPage=20 instead of default ?q=laptop
5. Response Mapping with Custom Transformer
Transform API response data dengan custom mapper function.
<script setup lang="ts">
import { useDataContainer } from '@bpmlib/utils-data-container';
// Backend response type
interface UserResponse {
user_id: number;
first_name: string;
last_name: string;
email_address: string;
created: string;
}
// Frontend model type
interface User {
id: number;
fullName: string;
email: string;
createdAt: Date;
}
interface UserStats {
totalUsers: number;
activeUsers: number;
}
const container = useDataContainer<User, Record<string, never>, UserStats>();
async function fetchUsers() {
container.loadStatus.value = 0;
try {
const response = await fetch('/api/users');
const data = await response.json();
// Map with transformer
container.mapFromResponse(data, (item: unknown) => {
const raw = item as UserResponse;
return {
id: raw.user_id,
fullName: `${raw.first_name} ${raw.last_name}`,
email: raw.email_address,
createdAt: new Date(raw.created)
};
});
// Access additional data
if (container.additionalData.value) {
console.log('Total users:', container.additionalData.value.totalUsers);
console.log('Active users:', container.additionalData.value.activeUsers);
}
} catch (error) {
container.setError('Failed to fetch users', 'FETCH_ERROR');
}
}
fetchUsers();
</script>
<template>
<div>
<!-- Stats from additionalData -->
<div v-if="container.additionalData.value" class="stats">
<p>Total Users: {{ container.additionalData.value.totalUsers }}</p>
<p>Active Users: {{ container.additionalData.value.activeUsers }}</p>
</div>
<!-- Transformed data -->
<div v-if="container.isLoaded.value">
<div v-for="user in container.data.value" :key="user.id" class="user">
<h3>{{ user.fullName }}</h3>
<p>{{ user.email }}</p>
<small>Joined: {{ user.createdAt.toLocaleDateString() }}</small>
</div>
</div>
</div>
</template>Key Takeaways:
- Mapper function transform setiap item dari backend format ke frontend model
- Useful untuk rename fields, compute values, atau convert types (string → Date)
additionalDatauntuk store metadata/stats dari response (viaappendedfield)- Type safety dengan generic:
<User, Filter, UserStats>
Catatan: Backend response structure:
{
"success": true,
"content": [...],
"appended": {
"totalUsers": 150,
"activeUsers": 120
}
}6. Complete CRUD Operations
Demonstrates data manipulation methods untuk optimistic updates.
<script setup lang="ts">
import { useDataContainer } from '@bpmlib/utils-data-container';
import { ref } from 'vue';
interface Todo {
id: number;
title: string;
completed: boolean;
}
const container = useDataContainer<Todo>();
const newTodoTitle = ref('');
async function fetchTodos() {
container.loadStatus.value = 0;
const response = await fetch('/api/todos');
const data = await response.json();
container.mapFromResponse(data);
}
async function createTodo() {
if (!newTodoTitle.value.trim()) return;
try {
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ title: newTodoTitle.value })
});
const data = await response.json();
// Optimistic update - append immediately
container.appendData({
id: data.id,
title: newTodoTitle.value,
completed: false
});
newTodoTitle.value = '';
} catch (error) {
container.setError('Failed to create todo', 'CREATE_ERROR');
}
}
async function toggleTodo(index: number) {
const todo = container.data.value[index];
try {
await fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
body: JSON.stringify({ completed: !todo.completed })
});
// Update in place
container.data.value[index].completed = !todo.completed;
} catch (error) {
container.setError('Failed to update todo', 'UPDATE_ERROR');
}
}
async function deleteTodo(index: number) {
const todo = container.data.value[index];
try {
await fetch(`/api/todos/${todo.id}`, { method: 'DELETE' });
// Optimistic delete
container.deleteDataByIndex(index);
} catch (error) {
container.setError('Failed to delete todo', 'DELETE_ERROR');
// Refetch to restore
fetchTodos();
}
}
fetchTodos();
</script>
<template>
<div>
<!-- Create Form -->
<div class="create-form">
<input v-model="newTodoTitle" placeholder="New todo..." @keyup.enter="createTodo">
<button @click="createTodo">Add</button>
</div>
<!-- Todo List -->
<div v-if="container.isLoaded.value">
<p>Total: {{ container.dataLength() }} todos</p>
<div
v-for="(todo, index) in container.data.value"
:key="todo.id"
class="todo"
>
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(index)"
>
<span :class="{ completed: todo.completed }">{{ todo.title }}</span>
<button @click="deleteTodo(index)">Delete</button>
</div>
</div>
<!-- Error -->
<div v-if="container.hasError.value" class="error">
{{ container.error.value?.message }}
<button @click="container.clearError">Dismiss</button>
</div>
</div>
</template>Key Takeaways:
appendData()untuk optimistic createdeleteDataByIndex()untuk optimistic delete- Direct mutation
data.value[index]untuk updates (karena shallowRef) dataLength()untuk display count- Error handling dengan manual refetch jika optimistic update fails
7. Error Handling Patterns
Different error handling strategies dan recovery patterns.
<script setup lang="ts">
import { useDataContainer } from '@bpmlib/utils-data-container';
import { ref } from 'vue';
interface Product {
id: number;
name: string;
}
const container = useDataContainer<Product>();
const retryCount = ref(0);
const maxRetries = 3;
async function fetchProducts(silent = false) {
if (!silent) {
container.loadStatus.value = 0;
}
try {
const response = await fetch('/api/products?' + container.getterUrlStringAttribute());
if (!response.ok) {
// HTTP error
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Backend error
if (!data.success) {
throw new Error(data.message || 'Unknown error');
}
container.mapFromResponse(data);
retryCount.value = 0; // Reset on success
} catch (error) {
const message = error instanceof Error ? error.message : 'Network error';
const code = error instanceof Error && 'status' in error ? error.status : 'NETWORK_ERROR';
container.setError(message, code);
// Auto-retry with exponential backoff
if (retryCount.value < maxRetries) {
retryCount.value++;
const delay = Math.pow(2, retryCount.value) * 1000; // 2s, 4s, 8s
console.log(`Retrying in ${delay / 1000}s... (${retryCount.value}/${maxRetries})`);
setTimeout(() => {
fetchProducts(true); // Silent retry
}, delay);
}
}
}
function handleManualRetry() {
retryCount.value = 0;
container.clearError();
fetchProducts();
}
function handleReset() {
retryCount.value = 0;
container.reset();
fetchProducts();
}
fetchProducts();
</script>
<template>
<div>
<!-- Loading State -->
<div v-if="container.isLoading.value && !container.hasError.value">
Loading products...
</div>
<!-- Error State -->
<div v-else-if="container.hasError.value" class="error">
<h3>Error occurred</h3>
<p><strong>Message:</strong> {{ container.error.value?.message }}</p>
<p><strong>Code:</strong> {{ container.error.value?.code }}</p>
<!-- Retry Info -->
<p v-if="retryCount > 0 && retryCount < maxRetries">
Auto-retrying... ({{ retryCount }}/{{ maxRetries }})
</p>
<p v-else-if="retryCount >= maxRetries">
Max retries reached. Please try again manually.
</p>
<!-- Actions -->
<button @click="handleManualRetry">Retry Now</button>
<button @click="handleReset">Reset All</button>
<button @click="container.clearError">Dismiss</button>
</div>
<!-- Success State -->
<div v-else-if="container.isLoaded.value">
<div v-if="container.isEmpty.value">No products found</div>
<div v-else>
<div v-for="product in container.data.value" :key="product.id">
{{ product.name }}
</div>
</div>
</div>
</div>
</template>Key Takeaways:
setError()untuk manual error state managementclearError()untuk dismiss errors- Exponential backoff retry pattern dengan silent flag
- Differentiate HTTP errors vs backend errors
reset()untuk complete state cleanup- Display error details (
message+code) untuk debugging
Catatan: Auto-retry useful untuk transient network errors, manual retry untuk user-triggered recovery.
Links
- Repository: GitLab
- Registry: Private Registry