VVibeFlui

Advanced Table

Generated UI previewSERVER TABLE

Orders

0 selected
#ActionsCustomerStatusTotalSummary
No orders found.
Page 1 of 1 · Total: 0

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

TXT
app/
  orders/
    page.tsx
schemas/
  orders/
    advanced-orders-table.schema.ts
components/
  vibeflui/
    renderers/
      orders/
        OrderStatusBadge.tsx
        CurrencyCell.tsx
lib/
  vibeflui/
    registry.tsx

Schema file

schemas/orders/advanced-orders-table.schema.ts

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

TSX
export function OrderStatusBadge({ value }: { value?: unknown }) {
  return <span>{String(value ?? "-")}</span>;
}

components/vibeflui/renderers/orders/CurrencyCell.tsx

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

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

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

TSX
<FluiKit schema={{ ...advancedOrdersTableSchema, mode: "static" }} data={orders} />