Simple Form and Table
Users
| # | Actions | Role | Status | ||
|---|---|---|---|---|---|
| 1 | Ada Lovelace | ada@example.test | admin | active | |
| 2 | Grace Hopper | grace@example.test | editor | pending |
Create, edit, and delete all run against local React state here — try adding a user with the modal form, then editing or deleting the row it creates.
This tutorial combines one table, one create/edit form, and delete behavior with local client-side data.
Use this for local demos, admin prototypes, or offline examples where the data can live in component state.
Lifecycle
- The table displays local rows.
- The create action opens the form.
- The edit action opens the same form with selected row values.
- Save creates or updates local state.
- Delete confirms, then removes the row.
File structure
app/
users/
page.tsx
schemas/
users/
user.resource.schema.ts
components/
vibeflui/
renderers/
status/
StatusBadge.tsx
lib/
vibeflui/
examples/
user-crud-actions.ts
registry.tsxSchema file
schemas/users/user.resource.schema.ts
export const userResourceSchema = {
resource: "users",
title: "Users",
identity: {
key: "id",
param: "id"
},
table: {
columns: [
{ key: "name", label: "Name", searchable: true, sortable: true },
{ key: "email", label: "Email", searchable: true },
{ key: "role", label: "Role" },
{ key: "status", label: "Status", renderer: "StatusBadge" }
],
search: {
enabled: true
},
pagination: {
enabled: true,
pageSize: 10
}
},
form: {
mode: "modal",
fields: [
{ name: "name", label: "Name", required: true },
{ name: "email", label: "Email", type: "email", required: true },
{
name: "role",
label: "Role",
type: "select",
options: [
{ label: "Admin", value: "admin" },
{ label: "Editor", value: "editor" },
{ label: "Viewer", value: "viewer" }
]
},
{
name: "status",
label: "Status",
type: "select",
defaultValue: "active",
options: [
{ label: "Active", value: "active" },
{ label: "Pending", value: "pending" },
{ label: "Suspended", value: "suspended" }
]
}
]
},
actions: {
create: {
enabled: true,
label: "Create user"
},
edit: {
enabled: true,
label: "Edit"
},
delete: {
enabled: true,
label: "Delete",
variant: "danger",
confirm: true
}
},
registry: {
renderers: ["StatusBadge"]
}
} as const;Renderer component
components/vibeflui/renderers/status/StatusBadge.tsx
export function StatusBadge({ value }: { value?: unknown }) {
return <span>{String(value ?? "-")}</span>;
}Registry file
lib/vibeflui/registry.tsx
import { createRegistry } from "@vibeflui/core";
import { StatusBadge } from "@/components/vibeflui/renderers/status/StatusBadge";
export const vibefluiRegistry = createRegistry({
renderers: {
StatusBadge
}
});Reusable action helpers
lib/vibeflui/examples/user-crud-actions.ts
import type { Dispatch, SetStateAction } from "react";
import type { FluiKitSubmitContext } from "@vibeflui/core";
export type UserRow = {
id: string;
name: string;
email: string;
role: string;
status: string;
};
export const initialUsers: UserRow[] = [
{ id: "usr_1", name: "Ada Lovelace", email: "ada@example.test", role: "admin", status: "active" },
{ id: "usr_2", name: "Grace Hopper", email: "grace@example.test", role: "editor", status: "pending" }
];
export function createUserCrudActions(setUsers: Dispatch<SetStateAction<UserRow[]>>) {
return {
createUser: async (values: Record<string, unknown>) => {
const user = mapValuesToUser(values);
setUsers((current) => [...current, user]);
return { status: true, code: 200, message: "User created" };
},
updateUser: async (values: Record<string, unknown>, context: FluiKitSubmitContext) => {
const id = String(context.row?.id ?? "");
const nextValues = mapValuesToUser(values, id);
setUsers((current) =>
current.map((user) => (user.id === id ? nextValues : user))
);
return { status: true, code: 200, message: "User updated" };
},
deleteUser: async (row: Record<string, unknown>) => {
const id = String(row.id ?? "");
setUsers((current) => current.filter((user) => user.id !== id));
return { status: true, code: 200, message: "User deleted" };
}
};
}
function mapValuesToUser(values: Record<string, unknown>, id = crypto.randomUUID()): UserRow {
return {
id,
name: String(values.name ?? ""),
email: String(values.email ?? ""),
role: String(values.role ?? "viewer"),
status: String(values.status ?? "active")
};
}Page file
app/users/page.tsx
"use client";
import { useState } from "react";
import { FluiKit, FluiKitProvider } from "@vibeflui/core";
import { userResourceSchema } from "@/schemas/users/user.resource.schema";
import { vibefluiRegistry } from "@/lib/vibeflui/registry";
import { createUserCrudActions, initialUsers } from "@/lib/vibeflui/examples/user-crud-actions";
export default function UsersPage() {
const [users, setUsers] = useState(initialUsers);
const userActions = createUserCrudActions(setUsers);
return (
<FluiKitProvider registry={vibefluiRegistry}>
<FluiKit
schema={userResourceSchema}
data={users}
onCreate={userActions.createUser}
onUpdate={userActions.updateUser}
onDelete={userActions.deleteUser}
/>
</FluiKitProvider>
);
}What renders
The page renders a local CRUD screen with a searchable table, modal create/edit form, custom status renderer, and delete confirmation. The table data updates from React state, while the schema stays declarative and the mutation functions stay in reusable helpers.