Bulk
Preview

CSV ingestion with column mapping, inline editing, validation, and row submission.

FormBuilder.Bulk reuses the same schema and formDefs as FormBuilder.Single, then adds CSV import, header mapping, a grid editor, validation, template download, and invalid-row export.

Usage

import { z } from "zod";
import {
  FormBuilder,
  type FormDefs,
} from "@tilt-legal/cubitt-components/form-builder";
const memberSchema = z.object({
  firstName: z.string().min(1, "Required"),
  email: z.string().email(),
  role: z.enum(["admin", "member"]).default("member"),
  active: z.boolean().default(true),
});

const memberDefs = [
  {
    kind: "field",
    name: "firstName",
    label: "First name",
    component: "text",
    size: "half",
  },
  {
    kind: "field",
    name: "email",
    label: "Email",
    component: "email",
    size: "full",
  },
  {
    kind: "field",
    name: "role",
    label: "Role",
    component: "select",
    options: [
      { value: "admin", label: "Admin" },
      { value: "member", label: "Member" },
    ],
    size: "full",
  },
  {
    kind: "field",
    name: "active",
    label: "Active",
    component: "switch",
    size: "full",
  },
] as const satisfies FormDefs;
<FormBuilder.Bulk
  formDefs={memberDefs}
  onSubmit={async ({ rows }) => {
    await importMembers(rows);
  }}
  schema={memberSchema}
  template={{ filename: "members.csv" }}
/>

Examples

Basic Import

CSV Flow

  1. Drop, choose, or paste CSV data.
  2. Headers are mapped to fields by name, label, and aliases.
  3. Raw CSV rows are shaped into schema values.
  4. Rows render in a table using Cubitt form fields.
  5. Zod validation populates inline row errors.
  6. Submit receives { rows }.

Mapping Aliases

<FormBuilder.Bulk
  formDefs={memberDefs}
  mapping={{
    headerAliases: {
      firstName: ["given_name", "first"],
      active: ["enabled", "status"],
    },
  }}
  onSubmit={handleImport}
  schema={memberSchema}
/>

Template Downloads

<FormBuilder.Bulk
  formDefs={memberDefs}
  onSubmit={handleImport}
  schema={memberSchema}
  template={{
    filename: "members-template.csv",
    exampleRow: {
      firstName: "Jane",
      email: "jane@example.com",
      role: "member",
      active: true,
    },
  }}
/>

Controlled Rows

const [rows, setRows] = useState<Member[]>([]);

<FormBuilder.Bulk
  formDefs={memberDefs}
  onRowsChange={setRows}
  onSubmit={handleImport}
  rows={rows}
  schema={memberSchema}
/>

API Reference

PropTypeDefaultDescription
schemaz.ZodTypeAny-Zod schema applied to each row
formDefsFormDefs<T>-Shared definitions for columns, labels, field components, and CSV templates
onSubmit(opts: { rows: T[] }) => BulkSubmitResult<T>-Submit handler. Return a BulkSubmitFailure to display row or form errors
onSubmitInvalid() => void-Called when the default toolbar submit button is pressed while rows are invalid
parseOptions{ delimiter?, headerRow?, maxRows?, localeDecimal? }-CSV parser configuration
mapping{ headerAliases?: Record<string, string[]> }-Header aliases for auto-mapping
template{ filename?, exampleRow? }-Template download filename and optional example row
acceptstring".csv"File picker accept string
validateOnEditbooleantrueRevalidate rows while editing
rowsT[]-Controlled rows
onRowsChange(rows: T[]) => void-Row change callback
disabledbooleanfalseDisable ingest, editing, mapping, and actions
classNamestring-Layout class for the root form
idstring-Form id for external submit buttons
submitRefRef<() => void>-Imperative submit function
toolbarfalse | RenderToolbar-Hide, replace, or extend the toolbar
enableExportInvalidCsvbooleantrueShow the invalid-row export action

Toolbar Hook

FormBuilder.Bulk.useToolbar() returns a handle for external toolbar composition.

PropertyTypeDescription
actions.uploadFile() => voidOpen the file picker
actions.reviewMapping() => voidOpen the mapping dialog
actions.downloadTemplate() => voidDownload the CSV template
actions.validateAll() => voidValidate all rows
actions.submit() => voidSubmit the form
actions.exportInvalidCsv() => voidExport invalid rows
render(slot) => ReactNodePass to the toolbar prop

Error Return

type BulkSubmitFailure = {
  error?: ZodError;
  formErrors?: string[];
  rows?: Array<{
    rowIndex: number;
    fieldErrors: Record<string, string[]>;
    rowErrors: string[];
  }>;
};

On this page