Skip to content

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)

npm versionTypeScriptVueTailwind CSS


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:

ts
import { SaTable, SaTableManual } from '@bpmlib/vue-satable';

Types:

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

bash
npm install vue@^3.3.0

Opsional:

bash
npm install @tanstack/vue-table@^8.0.0
npm install @bpmlib/utils-data-container@^0.4.0
DependencyVersiStatusDeskripsi
vue^3.3.0RequiredVue 3 framework
@tanstack/vue-table^8.0.0OptionalAktifkan :tanstackTable — lazy-loaded, hanya di-fetch saat prop ini benar-benar dipakai
@bpmlib/utils-data-container^0.4.0OptionalAktifkan :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

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

Import

Basic Import:

ts
import { SaTable, SaTableManual } from '@bpmlib/vue-satable';

CSS Import:

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

vue
<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 membaca row.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:

vue
<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.
  • expandable predikat menentukan baris mana yang boleh di-expand; rowKey wajib diisi begitu expandable truthy.

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:

ModeDitandai denganSiapa yang hitung sort/search/page
localTidak ada asyncTable/container/tanstackTableSaTable sendiri, dari data penuh
controlled + manualasyncTable (tanpa container)Consumer — data sudah berupa halaman aktif, SaTable emit manipulate
controlled + containerasyncTable + containercontainer (instance useDataContainer()-shaped)
tanstackTabletanstackTable diisiTanStack — 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

NameTypeDefaultDescription
dataTRow[]Dataset penuh (local) atau halaman aktif (controlled+manual). Overshadowed oleh container/tanstackTable
headersColumnDef[]Definisi kolom. Wajib kecuali tanstackTable diisi
withIndexbooleanfalseTampilkan kolom nomor urut di paling kiri
withSearchbooleanfalseTampilkan input search bawaan — lihat Explicit-Submit Search
searchLogicSearchLogicinclusive whole-row scanPredikat search top-level Lihat selengkapnya
perPageOptionsTablePerPage[][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
rowKeyRowKeyResolverWajib kalau expandable truthy dan tanstackTable tidak diisi
expandableExpandableConfigfalseboolean atau predikat per baris Lihat selengkapnya
colorTableColorString menyasar body saja; object override per region Lihat selengkapnya
headerColorTableAccent'primary'Menang atas color.header
footerColorTableAccent'primary'Menang atas color.footer
bodyColorTableBodyColor'card'Menang atas color.body
childColorTableBodyColoralt-toggle dari bodyMenang atas color.child
stripeTableStripefalseOverlay box-shadow, bukan penggantian token warna Lihat selengkapnya
asyncTablebooleanfalseDiscriminator eksplisit local vs controlled — lihat Core Concepts
isLoadingbooleanfalseMode controlled+manual saja. Overshadowed oleh container
isErrorbooleanfalseMode controlled+manual saja. Overshadowed oleh container
errorMessagestring''Mode controlled+manual saja. Overshadowed oleh container
totalDatanumberMode controlled+manual saja. Overshadowed oleh container.page.totalData
containerTableContainerLikeInstance useDataContainer()-shaped Lihat selengkapnya
onClickFilter() => voidTrigger UI murni — tampilkan tombol filter Lihat selengkapnya
titlestringFallback plain-text untuk slot #title
tanstackTableTable<any>Instance useVueTable() Lihat selengkapnya
searchLogic

Predikat pencarian top-level, menggantikan searchable/searchLogic per-kolom sepenuhnya.

Signature:

ts
searchLogic: (row: TRow) => string | (string | number)[]

Contoh:

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

ts
perPageOptions: TablePerPage[]

Contoh:

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

ts
expandable: boolean | ((row: TRow, index: number) => boolean)

Contoh:

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

ts
type TableColor = TableAccent | {
  header?: TableAccent;
  footer?: TableAccent;
  body?: TableBodyColor;
  child?: TableBodyColor;
};

Contoh:

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

ts
stripe: boolean | 'row' | 'child'

Contoh:

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

ts
container: TableContainerLike<TRow>

Contoh:

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

ts
onClickFilter: () => void

Contoh:

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

ts
tanstackTable: Table<any>

Contoh:

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

NameTypeDefaultDescription
v-model:searchstring''Live binding input search — filter baru jalan saat submit, lihat Core Concepts
v-model:currentPagenumber1Halaman aktif
v-model:sortSortState | nullnullState sort aktif
v-model:perPagenumber20Jumlah baris per halaman
v-model:expandedRowsArray<string | number>[]Key baris yang sedang di-expand

Events

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

ts
manipulate: (payload: { reason: string }) => void

Nilai reason:

ValueDipicu saatData/prop/v-model terkait
initKomponen pertama kali mounteddata/totalData — biasanya sinyal "muat halaman pertama"
searchSearch di-submit (Enter, tombol search, atau tombol clear)v-model:search, searchLogic
sortState sort berubah (klik header sortable)v-model:sort
pageHalaman aktif berubah (tombol first/prev/next/last, page-jumper, atau v-model:currentPage dari luar)v-model:currentPage
per-pageJumlah baris per halaman berubahv-model:perPage, perPageOptions
refreshTombol refresh footer ditekan — lihat catatan di bawahTidak ada state yang berubah — murni sinyal "muat ulang data yang sama"

Kapan dipicu, kapan tidak:

  • Hanya aktif di mode controlled (asyncTable truthy) — di mode local, 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.), SaTable tidak menjembataninya ke manipulate.
  • Di mode :container, sebagian besar reason (search/sort/page/per-page) tetap terkirim sebagai sinyal tambahan, meski container sendiri sudah otomatis re-fetch lewat method-nya masing-masing (setSort, submitSearch, dst.) — manipulate di 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:

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

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

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

NamePropsDescription
default-Markup <table> custom sepenuhnya milik consumer

Contoh:

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

ts
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
ts
id?: string

Wajib diisi hanya kalau cell berupa fungsi tanpa label fallback — dipakai sebagai nama slot #header-{id}/#cell-{id}.


header
ts
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
ts
label?: string

Flat key ke data baris. Sugar — dipakai sebagai default header/cell/sort lookup sekaligus.


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

ts
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

ts
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

ts
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

ts
type TablePerPage = number | 'all';

'all' menampilkan semua baris tanpa pagination. Dipakai oleh v-model:perPage dan perPageOptions — lihat perPageOptions prop.


SortState

ts
interface SortState {
  label: string;
  direction: 'asc' | 'desc';
}

Shape untuk v-model:sort.


SearchLogic

ts
type SearchLogic<TRow = any> = (row: TRow) => string | (string | number)[];

Lihat searchLogic prop untuk contoh.


ExpandableConfig

ts
type ExpandableConfig<TRow = any> = boolean | ((row: TRow, index: number) => boolean);

Lihat expandable prop untuk contoh.


RowKeyResolver

ts
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

ts
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

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

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

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

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

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

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

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

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

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

css
: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.