ModelSideForm
Split-pane view: a side panel on the left selects a record, and a ModelForm on the right displays/edits it.
Related Docs
- ModelForm — the form rendered on the right side
- ModelTable — table view (shared side panel components)
- ModelCard — card grid view
- Side Panel components — SideTree, SideCard, SideList
- Field — field widgets used in both side panel and form
- Action — toolbar and form actions
Quick Start
import { Field } from "@/components/fields";
import { FormBody } from "@/components/views/form/components/FormBody";
import { FormHeader } from "@/components/views/form/components/FormHeader";
import { FormSection } from "@/components/views/form/components/FormSection";
import { FormToolbar } from "@/components/views/form/components/FormToolbar";
import { SideTree } from "@/components/views/shared/side-panel/SideTree";
import { ModelSideForm } from "@/components/views/side-form/ModelSideForm";
export default function SettingPage() {
return (
<ModelSideForm modelName="Setting">
{/* Left: side panel for record selection */}
<SideTree
modelName="SettingGroup"
filterField="groupId"
labelField="name"
parentField="parentId"
/>
{/* Right: standard ModelForm content */}
<FormHeader />
<FormToolbar />
<FormBody>
<FormSection labelName="General">
<Field fieldName="key" />
<Field fieldName="value" />
</FormSection>
</FormBody>
</ModelSideForm>
);
}How It Works
- Children are split into one side panel element (
SideTree,SideCard, orSideList) and form content (everything else). - The side panel is wrapped in
SidePanelContainerProviderso its selection events flow back toModelSideForm. - When a record is selected, a
ModelFormis mounted with that record’sid. The form supports full read/edit lifecycle — the sameModelFormused in standalone[id]/page.tsxroutes. - Switching records re-mounts the form (via a key change), giving each record a fresh form state.
- If the form has unsaved changes, a confirmation dialog asks whether to discard before switching.
- When no record is selected, a placeholder message is shown.
Props
| Prop | Type | Required | Default | Notes |
|---|---|---|---|---|
modelName | string | Yes | - | The model to load into ModelForm. |
children | ReactNode | Yes | - | One side panel element + standard form components. |
Children Structure
Children are split into two groups:
| Group | Components | Renders at |
|---|---|---|
| Side panel | One of: SideTree, SideCard, SideList | Left panel (280px) |
| Form content | FormHeader, FormToolbar, FormBody, Field, etc. | Right panel (flex-1) |
The form content children are passed directly to ModelForm — use the same component composition as a standalone ModelForm page.
Layout
SideFormLayout renders a two-column layout:
┌──────────────┬──────────────────────────────────┐
│ Side Panel │ ModelForm │
│ (280px) │ (flex-1) │
│ │ │
│ SideTree / │ FormHeader │
│ SideCard / │ FormToolbar │
│ SideList │ FormBody │
│ │ FormSection │
│ │ Field ... │
│ │ │
└──────────────┴──────────────────────────────────┘- Side panel: fixed 280px,
border-rdivider - Form area:
flex-1, scrolls independently - Both panels fill the full available height
Side Panel Options
Any side panel component can be used. The component determines the record selection UI:
| Component | Best For | Selection Mode |
|---|---|---|
<SideTree> | Hierarchical data (departments, categories) | Tree node |
<SideCard> | Rich card display with header/body/footer | Card click |
<SideList> | Simple list with Field-based row templates | List item |
SideTree Example
<ModelSideForm modelName="SysField">
<SideTree
title="System Model"
modelName="SysModel"
filterField="modelId"
labelField="labelName"
parentField="parentId"
sortField="modelName"
selectionMode="single"
defaultExpandedLevel={2}
/>
<FormHeader />
<FormBody>
<FormSection labelName="Field Info">
<Field fieldName="fieldName" />
<Field fieldName="labelName" />
<Field fieldName="fieldType" />
</FormSection>
</FormBody>
</ModelSideForm>SideList Example
<ModelSideForm modelName="DesignWorkItem">
<SideList
modelName="DesignWorkItem"
filterField="id"
filters={[["status", "=", "IN_PROGRESS"], "OR", ["status", "=", "READY"]]}
searchable
>
<WorkItemListItem />
</SideList>
<FormHeader />
<FormToolbar />
<FormBody>
<FormSection labelName="General">
<Field fieldName="name" />
<Field fieldName="status" />
</FormSection>
</FormBody>
</ModelSideForm>SideList children define the row template. Each row is wrapped in RecordContextProvider, so custom components can use useRecordContext():
import { useRecordContext } from "@/components/contexts/RecordContext";
import { Badge } from "@/components/ui/badge";
function WorkItemListItem() {
const { record } = useRecordContext();
return (
<div className="flex w-full items-center justify-between gap-2">
<span className="truncate text-xs">{record.name as string}</span>
<Badge variant="outline" className="shrink-0 text-[10px]">
{record.status as string}
</Badge>
</div>
);
}SideCard Example
<ModelSideForm modelName="DesignWorkItem">
<SideCard
modelName="DesignApp"
filterField="appId"
sortField="appName"
searchable
>
<SideCard.Header>
<Field fieldName="appName" />
</SideCard.Header>
<Field fieldName="appCode" />
<SideCard.Footer>
<Field fieldName="updatedTime" />
</SideCard.Footer>
</SideCard>
<FormHeader />
<FormBody>
<FormSection labelName="General">
<Field fieldName="name" />
<Field fieldName="description" />
</FormSection>
</FormBody>
</ModelSideForm>Form Content
The right side renders a full ModelForm. Use the same form component composition as standalone form pages:
| Component | Purpose |
|---|---|
FormHeader | Title bar with model label and description |
FormToolbar | Business actions (Save, Delete, custom) |
FormBody | Form body with tabs and sections |
FormSection | Grid layout section with label |
Field | Individual form field |
Action | Custom toolbar or form actions |
With Toolbar Actions
import { Action, dependsOn } from "@/components/actions/Action";
import { CheckCircle, XCircle } from "lucide-react";
<ModelSideForm modelName="DesignWorkItem">
<SideList filterField="id" searchable>
<WorkItemListItem />
</SideList>
<FormHeader />
<FormToolbar>
<Action
type="default"
labelName="Approve"
icon={CheckCircle}
operation="approve"
placement="toolbar"
confirmMessage="Approve this item?"
successMessage="Approved."
disabled={dependsOn(["id"], ({ mode }) => mode === "create")}
hidden={["status", "!=", "PENDING"]}
/>
<Action
type="default"
labelName="Reject"
icon={XCircle}
operation="reject"
placement="more"
confirmMessage="Reject this item?"
successMessage="Rejected."
disabled={dependsOn(["id"], ({ mode }) => mode === "create")}
hidden={["status", "!=", "PENDING"]}
/>
</FormToolbar>
<FormBody>
<FormSection labelName="General">
<Field fieldName="name" />
<Field fieldName="status" />
<Field fieldName="description" />
</FormSection>
</FormBody>
</ModelSideForm>With Multiple FormSections
<ModelSideForm modelName="Employee">
<SideTree
modelName="Department"
filterField="departmentId"
labelField="name"
parentField="parentId"
/>
<FormHeader />
<FormToolbar />
<FormBody>
<FormSection labelName="Basic Info">
<Field fieldName="firstName" />
<Field fieldName="lastName" />
<Field fieldName="email" />
</FormSection>
<FormSection labelName="Employment">
<Field fieldName="departmentId" />
<Field fieldName="positionId" />
<Field fieldName="hireDate" />
</FormSection>
<FormSection labelName="Custom Content">
<MyCustomComponent />
</FormSection>
</FormBody>
</ModelSideForm>Dirty State & Record Switching
ModelSideForm automatically tracks whether the form has unsaved changes. When you click a different record in the side panel while the form is dirty:
- A confirmation dialog appears: “You have unsaved changes. Discard them and switch to the selected record?”
- Discard → switches to the new record, old changes are lost
- Keep editing → stays on the current record, side panel selection is not changed
This prevents accidental data loss. The form is fully re-mounted on each record switch (via React key), so each record gets a clean form state.
Comparison with Other Views
| Feature | ModelSideForm | ModelTable | ModelCard |
|---|---|---|---|
| Data display | Single record form | Multi-row table grid | Multi-card grid |
| Side panel | Required (selection) | Optional (filtering) | Optional (filtering) |
| Record editing | Full form edit | Optional inline edit | - |
| Click behavior | Panel selects record | Navigate or inline edit | Navigate |
| Dirty state guard | Yes | Inline edit only | - |
| Search/Filter/Sort | Side panel only | Full toolbar | Simplified toolbar |
| Pagination | Side panel (client) | Server-side | Server-side |