SaTable
Komponen tabel Vue 3 dengan empat mode sumber data (local, controlled+manual, controlled+container, tanstackTable), dilengkapi search, sort, pagination, expandable rows, dan sistem warna per-region — semuanya self-styled tanpa dependency Tailwind di sisi consumer.
Versi: 0.1.0-beta.0 Kategori: UI Component (Vue 3)
TL;DR
SaTable cocok untuk kebutuhan tabel data lengkap — search, sort, pagination, expandable rows — tanpa perlu menulis logic itu sendiri, tapi tetap "dumb": table tidak pernah fetch data sendiri, hanya emit sinyal. SaTableManual adalah versi bare-nya: sekadar wrapper overflow-x-auto, seluruh markup <table> jadi tanggung jawab consumer sepenuhnya.
Components:
import { SaTable, SaTableManual } from '@bpmlib/vue-satable';Types:
import type {
ColumnDef,
SaTableProps,
TableColor,
TableColorConfig,
TableBodyColor,
TableStripe,
TablePerPage,
TableAccent,
SortState,
SearchLogic,
ExpandableConfig,
RowKeyResolver,
TableContainerLike,
} from '@bpmlib/vue-satable';Installation & Setup
Requirements
Peer Dependencies
Wajib:
npm install vue@^3.3.0Opsional:
npm install @tanstack/vue-table@^8.0.0
npm install @bpmlib/utils-data-container@^0.4.0| Dependency | Versi | Status | Deskripsi |
|---|---|---|---|
vue | ^3.3.0 | Required | Vue 3 framework |
@tanstack/vue-table | ^8.0.0 | Optional | Aktifkan :tanstackTable — lazy-loaded, hanya di-fetch saat prop ini benar-benar dipakai |
@bpmlib/utils-data-container | ^0.4.0 | Optional | Aktifkan :container — di-duck-type, bukan hard dependency |
NOTE
@tanstack/vue-table tidak pernah masuk bundle vue-satable sendiri, dan tidak pernah di-fetch kalau :tanstackTable tidak dipakai — satu-satunya file yang mengimpor value (bukan sekadar type) dari package ini di-load lewat defineAsyncComponent(), dipicu hanya oleh kondisi v-if="tanstackTable".
Package Installation
npm install @bpmlib/vue-satableyarn add @bpmlib/vue-satablepnpm add @bpmlib/vue-satablebun install @bpmlib/vue-satableImport
Basic Import:
import { SaTable, SaTableManual } from '@bpmlib/vue-satable';CSS Import:
import '@bpmlib/vue-satable/style.css';See: Styling untuk detail CSS variables yang tersedia untuk theming.
Quick Start
Basic Usage
Contoh paling sederhana — satu kolom, tanpa config tambahan:
<script setup lang="ts">
import { SaTable } from '@bpmlib/vue-satable';
const employees = [
{ id: 1, name: 'Siti Aminah', department: 'Finance' },
{ id: 2, name: 'Budi Santoso', department: 'Engineering' },
];
</script>
<template>
<SaTable :data="employees" :headers="[{ label: 'name' }, { label: 'department' }]" />
</template>Key Points:
{ label: 'department' }saja sudah cukup — header di-humanize otomatis (department→ "Department"), cell membacarow.department, sort (kalau diaktifkan) memakai lookup yang sama.- Tanpa prop lain, table berjalan penuh di mode
local— sort/search/pagination dihitung sendiri oleh component.
Comprehensive Example
Full-featured: search, sort, pagination, expandable rows, semua lewat v-model:
<script setup lang="ts">
import { ref } from 'vue';
import { SaTable } from '@bpmlib/vue-satable';
import type { SortState } from '@bpmlib/vue-satable';
const rows = [
{ id: 1, name: 'Siti Aminah', department: 'Finance', notes: 'Handles quarterly review' },
{ id: 2, name: 'Budi Santoso', department: 'Engineering', notes: '' },
];
const columns = [
{ label: 'name', sort: true },
{ label: 'department', sort: true },
];
const search = ref('');
const currentPage = ref(1);
const sort = ref<SortState | null>(null);
const expandedRows = ref<Array<string | number>>([]);
</script>
<template>
<SaTable
:data="rows"
:headers="columns"
with-search
expandable="row => !!row.notes"
row-key="id"
v-model:search="search"
v-model:current-page="currentPage"
v-model:sort="sort"
v-model:expanded-rows="expandedRows"
>
<template #expanded-row="{ row }">
<div style="padding: 8px">Notes: {{ row.notes }}</div>
</template>
</SaTable>
</template>Key Points:
- Search di sini explicit-submit — user harus tekan Enter/tombol search, mengetik saja tidak langsung filter.
expandablepredikat menentukan baris mana yang boleh di-expand;rowKeywajib diisi begituexpandabletruthy.
Alternative: Data-Source Modes
SaTable punya empat mode sumber data — lihat Core Concepts untuk penjelasan lengkap masing-masing, dan Examples untuk contoh kode tiap mode (asyncTable, :container, :tanstackTable).
Core Concepts
Empat Mode Sumber Data
SaTable punya empat cara kerja, dipilih lewat kombinasi prop, bukan lewat prop mode yang eksplisit satu-satu:
| Mode | Ditandai dengan | Siapa yang hitung sort/search/page |
|---|---|---|
| local | Tidak ada asyncTable/container/tanstackTable | SaTable sendiri, dari data penuh |
| controlled + manual | asyncTable (tanpa container) | Consumer — data sudah berupa halaman aktif, SaTable emit manipulate |
| controlled + container | asyncTable + container | container (instance useDataContainer()-shaped) |
| tanstackTable | tanstackTable diisi | TanStack — mengesampingkan headers/data/row-computation sepenuhnya |
asyncTable adalah discriminator eksplisit (bukan hasil tebakan) — kalau ditebak salah, pagination bisa diam-diam berhenti bekerja. Begitu tanstackTable diisi, asyncTable tidak lagi relevan untuk perhitungan baris (TanStack selalu ambil alih), hanya masih dipakai untuk menentukan chrome loading/error.
Container: "Dumb", Hanya Trigger
:container menerima instance mirip useDataContainer() (di-duck-type lewat interface TableContainerLike, bukan hard-import dari package aslinya). SaTable tidak pernah memanggil fetcher sendiri — hanya memutasi state reaktif container (setSort, submitSearch, navigatePage, dst.) dan container-lah yang bertanggung jawab re-fetch. Ini sengaja: consumer sering butuh custom pre/post-processing yang tidak bisa diasumsikan oleh table.
Known gaps di upstream TableContainerLike: container.sort hanya single-column; tidak ada method "clear sort" eksplisit — table melakukan workaround via setSort('', 'asc').
tanstackTable: Tidak Terbatas & Lazy-Loaded
:tanstackTable valid dikombinasikan dengan apa pun — local, controlled+manual, atau :container sekaligus (:container mengurus data lifecycle, :tanstackTable mengurus konstruksi tabel; SaTable tidak pernah menjembatani keduanya secara otomatis, wiring onSortingChange ke container.setSort() adalah kode consumer sendiri).
Begitu :tanstackTable diisi, ia mengesampingkan headers, seluruh row-computation, hampir semua slot per-interaksi (customRows, #cell-{id}, dst. — mekanisme columnDef.cell/columnDef.header TanStack sendiri adalah lapisan itu), dan expandable/rowKey (pakai getExpandedRowModel/getRowId versi TanStack). refresh, row-click, dan slot status (customLoading/customError/customEmpty) tetap jalan — TanStack tidak punya konsep untuk hal-hal ini.
@tanstack/vue-table sendiri genuinely lazy — lihat catatan di Installation.
Kontrak Definisi Kolom
Setiap item headers adalah objek ColumnDef — kontrak "lite-TanStack": id/header/label/cell/sort. Contoh minimal yang sudah lengkap fungsinya: { label: 'address' } — header (di-humanize jadi "Address"), cell (baca row.address), dan sort (kalau diaktifkan, baca key yang sama) semua resolve otomatis dari satu field label.
cell dalam bentuk string selalu berarti lookup key, bukan konten literal — mencegah ambiguitas yang sengaja dihindari (mirip bagaimana TanStack sendiri memisahkan accessorKey dari cell). Bentuk fungsi bisa mengembalikan string | number | boolean | VNode; return boolean/null/undefined di-handle otomatis oleh default sort comparator/placeholder '--'.
Search: Explicit-Submit, Bukan Live-Filter
withSearch menampilkan input bawaan, tapi mengetik saja tidak pernah memfilter — baik di mode local maupun controlled/container. Filter baru berjalan saat submit eksplisit: Enter, tombol search, atau tombol clear (yang otomatis submit ulang dengan value kosong). Ini menjaga UX mode local tetap konsisten dengan mode controlled/container, yang memang secara alami tidak bisa filter sebelum ada aksi submit.
Baris Expandable
expandable adalah prop polymorphic — boolean untuk semua baris, atau (row, index) => boolean per baris. Wajib disertai rowKey (identitas stabil, tidak boleh berbasis index — akan rusak saat sort/pagination berubah). State expand dikontrol lewat v-model:expandedRows (array key, bukan array boolean) — tidak otomatis ter-reset saat pindah halaman (prinsip "table tidak memiliki apa pun" berlaku juga di sini); consumer yang ingin reset-per-halaman cukup tambahkan satu watch(currentPage, () => expandedRows.value = []).
Sistem Warna: color + Override Per-Region + Stripe/Hover sebagai Overlay
color adalah satu prop terpadu (mirip pola vue-salayout): string (menyasar body saja) atau object { header?, footer?, body?, child? }. Selain itu, tiap region juga punya standalone prop sendiri — headerColor/footerColor/bodyColor/childColor — yang selalu menang kalau dua-duanya diisi bersamaan (standalone ?? color[key] ?? default).
body/child (baik lewat color.body/color.child atau standalone) menerima bentuk kaya: string, array (cycle per index), { odd, even }, atau Record<number, TableAccent> — semua resolve unconditional, tidak lagi bergantung pada stripe. child (warna baris expanded) kalau tidak diisi otomatis pakai alt-toggle dari body di index yang sama.
stripe: boolean | 'row' | 'child' murni box-shadow overlay (bukan penggantian token warna) — diterapkan di atas warna apa pun yang sudah resolve, sehingga tidak pernah bentrok dengan warna child yang juga default ke alt-toggle. Hover highlight baris pakai teknik overlay yang sama, selalu aktif (tidak ada toggle-nya).
SaTableManual
Versi bare — hanya wrapper <div class="overflow-x-auto"><slot /></div>. Tidak ada props, tidak ada logic sort/search/pagination bawaan. Cocok untuk kasus consumer ingin markup <table> sepenuhnya custom tapi tetap dapat scroll-container styling yang konsisten dengan SaTable.
API Reference
Components
SaTable
Komponen tabel utama, dengan empat mode sumber data — lihat Core Concepts.
Cara Penggunaan:
Minimal butuh data + headers. Prop lain bersifat opsional dan mengaktifkan fitur tambahan (search, pagination custom, styling, mode async).
Props
| Name | Type | Default | Description |
|---|---|---|---|
data | TRow[] | — | Dataset penuh (local) atau halaman aktif (controlled+manual). Overshadowed oleh container/tanstackTable |
headers | ColumnDef[] | — | Definisi kolom. Wajib kecuali tanstackTable diisi |
withIndex | boolean | false | Tampilkan kolom nomor urut di paling kiri |
withSearch | boolean | false | Tampilkan input search bawaan — lihat Explicit-Submit Search |
searchLogic | SearchLogic | inclusive whole-row scan | Predikat search top-level Lihat selengkapnya |
perPageOptions | TablePerPage[] | [5,10,20,25,50,100,250,'all'] | Pilihan <select> per-halaman, dipakai apa adanya — array custom yang tidak menyertakan 'all' menyembunyikan pilihan itu sepenuhnya Lihat selengkapnya |
rowKey | RowKeyResolver | — | Wajib kalau expandable truthy dan tanstackTable tidak diisi |
expandable | ExpandableConfig | false | boolean atau predikat per baris Lihat selengkapnya |
color | TableColor | — | String menyasar body saja; object override per region Lihat selengkapnya |
headerColor | TableAccent | 'primary' | Menang atas color.header |
footerColor | TableAccent | 'primary' | Menang atas color.footer |
bodyColor | TableBodyColor | 'card' | Menang atas color.body |
childColor | TableBodyColor | alt-toggle dari body | Menang atas color.child |
stripe | TableStripe | false | Overlay box-shadow, bukan penggantian token warna Lihat selengkapnya |
asyncTable | boolean | false | Discriminator eksplisit local vs controlled — lihat Core Concepts |
isLoading | boolean | false | Mode controlled+manual saja. Overshadowed oleh container |
isError | boolean | false | Mode controlled+manual saja. Overshadowed oleh container |
errorMessage | string | '' | Mode controlled+manual saja. Overshadowed oleh container |
totalData | number | — | Mode controlled+manual saja. Overshadowed oleh container.page.totalData |
container | TableContainerLike | — | Instance useDataContainer()-shaped Lihat selengkapnya |
onClickFilter | () => void | — | Trigger UI murni — tampilkan tombol filter Lihat selengkapnya |
title | string | — | Fallback plain-text untuk slot #title |
tanstackTable | Table<any> | — | Instance useVueTable() Lihat selengkapnya |
searchLogic
Predikat pencarian top-level, menggantikan searchable/searchLogic per-kolom sepenuhnya.
Signature:
searchLogic: (row: TRow) => string | (string | number)[]Contoh:
const searchLogic = (row) => [row.name, row.email, row.department].join(' ');Use Case: wajib dipakai kalau kolom hasil customRows menggabungkan beberapa field jadi satu sel — tidak ada satu key kolom untuk dicari.
perPageOptions
Signature:
perPageOptions: TablePerPage[]Contoh:
<!-- default — termasuk "Semua" -->
<SaTable :data="rows" :headers="columns" />
<!-- custom, tanpa opsi "Semua" -->
<SaTable :data="rows" :headers="columns" :per-page-options="[10, 25, 50]" />Use Case: array custom sepenuhnya menggantikan default (bukan digabung) — kalau tidak ingin consumer tabel bisa memilih "tampilkan semua", cukup jangan sertakan 'all' di array yang dikirim. Di mode :container, memilih 'all' men-set page.setup.mode container ke 'none' (sentinel bawaan package untuk "tanpa pagination") — bukan sekadar meminta halaman dengan ukuran sangat besar.
expandable
Signature:
expandable: boolean | ((row: TRow, index: number) => boolean)Contoh:
<SaTable :expandable="row => !!row.notes" row-key="id" />Use Case: kontrol baris mana saja yang boleh di-expand, misalnya hanya baris dengan data detail tambahan.
color
Structure:
type TableColor = TableAccent | {
header?: TableAccent;
footer?: TableAccent;
body?: TableBodyColor;
child?: TableBodyColor;
};Contoh:
<SaTable color="secondary" />
<SaTable :color="{ header: 'card', footer: 'card' }" />Use Case: color="secondary" untuk ganti warna body baris dengan cepat; bentuk object untuk override beberapa region sekaligus (mis. tampilan "blended-island" dengan header/footer sama-sama 'card').
stripe
Signature:
stripe: boolean | 'row' | 'child'Contoh:
<SaTable stripe />
<SaTable :stripe="'child'" />Use Case: true untuk shading di body maupun baris expanded sekaligus; 'row'/'child' untuk batasi ke salah satu saja.
container
Signature:
container: TableContainerLike<TRow>Contoh:
<SaTable async-table :container="myContainer" with-search />Use Case: integrasi dengan @bpmlib/utils-data-container — table hanya memicu method container (setSort, submitSearch, dst.), tidak pernah fetch sendiri.
onClickFilter
Signature:
onClickFilter: () => voidContoh:
<SaTable :on-click-filter="() => (showFilterPanel = true)" />Use Case: table tidak punya panel filter sendiri — prop ini hanya menampilkan tombol trigger; consumer yang mengimplementasikan panel/logic filternya.
tanstackTable
Signature:
tanstackTable: Table<any>Contoh:
<script setup>
import { useVueTable, getCoreRowModel } from '@tanstack/vue-table';
const table = useVueTable({ data, columns, getCoreRowModel: getCoreRowModel() });
</script>
<template>
<SaTable :tanstack-table="table" />
</template>Use Case: butuh multi-sort, selection, atau column-pinning yang tidak didukung engine bawaan — lihat Core Concepts.
Model
| Name | Type | Default | Description |
|---|---|---|---|
v-model:search | string | '' | Live binding input search — filter baru jalan saat submit, lihat Core Concepts |
v-model:currentPage | number | 1 | Halaman aktif |
v-model:sort | SortState | null | null | State sort aktif |
v-model:perPage | number | 20 | Jumlah baris per halaman |
v-model:expandedRows | Array<string | number> | [] | Key baris yang sedang di-expand |
Events
| Name | Payload | Description |
|---|---|---|
manipulate | { reason: string } | Sinyal "consumer mungkin perlu refetch" — hanya di mode controlled Lihat selengkapnya |
refresh | - | Tombol refresh footer ditekan — tidak pernah overshadowed Lihat selengkapnya |
row-click | { row: TRow, index: number } | Baris di-klik. Tombol toggle expand pakai @click.stop supaya tidak double-fire |
manipulate
Satu event funneled untuk hampir semua interaksi yang mungkin butuh refetch — search submit, sort, ganti halaman, ganti per-page, mount awal, dan refresh — dibedakan lewat field reason, bukan lewat event terpisah per jenis interaksi.
Signature:
manipulate: (payload: { reason: string }) => voidNilai reason:
| Value | Dipicu saat | Data/prop/v-model terkait |
|---|---|---|
init | Komponen pertama kali mounted | data/totalData — biasanya sinyal "muat halaman pertama" |
search | Search di-submit (Enter, tombol search, atau tombol clear) | v-model:search, searchLogic |
sort | State sort berubah (klik header sortable) | v-model:sort |
page | Halaman aktif berubah (tombol first/prev/next/last, page-jumper, atau v-model:currentPage dari luar) | v-model:currentPage |
per-page | Jumlah baris per halaman berubah | v-model:perPage, perPageOptions |
refresh | Tombol refresh footer ditekan — lihat catatan di bawah | Tidak ada state yang berubah — murni sinyal "muat ulang data yang sama" |
Kapan dipicu, kapan tidak:
- Hanya aktif di mode
controlled(asyncTabletruthy) — di modelocal, table menghitung semua sendiri, tidak ada yang perlu di-refetch consumer. - Tidak pernah dipicu untuk interaksi yang berasal dari
tanstackTable— TanStack punya mekanisme callback-nya sendiri (onSortingChange, dst.),SaTabletidak menjembataninya kemanipulate. - Di mode
:container, sebagian besarreason(search/sort/page/per-page) tetap terkirim sebagai sinyal tambahan, meskicontainersendiri sudah otomatis re-fetch lewat method-nya masing-masing (setSort,submitSearch, dst.) —manipulatedi sini murni informatif, bukan trigger satu-satunya.
refresh mendapat dua sinyal sekaligus: menekan tombol refresh footer memicu event refresh (dedicated, selalu ada, tidak pernah overshadowed) dan manipulate dengan reason: 'refresh'. Ini satu-satunya interaksi yang punya event khusus di luar manipulate — dipilih karena baik container maupun tanstackTable sama-sama tidak punya konsep "refetch manual" bawaan, jadi sinyal terpisah selalu dibutuhkan di semua mode.
Contoh:
<script setup lang="ts">
function onManipulate({ reason }: { reason: string }) {
if (reason === 'init') return; // sudah di-fetch awal secara terpisah
fetchPage();
}
</script>
<template>
<SaTable :data="data" :headers="columns" async-table @manipulate="onManipulate" @refresh="fetchPage" />
</template>Use Case: satu handler untuk menangani semua kondisi "data mungkin perlu di-refetch", tanpa perlu wiring terpisah per jenis interaksi (bandingkan dengan pola clickRefresh/submitSearch/prevPage/nextPage/dst. yang masing-masing punya event sendiri di komponen lain seperti PaginatorContent).
Slots
| Name | Props | Description |
|---|---|---|
title | - | Konten baris pertama toolbar sisi kiri, fallback ke prop title |
buttons | { clickFilter } | Baris kedua toolbar, default berisi tombol filter (kalau onClickFilter diisi) |
preTable | - | Konten full-width di antara toolbar dan tabel |
header-{id} | { column } | Override header satu kolom — hilang di mode tanstackTable |
cell-{id} | { row, index, value } | Override cell satu kolom — hilang di mode tanstackTable |
customRows | { content, row, index, ...row } | Override seluruh baris (semua <td>) sekaligus — mode local/controlled/container saja |
customLoading | - | Override tampilan loading |
customError | { message } | Override tampilan error |
customEmpty | - | Override tampilan data kosong |
expanded-row | { row, index } | Konten baris yang di-expand |
footer | { pageCount, currentPage, totalItems, recordStart, recordEnd } | Override seluruh footer bawaan |
Contoh:
<SaTable
:data="employees"
:headers="columns"
with-search
v-model:current-page="page"
>
<template #title>Employee Directory</template>
<template #cell-status="{ value }">
<span :class="value === 'active' ? 'text-green-600' : 'text-gray-400'">{{ value }}</span>
</template>
</SaTable>SaTableManual
Bare wrapper — hanya overflow-x-auto, tidak ada logic tabel bawaan.
Cara Penggunaan:
Bungkus markup <table> custom milik consumer sepenuhnya; SaTableManual hanya menyediakan scroll-container yang konsisten stylingnya dengan SaTable.
Props: tidak ada.
Slots
| Name | Props | Description |
|---|---|---|
default | - | Markup <table> custom sepenuhnya milik consumer |
Contoh:
<SaTableManual>
<table class="sa-td--table">
<thead>
<tr><th>Name</th><th>Department</th></tr>
</thead>
<tbody>
<tr v-for="row in employees" :key="row.id">
<td>{{ row.name }}</td>
<td>{{ row.department }}</td>
</tr>
</tbody>
</table>
</SaTableManual>Types
Contains:
- ColumnDef
- TableColor / TableColorConfig
- TableBodyColor
- TableStripe
- TablePerPage
- SortState
- SearchLogic
- ExpandableConfig
- RowKeyResolver
- TableContainerLike
ColumnDef
interface ColumnDef<TRow = any> {
id?: string;
header?: string | ((ctx: { column: ColumnDef<TRow> }) => VNode);
label?: string;
cell?: string | ((row: TRow, index: number) => string | number | boolean | VNode);
sort?: boolean | ((a: TRow, b: TRow) => number);
}Kontrak "lite-TanStack" untuk satu kolom — lihat Core Concepts untuk contoh minimal dan aturan resolusi.
Contains:
id
id?: stringWajib diisi hanya kalau cell berupa fungsi tanpa label fallback — dipakai sebagai nama slot #header-{id}/#cell-{id}.
header
header?: string | ((ctx: { column: ColumnDef<TRow> }) => VNode)String literal, atau fungsi yang mengembalikan VNode untuk header custom. Tidak diisi → fallback ke label yang di-humanize (first_name → "First Name").
label
label?: stringFlat key ke data baris. Sugar — dipakai sebagai default header/cell/sort lookup sekaligus.
cell
cell?: string | ((row: TRow, index: number) => string | number | boolean | VNode)Bentuk string selalu berarti lookup key (bukan konten literal). Bentuk fungsi: return primitif dipakai juga sebagai sumber sort default; VNode tidak bisa dipakai untuk sort (butuh sort eksplisit).
sort
sort?: boolean | ((a: TRow, b: TRow) => number)true pakai comparator default (numeric untuk angka, localeCompare(..., {numeric:true}) untuk selainnya). Fungsi custom untuk comparator sendiri.
TableColor / TableColorConfig
type TableColor = TableAccent | TableColorConfig;
interface TableColorConfig {
header?: TableAccent;
footer?: TableAccent;
body?: TableBodyColor;
child?: TableBodyColor;
}Lihat Sistem Warna untuk penjelasan lengkap resolusi dan precedence dengan standalone *Color props.
TableBodyColor
type TableBodyColor =
| TableAccent
| TableAccent[]
| { odd?: TableAccent; even?: TableAccent }
| Record<number, TableAccent>;Dipakai oleh body/child — string untuk warna seragam, array untuk cycle per index, {odd, even} untuk alternasi dua warna, Record<number,> untuk mapping index eksplisit (sparse key didukung).
TableStripe
type TableStripe = boolean | 'row' | 'child';true = shading di body dan child; 'row'/'child' = batasi ke salah satu. Selalu berupa overlay box-shadow, bukan penggantian token warna — lihat Sistem Warna.
TablePerPage
type TablePerPage = number | 'all';'all' menampilkan semua baris tanpa pagination. Dipakai oleh v-model:perPage dan perPageOptions — lihat perPageOptions prop.
SortState
interface SortState {
label: string;
direction: 'asc' | 'desc';
}Shape untuk v-model:sort.
SearchLogic
type SearchLogic<TRow = any> = (row: TRow) => string | (string | number)[];Lihat searchLogic prop untuk contoh.
ExpandableConfig
type ExpandableConfig<TRow = any> = boolean | ((row: TRow, index: number) => boolean);Lihat expandable prop untuk contoh.
RowKeyResolver
type RowKeyResolver<TRow> = string | ((row: TRow, index: number) => string | number);string → flat key lookup pada baris; fungsi → key hasil komputasi. Harus stabil lintas sort/pagination — tidak boleh berbasis index array.
TableContainerLike
interface TableContainerLike<TRow = any> {
data: { value: TRow[] };
isLoading: { value: boolean };
hasError: { value: boolean };
error: { value: { message: string; code?: number | string } | null };
search: string;
sort: { sortBy: string; sortDir: 'asc' | 'desc' };
page: TableContainerPageConfig;
setSort: (sortBy: string, sortDir?: 'asc' | 'desc') => void;
submitSearch: (value?: string) => void;
navigatePage: (target: number | 'next' | 'prev') => void;
nextPage: () => void;
prevPage: () => void;
setPage: (config: Partial<TableContainerPageConfig> | null) => void;
}Subset struktural dari DataContainerObject milik @bpmlib/utils-data-container yang benar-benar dipakai SaTable — di-duck-type, bukan hard-import, supaya tetap jadi peer dependency opsional di level type juga. Lihat Container: "Dumb", Hanya Trigger.
NOTE
data, error, isLoading, hasError semuanya { value: T } — bentuk asli di package sebenarnya adalah Ref<T[]>/Ref<ErrorInfo | null>/ComputedRef<boolean>. Kalau membuat mock/implementasi custom yang mengikuti interface ini secara manual (bukan dari useDataContainer() langsung), pastikan keempat field ini tetap objek ber-.value, bukan primitif polos — SaTable selalu membaca lewat .value.
Examples
Contains:
- 1. Tabel Minimal
- 2. Local Mode Lengkap
- 3. Controlled + Manual (Async)
- 4. Integrasi :container
- 5. :tanstackTable — Data Lokal
- 6. :container + :tanstackTable
- 7. Custom Column Rendering
- 8. Kustomisasi Toolbar
- 9. Warna & Stripe
- 10. SaTableManual
1. Tabel Minimal
<SaTable :data="employees" :headers="[{ label: 'name' }, { label: 'department' }]" />2. Local Mode Lengkap
Lihat Comprehensive Example di Quick Start — search, sort, pagination, expandable rows sekaligus.
3. Controlled + Manual (Async)
<script setup lang="ts">
import { ref } from 'vue';
import { SaTable } from '@bpmlib/vue-satable';
const data = ref([]);
const isLoading = ref(false);
const totalData = ref(0);
const page = ref(1);
async function fetchPage() {
isLoading.value = true;
const res = await fetch(`/api/employees?page=${page.value}`);
const json = await res.json();
data.value = json.data;
totalData.value = json.total;
isLoading.value = false;
}
function onManipulate() {
fetchPage();
}
</script>
<template>
<SaTable
:data="data"
:headers="[{ label: 'name' }, { label: 'department' }]"
async-table
:is-loading="isLoading"
:total-data="totalData"
v-model:current-page="page"
@manipulate="onManipulate"
@refresh="fetchPage"
/>
</template>4. Integrasi :container
<script setup lang="ts">
import { useDataContainer } from '@bpmlib/utils-data-container';
import { SaTable } from '@bpmlib/vue-satable';
const container = useDataContainer({ url: '/api/employees' });
</script>
<template>
<SaTable :headers="[{ label: 'name' }, { label: 'department' }]" async-table :container="container" with-search />
</template>5. :tanstackTable — Data Lokal
<script setup lang="ts">
import { useVueTable, getCoreRowModel, getSortedRowModel, createColumnHelper } from '@tanstack/vue-table';
import { SaTable } from '@bpmlib/vue-satable';
const helper = createColumnHelper();
const table = useVueTable({
data: employees,
columns: [helper.accessor('name', {}), helper.accessor('department', {})],
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
</script>
<template>
<SaTable :tanstack-table="table" />
</template>6. :container + :tanstackTable
<script setup lang="ts">
const table = useVueTable({
data: computed(() => container.data.value),
columns,
manualSorting: true,
onSortingChange: (updater) => {
const [next] = typeof updater === 'function' ? updater([]) : updater;
if (next) container.setSort(next.id, next.desc ? 'desc' : 'asc');
},
getCoreRowModel: getCoreRowModel(),
});
</script>
<template>
<SaTable :tanstack-table="table" :is-loading="container.isLoading" />
</template>NOTE
SaTable tidak pernah menjembatani container dan tanstackTable secara otomatis — wiring onSortingChange ke container.setSort() di atas adalah kode consumer sendiri.
7. Custom Column Rendering
<script setup lang="ts">
import { h } from 'vue';
const columns = [
{ label: 'name', header: 'Nama' },
{
id: 'status',
label: 'status',
cell: (row) => h('span', { class: row.status === 'active' ? 'text-green-600' : 'text-gray-400' }, row.status),
},
];
</script>
<template>
<SaTable :data="employees" :headers="columns">
<template #customRows="{ row }">
<td colspan="2">{{ row.name }} — {{ row.status }}</td>
</template>
</SaTable>
</template>8. Kustomisasi Toolbar
<SaTable :data="employees" :headers="columns" title="Employee Directory" :on-click-filter="openFilterPanel">
<template #buttons="{ clickFilter }">
<button @click="clickFilter">Filter</button>
<button @click="exportCsv">Export CSV</button>
</template>
<template #preTable>
<div class="banner">Menampilkan {{ employees.length }} pegawai</div>
</template>
</SaTable>9. Warna & Stripe
<SaTable :data="employees" :headers="columns" color="primary" stripe />
<SaTable :data="employees" :headers="columns" :color="{ header: 'card', footer: 'card', body: ['card', 'primary'] }" />10. SaTableManual
Lihat SaTableManual di API Reference untuk contoh lengkap.
Styling
Import CSS
Library menyediakan styling lengkap secara mandiri — tidak bergantung pada class definitions dari parent project:
// main.ts atau app.ts
import '@bpmlib/vue-satable/style.css';Component akan tampil fully styled langsung setelah import, tanpa setup tambahan. Tailwind CSS tidak diperlukan di sisi consumer — seluruh class sa-td--* sudah dikompilasi jadi CSS asli saat build library.
CSS Variables
Palet warna diatur lewat CSS custom properties (triad base/alt/foreground per family), otomatis berganti di bawah class .dark:
Colors
--color-background/--color-background-foreground— kanvas halaman--color-card/--color-card-alt/--color-card-foreground— surface island (default warna body)--color-primary/--color-primary-alt/--color-primary-foreground— aksen brand (default header/footer)--color-secondary/--color-secondary-alt/--color-secondary-foreground— aksen brand kedua (dipakai juga oleh search input & page-jumper)--color-ternary/--color-ternary-alt/--color-ternary-foreground--color-success/--color-success-alt/--color-success-foreground--color-danger/--color-danger-alt/--color-danger-foreground
Usage:
:root {
--color-primary: #7c3aed;
--color-primary-alt: #6d28d9;
}Catatan: setiap nama family di atas (card, primary, secondary, ternary, success, danger) adalah value valid untuk prop color/headerColor/bodyColor/dst. — tidak ada family baru yang bisa didaftarkan di luar keenam ini tanpa override CSS variable-nya langsung.