Advanced Form
Step through Catalog then Inventory — Category and Status load their options from registry option loaders, Notes uses the custom field component below, and Slug is computed from Product name behind the scenes.
This tutorial uses an inventory product form to show advanced form capabilities beyond a user-management screen.
The example uses wizard steps, optional sections and groups, grid columns from 1 to 6, built-in validation, transformers, computed values, option loaders, a custom field component, create/update handlers, and form surface modes such as modal, drawer, page, and inline.
File structure
schemas/
products/
advanced-product-form.schema.ts
components/
vibeflui/
fields/
ProductNotesField.tsx
lib/
vibeflui/
examples/
product-form-actions.ts
registry.tsx
app/
products/
new/
page.tsxSchema file
schemas/products/advanced-product-form.schema.ts
export const advancedProductFormSchema = {
resource: "products",
title: "Product inventory form",
identity: {
key: "id",
param: "id"
},
table: {
enabled: false
},
layout: {
type: "wizard",
steps: [
{ key: "catalog", title: "Catalog", fields: ["sku", "name", "category"] },
{ key: "inventory", title: "Inventory", fields: ["stock", "reorderPoint", "status", "notes"] }
]
},
form: {
mode: "page",
width: "3xl",
grid: {
columns: { base: 1, md: 2, lg: 3 },
gap: "gap-4"
},
fields: [
{ name: "sku", label: "SKU", required: true, transformer: "trim" },
{ name: "name", label: "Product name", required: true, minLength: 2, transformer: "trim" },
{ name: "category", label: "Category", type: "select", required: true, optionLoader: "products.categories" },
{ name: "stock", label: "Stock", type: "number", defaultValue: 0, min: 0 },
{ name: "reorderPoint", label: "Reorder point", type: "number", defaultValue: 10, min: 0 },
{ name: "status", label: "Status", type: "select", optionLoader: "products.statuses" },
{ name: "notes", label: "Notes", type: "custom", component: "ProductNotesField", maxLength: 240 },
{ name: "slug", label: "Slug", type: "hidden", computed: "products.slug" }
]
},
actions: {
create: { enabled: true, handler: "products.create" },
edit: { enabled: true, handler: "products.update" }
},
feedback: {
mode: "inline"
},
registry: {
components: ["ProductNotesField"],
transformers: ["trim"],
optionLoaders: ["products.categories", "products.statuses"],
actionHandlers: ["products.create", "products.update"],
computedResolvers: ["products.slug"]
}
} as const;Custom field component
components/vibeflui/fields/ProductNotesField.tsx
type ProductNotesFieldProps = {
value?: unknown;
onChange?: (value: string) => void;
};
export function ProductNotesField({ value, onChange }: ProductNotesFieldProps) {
return (
<textarea
className="min-h-24 w-full rounded-md border px-3 py-2"
value={String(value ?? "")}
onChange={(event) => onChange?.(event.target.value)}
/>
);
}Action helpers
lib/vibeflui/examples/product-form-actions.ts
import type { FluiKitRuntimeRegistry, NormalizedFluiKitConfig } from "@vibeflui/core";
import { executeOperation } from "@vibeflui/core";
export async function submitProductForm(input: {
schema: NormalizedFluiKitConfig;
values: Record<string, unknown>;
registry: FluiKitRuntimeRegistry;
}) {
return executeOperation({
schema: input.schema,
operation: "create",
values: input.values,
registry: input.registry
});
}Registry file
lib/vibeflui/registry.tsx
import { createRegistry } from "@vibeflui/core";
import { ProductNotesField } from "@/components/vibeflui/fields/ProductNotesField";
export const vibefluiRegistry = createRegistry({
components: {
ProductNotesField
},
transformers: {
trim: (value) => (typeof value === "string" ? value.trim() : value)
},
optionLoaders: {
"products.categories": async () => [
{ label: "Hardware", value: "hardware" },
{ label: "Software", value: "software" },
{ label: "Services", value: "services" }
],
"products.statuses": async () => [
{ label: "Draft", value: "draft" },
{ label: "Active", value: "active" },
{ label: "Discontinued", value: "discontinued" }
]
},
actionHandlers: {
"products.create": async ({ values }) => ({ status: true, code: 200, message: "Product created", data: values }),
"products.update": async ({ values }) => ({ status: true, code: 200, message: "Product updated", data: values })
},
computedResolvers: {
"products.slug": ({ values }) =>
String(values?.name ?? "")
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "")
}
});Page file
app/products/new/page.tsx
"use client";
import { useState } from "react";
import {
FluiKitFeedback,
FluiKitForm,
FluiKitProvider,
normalizeSchema,
type FluiKitFeedbackMessage
} from "@vibeflui/core";
import { advancedProductFormSchema } from "@/schemas/products/advanced-product-form.schema";
import { submitProductForm } from "@/lib/vibeflui/examples/product-form-actions";
import { vibefluiRegistry } from "@/lib/vibeflui/registry";
const schema = normalizeSchema(advancedProductFormSchema);
export default function AdvancedProductFormPage() {
const [feedback, setFeedback] = useState<FluiKitFeedbackMessage | null>(null);
const handleSubmitProduct = async (values: Record<string, unknown>) => {
const result = await submitProductForm({
schema,
values,
registry: vibefluiRegistry
});
setFeedback({
status: "success",
title: "Saved",
message: String((result.data as { message?: unknown } | undefined)?.message ?? "Product saved")
});
};
return (
<FluiKitProvider registry={vibefluiRegistry}>
<FluiKitFeedback
schema={schema}
mode="inline"
feedback={feedback}
onClose={() => setFeedback(null)}
/>
<FluiKitForm
schema={schema}
mode="create"
onSubmit={handleSubmitProduct}
/>
</FluiKitProvider>
);
}Mode variants
Change form.mode to test the same fields in different surfaces:
schemas/products/advanced-product-form.schema.ts
form: {
mode: "modal"
}Supported form modes are modal, drawer, page, and inline.
Sections and groups
Use form.sections when you want named visual sections:
schemas/products/advanced-product-form.schema.ts
form: {
sections: [
{ key: "catalog", title: "Catalog", fields: ["sku", "name", "category"] },
{ key: "inventory", title: "Inventory", fields: ["stock", "reorderPoint", "status"] }
]
}Use form.groups when you want fieldsets:
schemas/products/advanced-product-form.schema.ts
form: {
groups: [
{ title: "Catalog", fields: ["sku", "name", "category"] },
{ title: "Inventory", fields: ["stock", "reorderPoint", "status"] }
]
}Render sections, groups, tabs, and wizard steps as separate variants. The core form renderer picks one layout branch at a time.