Advanced Table
Orders
| # | Actions | Customer | Status | Total | Summary | |||
|---|---|---|---|---|---|---|---|---|
| No orders found. | ||||||||
Runs against an in-memory mock of the REST endpoint below (same ~450ms round trip) since this docs site has no backend — search, status filter, sort, pagination, row selection, export, edit, delete, refund, and invoice all go through it. Failed orders get the conditional red row style.
This tutorial shows advanced table capabilities.
The example uses static mode, server mode query state, columns, row numbers, search, filters, sorting, pagination, loading, empty and error states, row actions, icon-only actions, custom action icons, action grouping, column widths, minWidth, horizontal scrolling, row selection, bulk actions, export controls, column visibility, persistence, custom renderers, computed columns, and conditional row classes.
File structure
app/
orders/
page.tsx
schemas/
orders/
advanced-orders-table.schema.ts
components/
vibeflui/
renderers/
orders/
OrderStatusBadge.tsx
CurrencyCell.tsx
lib/
vibeflui/
registry.tsxSchema file
schemas/orders/advanced-orders-table.schema.ts
export const advancedOrdersTableSchema = {
resource: "orders",
title: "Orders",
mode: "server",
identity: {
key: "id",
param: "id"
},
endpoint: "/api/orders",
provider: {
type: "rest"
},
table: {
dataPath: "data",
totalPath: "total",
minWidth: 960,
rowNumber: { enabled: true, label: "#" },
actionGrouping: true,
actionIcon: {
hoverShape: "circle",
hoverColor: "brand"
},
columns: [
{ key: "orderNo", label: "Order", width: 16, searchable: true, sortable: true },
{ key: "customer", label: "Customer", width: 24, searchable: true },
{ key: "status", label: "Status", width: 16, renderer: "OrderStatusBadge" },
{ key: "total", label: "Total", width: 14, renderer: "CurrencyCell" },
{ key: "createdAt", label: "Created", width: 18, sortable: true },
{ key: "summary", label: "Summary", type: "computed", computed: "orders.summary" }
],
search: { enabled: true, placeholder: "Search orders..." },
filters: [
{
key: "status",
label: "Status",
type: "select",
options: [
{ label: "Paid", value: "paid" },
{ label: "Pending", value: "pending" },
{ label: "Failed", value: "failed" }
]
}
],
sort: {
defaultKey: "createdAt",
defaultDirection: "desc"
},
pagination: {
enabled: true,
pageSize: 10,
pageSizeOptions: [10, 25, 50]
},
rowSelection: {
enabled: true,
mode: "multiple"
},
export: {
enabled: true,
copy: true,
csv: true,
excel: true,
columnVisibility: true,
filename: "orders"
},
states: {
loading: { text: "Loading orders..." },
empty: { text: "No orders found." },
error: { text: "Unable to load orders." }
}
},
actions: {
create: { enabled: false },
view: { enabled: true, label: "View", display: "icon", icon: "mdi:eye-outline" },
edit: { enabled: true, label: "Edit", display: "icon", icon: "mdi:pencil-outline" },
delete: { enabled: true, label: "Delete", display: "icon", icon: "mdi:trash-can-outline", variant: "danger", confirm: true },
row: [
{ key: "refund", label: "Refund", display: "icon", icon: "mdi:cash-refund", handler: "orders.refund" },
{ key: "invoice", label: "Invoice", display: "icon", icon: "mdi:file-document-outline", handler: "orders.invoice" }
],
bulk: [
{ key: "export-selected", label: "Export selected", handler: "orders.exportSelected" }
]
},
persist: {
search: true,
filter: true,
sort: true,
page: true,
columnVisibility: true,
storage: "localStorage"
},
theme: {
classNameWhen: [
{
slot: "tableRow",
when: { key: "status", operator: "equals", value: "failed" },
className: "bg-red-50 text-red-950"
}
]
},
registry: {
renderers: ["OrderStatusBadge", "CurrencyCell"],
actionHandlers: ["orders.refund", "orders.invoice", "orders.exportSelected"],
computedResolvers: ["orders.summary"]
}
} as const;Renderer components
components/vibeflui/renderers/orders/OrderStatusBadge.tsx
export function OrderStatusBadge({ value }: { value?: unknown }) {
return <span>{String(value ?? "-")}</span>;
}components/vibeflui/renderers/orders/CurrencyCell.tsx
export function CurrencyCell({ value }: { value?: unknown }) {
return (
<span>
{new Intl.NumberFormat(undefined, {
style: "currency",
currency: "USD"
}).format(Number(value ?? 0))}
</span>
);
}Registry file
lib/vibeflui/registry.tsx
import { createRegistry } from "@vibeflui/core";
import { CurrencyCell } from "@/components/vibeflui/renderers/orders/CurrencyCell";
import { OrderStatusBadge } from "@/components/vibeflui/renderers/orders/OrderStatusBadge";
export const vibefluiRegistry = createRegistry({
renderers: {
OrderStatusBadge,
CurrencyCell
},
actionHandlers: {
"orders.refund": async ({ row }) => ({ status: true, code: 200, message: `Refund queued for ${String(row?.orderNo ?? "order")}` }),
"orders.invoice": async ({ row }) => ({ status: true, code: 200, message: `Invoice opened for ${String(row?.orderNo ?? "order")}` }),
"orders.exportSelected": async ({ values }) => ({ status: true, code: 200, message: `${Array.isArray(values?.selectedRows) ? values.selectedRows.length : 0} rows exported` })
},
computedResolvers: {
"orders.summary": ({ row }) => `${String(row?.status ?? "-")} / ${String(row?.customer ?? "-")}`
}
});Page file
app/orders/page.tsx
"use client";
import { FluiKit, FluiKitProvider } from "@vibeflui/core";
import { advancedOrdersTableSchema } from "@/schemas/orders/advanced-orders-table.schema";
import { vibefluiRegistry } from "@/lib/vibeflui/registry";
export default function OrdersPage() {
return (
<FluiKitProvider registry={vibefluiRegistry}>
<FluiKit schema={advancedOrdersTableSchema} />
</FluiKitProvider>
);
}Client-side fallback
For local demos, use the same schema with mode: "static" and pass data={orders}.
app/orders/page.tsx
<FluiKit schema={{ ...advancedOrdersTableSchema, mode: "static" }} data={orders} />