| title | Plugin Form |
|---|
import { InteractiveDemo } from '@/app/components/InteractiveDemo'; import { PluginLoader } from '@/app/components/PluginLoader';
Advanced form components with validation, multi-step forms, and comprehensive field support.
npm install @object-ui/plugin-form<PluginLoader plugins={['form']}>
- Form Builder - Create complex forms from schemas
- Validation - Built-in validation with custom rules
- Multi-Step Forms - Wizard-style multi-step forms
- Field Types - All standard HTML5 input types
- Form State - Automatic state management
- Error Handling - Field-level and form-level errors
{
type: 'form',
fields: FormField[],
submitLabel?: string,
cancelLabel?: string,
onSubmit?: (data) => void,
onCancel?: () => void,
className?: string
}
interface FormField {
name: string;
type: string; // 'input', 'select', 'checkbox', etc.
label: string;
placeholder?: string;
required?: boolean;
validation?: ValidationRule[];
defaultValue?: any;
disabled?: boolean;
className?: string;
}
A sectioned form renders as ONE grid. The form view's columns (spec
FormView.columns) sets how wide that grid is; a section's columns sets how
densely that section fills it. The view's value wins; without it the grid takes
the widest section's density, and without either it is single-column.
ObjectForm (simple), ModalForm, TabbedForm, SplitForm and WizardForm all
resolve it that way, so the same metadata lays out identically in every host
(WizardForm has no widest-section fallback — its steps never share a viewport,
so each keeps its own authored width). The grid is applied to the field container
inside the form, never wrapped around the <form>.
A sectioned form stays one form: declare the tabs on it and the renderer distributes the fields into panels, instead of rendering a form per section (which strands every section but the first outside the submit, and lets an inactive tab unmount along with its values).
{
type: 'form',
fields: [/* every tab's fields, in one flat list */],
fieldTabs: [
{ key: 'basics', label: 'Basics', fields: ['subject', 'status'] },
{ key: 'detail', label: 'Detail', description: 'Anything else', fields: ['description'] },
],
defaultFieldTab?: 'basics', // defaults to the first tab
fieldTabsPosition?: 'top', // 'top' | 'bottom' | 'left' | 'right'
}
- Panels are force-mounted and only CSS-hidden, so a tab the user leaves keeps its values and its validation.
- A failed submit activates the tab holding the first offending field and marks
every tab with a rejected field — client rules and server
fields[]alike. - Fields no tab claims render above the tab strip rather than disappearing.
- Needs at least two tabs; ignored when the form uses
children.
ModalForm (contentLayout: 'tabbed') and TabbedForm build on this.
allowSkip lets the user jump to any step from the indicator instead of walking
through them in order. It is navigation freedom, not an exemption from the
object's rules: the final submit checks every step's required fields, and if
something is outstanding it returns the user to the first step that has one, marks
that step's indicator (data-error="true"), and names the fields in a toast —
nothing is sent. Conditional rules are respected (visibleWhen / requiredWhen
are evaluated on the same canonical engine as the renderer and the server).
This matters because react-hook-form only validates the fields currently mounted, and a wizard mounts one step at a time — so a required field on a step nobody opened used to be absent from the payload with nothing on screen saying so.
Side-by-side panels follow the same rule: the <form> wraps the whole panel
group and each pane holds only fields, so one react-hook-form instance spans the
divider.
{
type: 'form',
fields: [/* every pane's fields, in one flat list */],
fieldPanes: [
{ key: 'primary', fields: ['subject'], defaultSize: 50 },
{ key: 'secondary', fields: ['status', 'priority'], defaultSize: 50, minSize: 20 },
],
fieldPanesOrientation?: 'horizontal', // 'horizontal' | 'vertical'
fieldPanesResizable?: true, // false pins the divider
}
- A submit from anywhere carries every pane's values, and a field rule in one pane can read a field in another — neither works with a form per panel.
defaultSize/minSizeare percentages of the group.- Each pane is its own
@container, so a multi-column group collapses as the divider is dragged narrower. - Fields no pane claims render above the panel group rather than disappearing.
- Needs at least two panes; ignored when the form uses
childrenorfieldTabs.
SplitForm builds on this: section 1 becomes the primary pane, the rest stack in
the secondary one behind inline section headers.
import '@object-ui/plugin-form';
import { formComponents } from '@object-ui/plugin-form';
import { ComponentRegistry } from '@object-ui/core';
Object.entries(formComponents).forEach(([type, component]) => {
ComponentRegistry.register(type, component);
});
{
"type": "form",
"fields": [
{
"name": "username",
"type": "input",
"label": "Username",
"required": true,
"validation": [
{ "type": "minLength", "value": 3, "message": "Min 3 characters" },
{ "type": "maxLength", "value": 20, "message": "Max 20 characters" }
]
},
{
"name": "password",
"type": "input",
"inputType": "password",
"label": "Password",
"required": true,
"validation": [
{ "type": "minLength", "value": 8, "message": "Min 8 characters" }
]
}
],
"submitLabel": "Sign Up"
}{
"type": "multi-step-form",
"steps": [
{
"title": "Personal Info",
"fields": [
{ "name": "firstName", "type": "input", "label": "First Name", "required": true },
{ "name": "lastName", "type": "input", "label": "Last Name", "required": true }
]
},
{
"title": "Contact Info",
"fields": [
{ "name": "email", "type": "input", "inputType": "email", "label": "Email", "required": true },
{ "name": "phone", "type": "input", "inputType": "tel", "label": "Phone" }
]
}
]
}The plugin supports these field types:
- input - Text, email, password, number, tel, url, etc.
- textarea - Multi-line text input
- select - Dropdown select
- checkbox - Single checkbox
- radio-group - Radio button group
- date-picker - Date selection
- file-upload - File upload
import type { FormSchema, FormField } from '@object-ui/plugin-form';
const loginForm: FormSchema = {
type: 'form',
fields: [...],
submitLabel: 'Sign In'
};
MIT