Advanced Form and Table
Order operations
| # | Actions | Customer | Status | Total | ||
|---|---|---|---|---|---|---|
| 1 | ORD-1001 | Ada Supply Co. | paid | $1,290.00 | ||
| 2 | ORD-1002 | Grace Studio | pending | $840.00 |
Create, edit, and delete all run against local React state here — the email validator only accepts example.test addresses, Status options load from a registry option loader, and both Edit and Delete are gated behind the granted orders.write / orders.delete permissions.
This tutorial combines an advanced table, advanced form, CRUD operations, permissions, custom messages, validation, transforms, dynamic options, delete confirmation, and backend response shapes.
Use this as the production-like CRUD example for order operations.
File structure
app/
orders/
page.tsx
schemas/
orders/
order.resource.schema.ts
components/
vibeflui/
renderers/
orders/
OrderStatusBadge.tsx
OrderTotalCell.tsx
lib/
vibeflui/
examples/
order-crud-actions.ts
registry.tsxSchema file
schemas/orders/order.resource.schema.ts
export const orderResourceSchema = {
resource: "orders",
title: "Order operations",
mode: "static",
identity: { key: "id", param: "id" },
table: {
columns: [
{ key: "orderNo", label: "Order", searchable: true, sortable: true },
{ key: "customer", label: "Customer", searchable: true },
{ key: "status", label: "Status", renderer: "OrderStatusBadge" },
{ key: "total", label: "Total", renderer: "OrderTotalCell" }
],
search: { enabled: true },
filters: [{ key: "status", label: "Status", type: "select", optionLoader: "orders.statuses" }],
pagination: { enabled: true, pageSize: 10 },
rowSelection: { enabled: true, mode: "multiple" }
},
form: {
mode: "modal",
width: "2xl",
grid: { columns: { base: 1, md: 2 } },
fields: [
{ name: "orderNo", label: "Order number", required: true, transformer: "trim" },
{ name: "customer", label: "Customer", required: true, transformer: "trim" },
{ name: "email", label: "Customer email", type: "email", required: true, validator: "orders.email" },
{ name: "status", label: "Status", type: "select", optionLoader: "orders.statuses" },
{ name: "total", label: "Total", type: "number", min: 0 }
]
},
detail: {
enabled: true,
mode: "drawer",
fields: ["orderNo", "customer", "email", "status", "total"]
},
actions: {
create: { enabled: true, label: "Create order" },
edit: { enabled: true, permission: "orders.write" },
delete: {
enabled: true,
variant: "danger",
permission: "orders.delete",
confirm: {
title: "Delete order?",
description: "This removes the order from the local list.",
secondConfirm: {
title: "Confirm deletion",
confirmLabel: "Delete order"
}
}
},
row: [
{ key: "invoice", label: "Invoice", icon: "mdi:file-document-outline", handler: "orders.invoice" }
],
bulk: [
{ key: "bulk-export", label: "Export selected", handler: "orders.bulkExport" }
]
},
messages: {
defaults: {
createSuccess: "Order created.",
updateSuccess: "Order updated.",
deleteSuccess: "Order deleted."
}
},
feedback: {
mode: "modal"
},
registry: {
renderers: ["OrderStatusBadge", "OrderTotalCell"],
validators: ["orders.email"],
transformers: ["trim"],
optionLoaders: ["orders.statuses"],
actionHandlers: ["orders.invoice", "orders.bulkExport"],
permissionResolvers: ["orders.write", "orders.delete"]
}
} 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/OrderTotalCell.tsx
export function OrderTotalCell({ 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 { OrderStatusBadge } from "@/components/vibeflui/renderers/orders/OrderStatusBadge";
import { OrderTotalCell } from "@/components/vibeflui/renderers/orders/OrderTotalCell";
export const vibefluiRegistry = createRegistry({
renderers: {
OrderStatusBadge,
OrderTotalCell
},
validators: {
"orders.email": async (value) =>
String(value ?? "").endsWith("@example.test") ? undefined : "Use an example.test email"
},
transformers: {
trim: (value) => (typeof value === "string" ? value.trim() : value)
},
optionLoaders: {
"orders.statuses": async () => [
{ label: "Paid", value: "paid" },
{ label: "Pending", value: "pending" },
{ label: "Failed", value: "failed" }
]
},
permissionResolvers: {
"orders.write": ({ permissionContext }) => Boolean(permissionContext?.permissions?.includes("orders.write")),
"orders.delete": ({ permissionContext }) => Boolean(permissionContext?.permissions?.includes("orders.delete"))
},
actionHandlers: {
"orders.invoice": async ({ row }) => ({ status: true, code: 200, message: `Invoice opened for ${String(row?.orderNo ?? "order")}` }),
"orders.bulkExport": async ({ values }) => ({ status: true, code: 200, message: `${Array.isArray(values?.selectedRows) ? values.selectedRows.length : 0} orders exported` })
}
});Reusable action helpers
lib/vibeflui/examples/order-crud-actions.ts
import type { Dispatch, SetStateAction } from "react";
import type { FluiKitSubmitContext } from "@vibeflui/core";
export type OrderRow = {
id: string;
orderNo: string;
customer: string;
email: string;
status: string;
total: number;
};
export const initialOrders: OrderRow[] = [
{ id: "ord_1", orderNo: "ORD-1001", customer: "Ada Supply Co.", email: "ada@example.test", status: "paid", total: 1290 },
{ id: "ord_2", orderNo: "ORD-1002", customer: "Grace Studio", email: "grace@example.test", status: "pending", total: 840 }
];
export function createOrderCrudActions(setOrders: Dispatch<SetStateAction<OrderRow[]>>) {
return {
createOrder: async (values: Record<string, unknown>) => {
const order = mapValuesToOrder(values);
setOrders((current) => [...current, order]);
return { status: true, code: 200, message: "Order created" };
},
updateOrder: async (values: Record<string, unknown>, context: FluiKitSubmitContext) => {
const id = String(context.row?.id ?? "");
const nextOrder = mapValuesToOrder(values, id);
setOrders((current) => current.map((order) => (order.id === id ? nextOrder : order)));
return { status: true, code: 200, message: "Order updated" };
},
deleteOrder: async (row: Record<string, unknown>) => {
const id = String(row.id ?? "");
setOrders((current) => current.filter((order) => order.id !== id));
return { status: true, code: 200, message: "Order deleted" };
}
};
}
function mapValuesToOrder(values: Record<string, unknown>, id = crypto.randomUUID()): OrderRow {
return {
id,
orderNo: String(values.orderNo ?? ""),
customer: String(values.customer ?? ""),
email: String(values.email ?? ""),
status: String(values.status ?? "pending"),
total: Number(values.total ?? 0)
};
}Page file
app/orders/page.tsx
"use client";
import { useState } from "react";
import { FluiKit, FluiKitProvider } from "@vibeflui/core";
import { orderResourceSchema } from "@/schemas/orders/order.resource.schema";
import { vibefluiRegistry } from "@/lib/vibeflui/registry";
import { createOrderCrudActions, initialOrders } from "@/lib/vibeflui/examples/order-crud-actions";
export default function OrdersPage() {
const [rows, setRows] = useState(initialOrders);
const orderActions = createOrderCrudActions(setRows);
return (
<FluiKitProvider
registry={vibefluiRegistry}
permissionContext={{ permissions: ["orders.write", "orders.delete"] }}
>
<FluiKit
schema={orderResourceSchema}
data={rows}
onCreate={orderActions.createOrder}
onUpdate={orderActions.updateOrder}
onDelete={orderActions.deleteOrder}
/>
</FluiKitProvider>
);
}Response samples
{ "status": true, "code": 200, "message": "Order updated" }{ "status": false, "code": 409, "message": "Order is already fulfilled" }This response maps to warning feedback because 409 is a 400..499 code.
{ "status": false, "code": 500, "message": "Unable to update order" }