Forms and validation
Connect fields, initial values, validation, and patch submission.
A form catalog describes how a record is edited. Give it a stable ID, define the fields, and connect it to the table’s create or edit form type.
Use the root TableConfig.presentation option for a shared view/create/edit/bulk surface, with an optional mobile choice. The default is a right drawer on every viewport. See shared record presentation for configuration and migration.
Configuration example
Start from the React quick start or Vue quick start. The following file extends their product catalog. Pass this configuration through getTableConfig in React or config in Vue.
import { z } from "zod";
import type { FormConfig } from "@/components/ui/yayaw-table/components/forms/types";
export const productForm: FormConfig = {
id: "products",
title: "Edit product",
presentation: "drawer",
width: "38rem",
submitMode: "patch",
fields: [
{
name: "name",
label: "Name",
type: "text",
required: true,
bulkEdit: false,
schema: z.string().trim().min(1),
},
{
name: "price",
label: "Price",
type: "number",
min: 0,
schema: z.number().min(0),
},
{
name: "stock",
label: "Stock",
type: "number",
min: 0,
step: 1,
schema: z.number().int().min(0),
},
{
name: "status",
label: "Status",
type: "select",
options: [
{ value: "draft", label: "Draft" },
{ value: "active", label: "Active" },
{ value: "archived", label: "Archived" },
],
},
],
blocks: [
{
type: "content",
id: "help",
text: "Only changed fields are saved.",
tone: "info",
},
{ type: "field", name: "name" },
{
type: "section",
id: "inventory",
title: "Inventory",
columns: 2,
blocks: [
{ type: "field", name: "price" },
{ type: "field", name: "stock" },
],
},
{ type: "field", name: "status" },
],
};import { z } from "zod";
import type { FormConfig } from "@/components/ui/yayaw-table-vue/types";
export const productForm: FormConfig = {
id: "products",
title: "Edit product",
presentation: "drawer",
width: "38rem",
submitMode: "patch",
fields: [
{
name: "name",
label: "Name",
type: "text",
required: true,
bulkEdit: false,
schema: z.string().trim().min(1),
},
{
name: "price",
label: "Price",
type: "number",
min: 0,
schema: z.number().min(0),
},
{
name: "stock",
label: "Stock",
type: "number",
min: 0,
step: 1,
schema: z.number().int().min(0),
},
{
name: "status",
label: "Status",
type: "select",
options: [
{ value: "draft", label: "Draft" },
{ value: "active", label: "Active" },
{ value: "archived", label: "Archived" },
],
},
],
blocks: [
{
type: "content",
id: "help",
text: "Only changed fields are saved.",
tone: "info",
},
{ type: "field", name: "name" },
{
type: "section",
id: "inventory",
title: "Inventory",
columns: 2,
blocks: [
{ type: "field", name: "price" },
{ type: "field", name: "stock" },
],
},
{ type: "field", name: "status" },
],
};import type {
FieldValues,
FormConfig,
} from "@/components/ui/yayaw-table/components/forms/types";
import { productForm } from "./product-form";
/** The registry's generic lookup boundary resolves this known product catalogue. */
export function getFormConfig<T extends FieldValues>(
formType: string
): FormConfig<T> | undefined {
const form = formType === "products" ? productForm : undefined;
const resolved = form;
return resolved as FormConfig<T> | undefined;
}import { productForm } from "./product-form";
export const getFormConfig = (formType: string) =>
formType === "products" ? productForm : undefined;Connect the catalog
Set form.editFormType: "products" in the table catalog and pass getFormConfig to the table. Provide update through the actions catalog. The administration recipe supplies all three. For creation, use createFormType, initial defaults and a create action that returns the persisted record.
Initialize, validate, transform
defaultValues seeds a create form. loadInitialValues can hydrate an edit form asynchronously and receives an abort signal. Field schemas validate individual controls; a form schema validates the whole record. transform adapts submitted values to the write contract. submitMode: "patch" submits changed declared fields; full submission remains the default.
Conditional fields
FormConfig.rules shows, hides, requires or sets fields from other values, in create, edit and bulk edit forms. The rules have the same shape and comparisons as the Form view's conditional questions: conditions read field names (fieldId), and effects list the fields they act on in then.fieldIds. Record form rules are configured in code; there is no settings panel for them.
import type { FormConfig } from "@/components/ui/yayaw-table/components/forms/types";
// Vue: import type { FormConfig } from "@/components/ui/yayaw-table-vue/types";
import { productForm } from "./product-form";
const statusIs = (value: string) => ({
join: "and" as const,
items: [{ fieldId: "status", operator: "is" as const, value }],
});
export const exampleForm: FormConfig = {
...productForm,
rules: [
// Active products need a price.
{ id: "active-price", when: statusIs("active"), then: { action: "require", fieldIds: ["price"] } },
// Archived products no longer track stock.
{ id: "archived-stock", when: statusIs("archived"), then: { action: "hide", fieldIds: ["stock"] } },
],
};showhides its fields until one of its rules matches;hidewins overshow.requiremakes a shown field required, on top of its ownrequiredflag and schema.set({ action: "set", fieldIds, value }) writes a value before the form is validated.Hidden fields are neither validated nor submitted, in full and in
patchmode, and inline editing refuses a field that its rules hide. A hidden value counts as empty for other conditions.An error already shown under a field is checked again when the values change, so a rule that hides a field or stops requiring it never leaves a stale error blocking Save.
The fields of collection items do not inherit the parent form's rules.
Rules that cannot act, such as a condition on an unknown field or a comparison that does not fit its type, are dropped when the form loads.
Comparisons by field type
Field type | Compared as | Comparisons (operator) |
|---|---|---|
text, textarea and other text fields | Text | is, isNot, contains, notContains, startsWith |
number, currency, percent | Number | eq, neq, lt, lte, gt, gte, between |
date, datetime | Date, by day | on, before, after, between, inLast, inNext (a number of days) |
select, radio, select-with-add-new, tag | Single choice | is, isNot, isAnyOf, isNoneOf |
multiSelect, tags | Multiple choice | containsAny, containsAll, containsNone |
boolean, checkbox, switch | Yes/no | isChecked, isUnchecked |
Every type but yes/no also accepts isEmpty and isNotEmpty. between takes [from, to], where one end may be null; the list comparisons take an array of option values.
Bulk edit and mixed values
In bulk edit, conditions read the value set in the bulk draft, or else the value that every selected row shares. When the selected rows hold different values for a field that is not in the draft, that field is "mixed" and any condition on it counts as not matching:
A field hidden only because of mixed values is listed, disabled, in Add a field, with a note such as "Depends on Category, whose values differ across the selection. Set Category first.".
A field already added whose rules read mixed values shows "Values of Category differ across the selection: the condition is treated as not met." under it.
Adding the mixed field to the draft gives it one value for every row, and the conditions then read that value. The notes have built-in English and French text.
Migrating from the hidden flag
Fields keep their hidden option, true or a function (context) => boolean. When the form loads, it is converted into a rule with a code-only condition that receives the full context and the values as given, so existing forms behave as before. These code conditions are never saved as JSON. Code that reads hidden outside a form, such as a cell renderer, keeps calling the function directly.
A hidden function that only reads other values can become a rule:
// Before: a predicate on the field.
{ name: "stock", label: "Stock", type: "number", hidden: ({ values }) => values?.status === "archived" }
// After: a declarative rule on the form.
rules: [
{
id: "archived-stock",
when: { join: "and", items: [{ fieldId: "status", operator: "is", value: "archived" }] },
then: { action: "hide", fieldIds: ["stock"] },
},
],Declarative rules are worth the move: they treat hidden values as empty, so chains settle; they can require or set fields; and in bulk edit they read the values the selection shares and explain mixed values, while a legacy predicate keeps running row by row. Keep a function when the decision depends on something other than the values, such as the row, the mode or the user.
Choose the next feature
Continue with layout, asynchronous options, table pickers, collections, inline editing, or bulk editing. The form reference retains detailed field contracts and examples.
Previous guide sections
Forms, Sections, Multi-select, and Collection Fields
Shared React and Vue form contract
Table picker
Initialization, options, and submission
Form blocks
Multi-select Fields
Form Sections
What Collection Fields Enable
Mental Model
Quick Example
API Reference
Multiple Item Types
Nested Collections
Validation Layers
Modal and Drawer Forms
Translation and Labels
When to Use Custom Instead
Common Pitfalls
Related Docs
Generated fields and JSON
Record consultation
Append-only history and undo
Test record behavior in the Yayaw example
A compact two-field modal
Return exampleForm from the existing getFormConfig resolver for products; keep the same update action. Each variant below is a complete file extending the product form above.
import type { FormConfig } from "@/components/ui/yayaw-table/components/forms/types";
import { productForm } from "./product-form";
export const exampleForm: FormConfig = {
...productForm,
presentation: "modal",
width: "32rem",
blocks: undefined,
fields: productForm.fields.filter((field) =>
["name", "price"].includes(field.name)
),
};import type { FormConfig } from "@/components/ui/yayaw-table-vue/types";
import { productForm } from "./product-form";
export const exampleForm: FormConfig = {
...productForm,
presentation: "modal",
width: "32rem",
blocks: undefined,
fields: productForm.fields.filter((field) =>
["name", "price"].includes(field.name)
),
};Require an explicit stock value
Keep the integer and non-negative schema, and additionally require a value. Zero is valid; a blank or fractional stock is not. A failed action retains the user’s draft.
import type { FormConfig } from "@/components/ui/yayaw-table/components/forms/types";
import { productForm } from "./product-form";
export const exampleForm: FormConfig = {
...productForm,
fields: productForm.fields.map((field) =>
field.name === "stock" ? { ...field, required: true } : field
),
};import type { FormConfig } from "@/components/ui/yayaw-table-vue/types";
import { productForm } from "./product-form";
export const exampleForm: FormConfig = {
...productForm,
fields: productForm.fields.map((field) =>
field.name === "stock" ? { ...field, required: true } : field
),
};