DataContainer (@bpmlib/utils-data-container)
Vue 3 composable untuk mengelola paginated data dengan filtering, sorting, dan search capabilities
Versi: 0.1.1
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,
QueryIntent,
PaginationIntent,
SortIntent,
SearchIntent,
} 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,
QueryIntent
} 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;
}
const container = useDataContainer<User>();
async function fetchUsers() {
container.loadStatus = 0;
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">Loading...</div>
<div v-else-if="container.hasError">{{ container.error?.message }}</div>
<div v-else>
<div v-for="user in container.data" :key="user.id">
{{ user.name }} - {{ user.email }}
</div>
</div>
</div>
</template>Key Points:
useDataContainermanage reactive state untuk data, filter, pagination, sort, searchloadStatusdirect value (tidak perlu.valuedi script)getterUrlStringAttribute()generate URL query stringmapFromResponse()auto-detect pagination mode
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 = 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) {
container.setSort(field, container.sort.sortDir === 'a' ? 'd' : 'a');
fetchProducts();
}
function handlePageChange(page: number) {
container.setPage({
numeric: { ...container.page.numeric, current: page }
});
fetchProducts();
}
fetchProducts();
</script>
<template>
<div>
<div class="filters">
<input v-model="container.filter.category" @change="handleFilterChange">
<input v-model.number="container.filter.minPrice" @change="handleFilterChange">
<input v-model.number="container.filter.maxPrice" @change="handleFilterChange">
<button @click="container.resetFilterToInit">Reset</button>
</div>
<input v-model="container.search" @change="fetchProducts" placeholder="Search...">
<div class="sort">
<button @click="handleSort('name')">Sort by Name</button>
<button @click="handleSort('price')">Sort by Price</button>
</div>
<div v-if="container.isLoading">Loading...</div>
<div v-else-if="container.hasError">{{ container.error?.message }}</div>
<div v-else-if="container.isEmpty">No products found</div>
<div v-else>
<div v-for="product in container.data" :key="product.id">
{{ product.name }} - ${{ product.price }} ({{ product.category }})
</div>
</div>
<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>
</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:
- Check response fields untuk determine mode
max_page+total→'lengthaware'(classic pagination)next_cursor/previous_cursor→'cursor'(token-based)has_more→'simple'(next/prev only)- No pagination fields →
'none' - Mode automatically set di
page.paginationMode
Detection Logic:
// Backend response examples
// Length-aware (classic)
{
"content": [...],
"max_page": 10,
"current_page": 1,
"total": 95
}
// → paginationMode: 'lengthaware'
// Cursor-based
{
"content": [...],
"next_cursor": "eyJpZCI6MTAwfQ==",
"previous_cursor": null
}
// → paginationMode: 'cursor'
// Simple
{
"content": [...],
"has_more": true
}
// → paginationMode: 'simple'
// No pagination
{
"content": [...]
}
// → paginationMode: 'none'Use Case: Backend API consistency, support multiple APIs dengan format berbeda, dynamic UI rendering based on mode.
See: PaginationMode Type, Example 3
WORKS WELL WITH
Library ini dirancang untuk bekerja seamless dengan @bpmlib/vue-sapaginator - Vue 3 pagination component yang auto-detect mode dari DataContainer dan render UI sesuai pagination type.
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
Query Intent Initialization
NEW v0.1.1
Feature baru untuk initialize container state dari URL query parameters atau router state.
QueryIntent memungkinkan kamu initialize pagination, sort, dan search state saat composable dibuat, berguna untuk URL-driven state atau SSR scenarios.
How it works:
- Pass
QueryIntentsebagai second parameter keuseDataContainer() - Object ini define initial state untuk pagination, sort, dan search
- Berguna untuk hydrate state dari URL query params (e.g., Vue Router, Nuxt route)
- State langsung applied saat initialization, tidak perlu manual setup
Example:
import { useRoute } from 'vue-router';
const route = useRoute();
// Parse query params dari URL
const queryIntent: QueryIntent = {
pagination: {
mode: 'lengthaware',
perPage: Number(route.query.perPage) || 20,
page: Number(route.query.page) || 1
},
sort: {
by: route.query.sortBy as string || 'name',
dir: route.query.sortDir === 'desc' ? 'desc' : 'asc'
},
search: {
param: 'q',
value: route.query.q as string || ''
}
};
// Initialize container dengan URL state
const container = useDataContainer<Product, ProductFilter>(
{ category: '', status: '' },
queryIntent
);
// Container sudah initialized dengan state dari URL
// page.numeric.current = 1
// page.perPage = 20
// sort.sortBy = 'name'
// search = ''Use Case: Deep linking, shareable URLs, SSR hydration, browser back/forward support, bookmarkable search results.
See: QueryIntent Types, Example 8
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 |
queryIntent | QueryIntent | undefined | Initial state untuk pagination, sort, search dari URL/router Lihat selengkapnya |
queryIntent
NEW v0.1.1
Parameter baru untuk initialize container state dari URL query params atau router state.
Optional object untuk set initial pagination, sort, dan search configuration. Berguna untuk URL-driven state atau deep linking.
Type: QueryIntent
Structure:
interface QueryIntent {
pagination?: PaginationIntent;
sort?: SortIntent;
search?: SearchIntent;
}Contoh:
const container = useDataContainer<User, UserFilter>(
{ status: '' },
{
pagination: { mode: 'lengthaware', page: 2, perPage: 20 },
sort: { by: 'name', dir: 'asc' },
search: { param: 'q', value: 'john' }
}
);
// Container sudah initialized dengan state di atas
console.log(container.page.numeric.current); // 2
console.log(container.sort.sortBy); // 'name'
console.log(container.search); // 'john'Use Case:
- Initialize dari Vue Router query params
- SSR hydration dengan URL state
- Deep linking untuk shareable search results
- Browser back/forward support
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
BREAKING CHANGE v0.1.1
Changed dari Ref<-1 | 0 | 1> ke direct value dengan getter/setter. Tidak perlu .value lagi di <script>.
loadStatus: -1 | 0 | 1Loading status indicator: -1 (error), 0 (loading), 1 (loaded successfully).
Penggunaan:
// v0.1.1 - Direct assignment (NO .value)
container.loadStatus = 0;
if (container.loadStatus === 1) { }
// v0.1.0 - Old way (deprecated, akan error)
// container.loadStatus.value = 0; // ERROR: Cannot read 'value' of numberDi template: Tetap works tanpa .value (tidak ada breaking change di template).
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: (key: string, value: unknown) => voidSet single filter value dengan support nested paths menggunakan dot notation.
Parameters:
key- Filter key atau nested path (e.g.,'status','nested.category')value- Filter value (any type)
Contoh:
container.setFilter('status', 'active');
container.setFilter('nested.category', 'tech');
container.setFilter('pricing.min', 100);
// Result:
// filter = {
// status: 'active',
// nested: { category: 'tech' },
// pricing: { min: 100 }
// }applyFilter()
applyFilter: () => voidCopy current filter state ke _appliedFilter. Applied filter digunakan untuk URL generation dan backend request.
Use Case: "Apply" button pattern, commit filter changes sebelum fetch.
resetFilterToInit()
resetFilterToInit: () => voidReset filter ke initialFilter values (passed saat create container).
Use Case: "Reset All" button.
resetFilterToApplied()
resetFilterToApplied: () => voidDiscard draft changes, reset filter ke last applied state (_appliedFilter).
Use Case: "Cancel" button, revert uncommitted changes.
hasUnappliedFilterChanges()
hasUnappliedFilterChanges: () => booleanCheck apakah filter berbeda dengan _appliedFilter. Returns true jika ada pending changes.
Use Case: Enable/disable "Apply" button, dirty state indicator, prevent navigation dengan unsaved changes.
Search Methods
setSearchParam()
setSearchParam: (param: string) => voidSet parameter name untuk search di URL query string.
Default: 'q'
Contoh:
container.setSearchParam('search');
container.search = 'laptop';
// URL: ?search=laptopgetSearchParam()
getSearchParam: () => stringGet current search parameter name.
Pagination Methods
setPage()
setPage: (config: Partial<PageConfig> | null) => voidUpdate pagination configuration. Accept partial config, hanya update field yang di-pass.
Parameters:
config- PartialPageConfigobject ataunulluntuk reset
Contoh:
// Update current page
container.setPage({
numeric: { ...container.page.numeric, current: 2 }
});
// Update per page
container.setPage({
perPage: 50
});
// Switch to cursor mode
container.setPage({
paginationMode: 'cursor',
cursor: { next: 'token123', prev: null }
});
// Reset pagination
container.setPage(null);Sort Methods
setSort()
setSort: (sortBy: string, sortDir?: 'a' | 'd') => voidSet sort field dan direction.
Parameters:
sortBy- Field name untuk sortsortDir- Optional direction:'a'(ascending) or'd'(descending). Default:'a'
Contoh:
container.setSort('name'); // Sort by name ascending
container.setSort('price', 'd'); // Sort by price descending
container.setSort('', 'a'); // Clear sortError Methods
setError()
setError: (message: string, code: number | string) => voidSet error state manually.
Parameters:
message- Human-readable error messagecode- Error code (HTTP status atau custom identifier)
Contoh:
container.setError('Failed to fetch data', 500);
container.setError('Validation failed', 'VALIDATION_ERROR');Note: Automatically sets loadStatus to -1.
clearError()
clearError: () => voidClear error state. Set error ke null, tidak mengubah loadStatus.
URL Generation Methods
getterObjectAttribute()
getterObjectAttribute: () => UrlParamsGenerate plain object dari applied state untuk use dengan axios/fetch params.
Returns: UrlParams object
Contoh:
const params = container.getterObjectAttribute();
// {
// page: 2,
// perPage: 20,
// sortBy: 'name',
// sortDir: 'asc',
// filter: { status: 'active' },
// q: 'search term'
// }
// Use dengan axios
axios.get('/api/users', { params });
// Use dengan fetch
const url = new URL('/api/users', window.location.origin);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, String(value));
});getterUrlStringAttribute()
getterUrlStringAttribute: () => stringGenerate URL-encoded query string dari applied state untuk direct append.
Returns: URL-encoded string
Contoh:
const queryString = container.getterUrlStringAttribute();
// "page=2&perPage=20&sortBy=name&sortDir=asc&filter[status]=active&q=search"
// Use dengan fetch
fetch('/api/users?' + queryString);
// Use dengan axios
axios.get('/api/users?' + queryString);Utility Methods
reset()
reset: () => voidComplete state reset. Reset semua state ke initial values:
data→[]filter→initialFilterpage→ default paginationsort→ no sortsearch→''loadStatus→0error→nulladditionalData→null
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. Auto-detect pagination mode, update data, handle errors, dan populate metadata.
Parameters:
response- API response object dengan standard structuremapper- Optional transformer function untuk convert backend items ke frontend model
Response Structure:
{
success: boolean, // Success flag (optional)
message: string, // Error message jika failed (optional)
code: string | number, // Error code (optional)
content: TData[] | TData, // Data array atau single item
appended: TAdditional, // Additional metadata (optional)
// Pagination fields (optional, auto-detected)
max_page: number, // Total pages (lengthaware)
current_page: number, // Current page (lengthaware)
total: number, // Total items count (lengthaware)
next_cursor: string, // Next cursor token (cursor)
previous_cursor: string, // Previous cursor token (cursor)
per_page: number, // Items per page
has_more: boolean, // Has more pages (simple)
}Contoh:
// Basic usage
const response = await fetch('/api/users').then(r => r.json());
container.mapFromResponse(response);
// With mapper function
container.mapFromResponse(response, (item: any) => ({
id: item.user_id,
name: item.full_name,
email: item.email_address,
createdAt: new Date(item.created_at)
}));
// Auto-detects pagination mode
// Response dengan max_page → paginationMode: 'lengthaware'
// Response dengan next_cursor → paginationMode: 'cursor'
// Response dengan has_more → paginationMode: 'simple'
// Response tanpa pagination → paginationMode: 'none'See: Pagination Mode Auto-Detection, Example 5
Types
DataContainerObject
TIP
Type definition untuk return value dari useDataContainer(). Useful untuk typing component props atau function parameters.
interface DataContainerObject<
TData = unknown,
TFilter extends Record<string, unknown> = Record<string, unknown>,
TAdditional = unknown,
>See Reactive State, Computed Properties, dan Methods sections untuk full structure.
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 yang contains state untuk semua pagination modes.
Properties:
numeric.current- Current page number (lengthaware/simple)numeric.max- Total pages (lengthaware only)cursor.next- Next page cursor token (cursor mode)cursor.prev- Previous page cursor token (cursor mode)perPage- Items per pagehasMore- Has more pages flag (simple mode)totalData- Total items count (lengthaware only)paginationMode- Current detected modeisFrontEndPreferCursor- Frontend preference untuk cursor mode (set via QueryIntent)
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
QueryIntent
NEW v0.1.1
Type baru untuk initialize container state dari URL query params.
interface QueryIntent {
pagination?: PaginationIntent;
sort?: SortIntent;
search?: SearchIntent;
}Optional configuration untuk initialize pagination, sort, dan search state saat container creation.
Properties:
pagination- Initial pagination config Lihat selengkapnyasort- Initial sort config Lihat selengkapnyasearch- Initial search config Lihat selengkapnya
See: Query Intent Initialization, Example 8
PaginationIntent
NEW v0.1.1
Part of QueryIntent untuk initialize pagination state.
interface PaginationIntent {
mode: 'none' | 'lengthaware' | 'simple' | 'cursor';
perPage?: number;
page?: number;
preferCursor?: boolean;
}Initial pagination configuration.
Properties:
mode- Pagination mode to useperPage- Items per page (optional)page- Starting page number (optional, for lengthaware/simple)preferCursor- Prefer cursor mode jika backend supports both (optional)
Contoh:
const intent: PaginationIntent = {
mode: 'lengthaware',
perPage: 20,
page: 2
};SortIntent
NEW v0.1.1
Part of QueryIntent untuk initialize sort state.
interface SortIntent {
by: string;
dir?: 'asc' | 'desc';
}Initial sort configuration.
Properties:
by- Field name to sort bydir- Sort direction (optional, default:'asc')
Contoh:
const intent: SortIntent = {
by: 'name',
dir: 'desc'
};SearchIntent
NEW v0.1.1
Part of QueryIntent untuk initialize search state.
interface SearchIntent {
param?: string;
value?: string;
}Initial search configuration.
Properties:
param- Search parameter name untuk URL (optional, default:'q')value- Initial search value (optional, default:'')
Contoh:
const intent: SearchIntent = {
param: 'q',
value: 'laptop'
};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
- 8. URL-Driven State with QueryIntent ⭐ NEW v0.1.1
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 = 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({
numeric: { ...container.page.numeric, current: page }
});
fetchProducts();
}
fetchProducts();
</script>
<template>
<div>
<div v-if="container.isLoading">Loading...</div>
<div v-else-if="container.hasError" class="error">
{{ container.error?.message }}
<button @click="fetchProducts">Retry</button>
</div>
<div v-else-if="container.isEmpty">No products found</div>
<div v-else>
<div v-for="product in container.data" :key="product.id">
<h3>{{ product.name }}</h3>
<p>${{ product.price }}</p>
</div>
<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>
</div>
</div>
</div>
</template>Key Takeaways:
getterUrlStringAttribute()builds query string dengan pagination paramsmapFromResponse()auto-detect pagination mode- Use
isLoading,hasError,isEmptyuntuk 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;
}
interface ProductFilter {
status?: string;
pricing?: {
min?: number;
max?: number;
};
}
const container = useDataContainer<Product, ProductFilter>({
status: '',
pricing: { min: 0, max: 0 }
});
async function fetchProducts() {
container.loadStatus = 0;
const response = await fetch('/api/products?' + container.getterUrlStringAttribute());
const data = await response.json();
container.mapFromResponse(data);
}
function handleApply() {
if (container.hasUnappliedFilterChanges()) {
container.applyFilter();
fetchProducts();
}
}
fetchProducts();
</script>
<template>
<div>
<div class="filters">
<input v-model="container.filter.status" placeholder="Status">
<input v-model.number="container.filter.pricing.min" placeholder="Min">
<input v-model.number="container.filter.pricing.max" placeholder="Max">
<button @click="handleApply" :disabled="!container.hasUnappliedFilterChanges()">
Apply
</button>
<button @click="container.resetFilterToApplied">Cancel</button>
<button @click="container.resetFilterToInit">Reset All</button>
<p v-if="container.hasUnappliedFilterChanges()" class="warning">
Unapplied changes
</p>
</div>
<div v-if="container.isLoading">Loading...</div>
<div v-else>
<div v-for="product in container.data" :key="product.id">
{{ product.name }}
</div>
</div>
</div>
</template>Key Takeaways:
- Nested filter paths:
container.filter.pricing.min hasUnappliedFilterChanges()enable/disable Apply buttonapplyFilter()commit changes sebelum fetchresetFilterToApplied()cancel (revert ke last applied)resetFilterToInit()reset ke initial values
3. Multiple Pagination Modes
Handle different pagination modes dari same component.
<script setup lang="ts">
import { useDataContainer } from '@bpmlib/utils-data-container';
import { computed } from 'vue';
interface Item {
id: number;
title: string;
}
const container = useDataContainer<Item>();
async function fetchItems() {
container.loadStatus = 0;
const response = await fetch('/api/items?' + container.getterUrlStringAttribute());
const data = await response.json();
container.mapFromResponse(data);
}
function nextPage() {
const mode = container.page.paginationMode;
if (mode === 'lengthaware' || mode === 'simple') {
const current = container.page.numeric.current ?? 1;
container.setPage({
numeric: { ...container.page.numeric, current: current + 1 }
});
} else if (mode === 'cursor') {
container.setPage({
cursor: { ...container.page.cursor, prev: container.page.cursor.next }
});
}
fetchItems();
}
function prevPage() {
const mode = container.page.paginationMode;
if (mode === 'lengthaware' || mode === 'simple') {
const current = container.page.numeric.current ?? 1;
if (current > 1) {
container.setPage({
numeric: { ...container.page.numeric, current: current - 1 }
});
fetchItems();
}
} else if (mode === 'cursor' && container.page.cursor.prev) {
container.setPage({
cursor: { next: container.page.cursor.prev, prev: null }
});
fetchItems();
}
}
const canGoNext = computed(() => {
const mode = container.page.paginationMode;
if (mode === 'lengthaware') {
return (container.page.numeric.current ?? 0) < (container.page.numeric.max ?? 0);
} else if (mode === 'simple') {
return container.page.hasMore === true;
} else if (mode === 'cursor') {
return container.page.cursor.next !== null;
}
return false;
});
const canGoPrev = computed(() => {
const mode = container.page.paginationMode;
if (mode === 'lengthaware' || mode === 'simple') {
return (container.page.numeric.current ?? 1) > 1;
} else if (mode === 'cursor') {
return container.page.cursor.prev !== null;
}
return false;
});
fetchItems();
</script>
<template>
<div>
<!-- Data -->
<div v-if="container.isLoading">Loading...</div>
<div v-else>
<div v-for="item in container.data" :key="item.id">
{{ item.title }}
</div>
</div>
<!-- Universal Pagination Controls -->
<div class="pagination">
<button @click="prevPage" :disabled="!canGoPrev">Previous</button>
<!-- Show current page for lengthaware/simple -->
<span v-if="container.page.paginationMode === 'lengthaware'">
Page {{ container.page.numeric.current }} of {{ container.page.numeric.max }}
</span>
<span v-else-if="container.page.paginationMode === 'simple'">
Page {{ container.page.numeric.current }}
</span>
<span v-else-if="container.page.paginationMode === 'cursor'">
Cursor pagination
</span>
<button @click="nextPage" :disabled="!canGoNext">Next</button>
</div>
<!-- Mode Indicator -->
<p>Mode: {{ container.page.paginationMode }}</p>
</div>
</template>Key Takeaways:
- Single component handles all pagination modes
mapFromResponse()automatically detects mode dari backend response- Mode-specific logic for next/prev navigation
- Computed properties untuk enable/disable buttons based on mode
- Works dengan any API tanpa configuration changes
4. Search with Custom Parameter
Customize search parameter name untuk match backend API.
<script setup lang="ts">
import { useDataContainer } from '@bpmlib/utils-data-container';
import { watch } from 'vue';
interface Article {
id: number;
title: string;
}
const container = useDataContainer<Article>();
container.setSearchParam('query');
async function fetchArticles() {
container.loadStatus = 0;
const response = await fetch('/api/articles?' + container.getterUrlStringAttribute());
const data = await response.json();
container.mapFromResponse(data);
}
let searchTimeout: number;
watch(() => container.search, () => {
clearTimeout(searchTimeout);
searchTimeout = window.setTimeout(fetchArticles, 300);
});
fetchArticles();
</script>
<template>
<div>
<input v-model="container.search" placeholder="Search...">
<div v-if="container.isLoading">Searching...</div>
<div v-else-if="container.isEmpty">
{{ container.search ? 'No results' : 'Start typing' }}
</div>
<div v-else>
<div v-for="article in container.data" :key="article.id">
<h3>{{ article.title }}</h3>
</div>
</div>
</div>
</template>Key Takeaways:
setSearchParam()customize parameter name (default:'q')container.searchdirect access tanpa.value- Watch search untuk auto-fetch dengan debounce
- Generated URL:
?query=search+term
5. Response Mapping with Custom Transformer
Transform backend data format ke frontend model.
<script setup lang="ts">
import { useDataContainer } from '@bpmlib/utils-data-container';
interface User {
id: number;
fullName: string;
email: string;
createdAt: Date;
}
interface BackendUser {
user_id: number;
first_name: string;
last_name: string;
email_address: string;
created_at: string;
}
const container = useDataContainer<User>();
async function fetchUsers() {
container.loadStatus = 0;
const response = await fetch('/api/users');
const data = await response.json();
container.mapFromResponse(data, (item: BackendUser) => ({
id: item.user_id,
fullName: `${item.first_name} ${item.last_name}`,
email: item.email_address,
createdAt: new Date(item.created_at)
}));
}
fetchUsers();
</script>
<template>
<div v-if="container.isLoaded">
<div v-for="user in container.data" :key="user.id">
<h3>{{ user.fullName }}</h3>
<p>{{ user.email }}</p>
<small>{{ user.createdAt.toLocaleDateString() }}</small>
</div>
</div>
</template>Key Takeaways:
- Mapper function transform backend format ke frontend model
- Useful untuk rename fields, compute values, convert types
additionalDataviaappendedfield untuk metadata/stats
6. Complete CRUD Operations
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 newTitle = ref('');
async function fetchTodos() {
container.loadStatus = 0;
const response = await fetch('/api/todos');
const data = await response.json();
container.mapFromResponse(data);
}
async function createTodo() {
if (!newTitle.value.trim()) return;
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ title: newTitle.value })
});
const data = await response.json();
container.appendData({ id: data.id, title: newTitle.value, completed: false });
newTitle.value = '';
}
async function toggleTodo(index: number) {
const todo = container.data.value[index];
await fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
body: JSON.stringify({ completed: !todo.completed })
});
container.data.value[index].completed = !todo.completed;
}
async function deleteTodo(index: number) {
const todo = container.data.value[index];
await fetch(`/api/todos/${todo.id}`, { method: 'DELETE' });
container.deleteDataByIndex(index);
}
fetchTodos();
</script>
<template>
<div>
<div class="create-form">
<input v-model="newTitle" @keyup.enter="createTodo">
<button @click="createTodo">Add</button>
</div>
<div v-if="container.isLoaded">
<p>Total: {{ container.dataLength() }}</p>
<div v-for="(todo, index) in container.data" :key="todo.id">
<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>
</div>
</template>Key Takeaways:
appendData()untuk optimistic createdeleteDataByIndex()untuk optimistic delete- Direct mutation
data.value[index]untuk updates dataLength()untuk display count
7. Error Handling Patterns
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 = 0;
try {
const response = await fetch('/api/products?' + container.getterUrlStringAttribute());
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (!data.success) throw new Error(data.message);
container.mapFromResponse(data);
retryCount.value = 0;
} catch (error) {
const message = error instanceof Error ? error.message : 'Network error';
container.setError(message, 'FETCH_ERROR');
if (retryCount.value < maxRetries) {
retryCount.value++;
const delay = Math.pow(2, retryCount.value) * 1000;
setTimeout(() => fetchProducts(true), delay);
}
}
}
function handleRetry() {
retryCount.value = 0;
container.clearError();
fetchProducts();
}
fetchProducts();
</script>
<template>
<div>
<div v-if="container.isLoading && !container.hasError">Loading...</div>
<div v-else-if="container.hasError" class="error">
<h3>Error: {{ container.error?.message }}</h3>
<p v-if="retryCount < maxRetries">Retrying ({{ retryCount }}/{{ maxRetries }})</p>
<p v-else>Max retries reached</p>
<button @click="handleRetry">Retry</button>
<button @click="container.reset(); fetchProducts()">Reset</button>
</div>
<div v-else-if="container.isLoaded">
<div v-for="product in container.data" :key="product.id">
{{ product.name }}
</div>
</div>
</div>
</template>Key Takeaways:
setError()untuk manual error stateclearError()untuk dismiss errors- Exponential backoff retry pattern
reset()untuk complete state cleanup
8. URL-Driven State with QueryIntent
NEW v0.1.1
Initialize container dari URL query parameters untuk deep linking.
<script setup lang="ts">
import { useDataContainer } from '@bpmlib/utils-data-container';
import { useRoute, useRouter } from 'vue-router';
import type { QueryIntent } from '@bpmlib/utils-data-container';
interface Product {
id: number;
name: string;
price: number;
}
interface ProductFilter {
category?: string;
}
const route = useRoute();
const router = useRouter();
const queryIntent: QueryIntent = {
pagination: {
mode: 'lengthaware',
perPage: Number(route.query.perPage) || 20,
page: Number(route.query.page) || 1
},
sort: {
by: (route.query.sortBy as string) || 'name',
dir: route.query.sortDir === 'desc' ? 'desc' : 'asc'
},
search: {
value: (route.query.q as string) || ''
}
};
const container = useDataContainer<Product, ProductFilter>(
{ category: (route.query.category as string) || '' },
queryIntent
);
container.applyFilter();
async function fetchProducts() {
container.loadStatus = 0;
const response = await fetch('/api/products?' + container.getterUrlStringAttribute());
const data = await response.json();
container.mapFromResponse(data);
}
function syncToUrl() {
const query: Record<string, string> = {};
if (container.page.numeric.current) query.page = String(container.page.numeric.current);
if (container.page.perPage) query.perPage = String(container.page.perPage);
if (container.sort.sortBy) {
query.sortBy = container.sort.sortBy;
query.sortDir = container.sort.sortDir === 'a' ? 'asc' : 'desc';
}
if (container.search) query.q = container.search;
if (container._appliedFilter.category) query.category = container._appliedFilter.category;
router.push({ query });
}
function handleApply() {
container.applyFilter();
syncToUrl();
fetchProducts();
}
function handleSort(field: string) {
container.setSort(field, container.sort.sortDir === 'a' ? 'd' : 'a');
syncToUrl();
fetchProducts();
}
fetchProducts();
</script>
<template>
<div>
<h2>URL-Driven State</h2>
<div class="filters">
<input v-model="container.filter.category">
<button @click="handleApply" :disabled="!container.hasUnappliedFilterChanges()">
Apply
</button>
</div>
<input v-model="container.search" @change="syncToUrl(); fetchProducts()">
<div class="sort">
<button @click="handleSort('name')">Name</button>
<button @click="handleSort('price')">Price</button>
<span>({{ container.sort.sortBy }} {{ container.sort.sortDir === 'a' ? '↑' : '↓' }})</span>
</div>
<div v-if="container.isLoading">Loading...</div>
<div v-else>
<div v-for="product in container.data" :key="product.id">
{{ product.name }} - ${{ product.price }}
</div>
</div>
</div>
</template>Key Takeaways:
QueryIntentinitialize dari URL saat creation- Two-way sync: URL → Container (init), Container → URL (changes)
- Users dapat share/bookmark URLs dengan full state
- Browser back/forward works naturally
- Useful untuk: shareable search, SSR hydration, bookmarkable filters