VVibeFlui

Advanced Form

Generated UI previewWIZARD FORM
1. Catalog2. Inventory

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

TXT
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.tsx

Schema file

schemas/products/advanced-product-form.schema.ts

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

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

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

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

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

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

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

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.