| name | form-building |
| description | Building create/edit forms with FormField components, validation integration, and save patterns |
Form Building Skill
Core Principles
- MobX state management - NO state hooks or other libraries unless specified
- Group related fields using
DetailFieldContainer - Ask first if unclear about field importance
- Forms are for CREATE and EDIT operations
Available Components
Located in: ui/src/common/components/form/fields/
Basic Fields
FormFieldText- Short text fieldsFormFieldTextArea- Long text fieldsFormFieldCheckbox- Boolean 0/1 fieldsFormFieldReadOnly- Display-only fieldsFormFieldDate- Date pickerFormFieldColor- Color pickerFormFieldCodeEdit- Code editor
Single Select (Constants)
import { constants } from "@/models/constants";
<FormFieldSelect
record={props.asset}
field="status"
label="Status"
options={constants.asset.status}
/>
Multi Select (Constants)
<FormFieldMultiSelect
record={props.account}
field="permissions"
label="Permissions"
options={constants.account.permissions}
/>
Model Relationships
Small lists (< 50 options):
<FormFieldModelSelect<AssetModel, OrganizationModel>
record={props.asset}
field="organization_id"
label="Organization"
placeholder="Select Organization"
modelName="organization"
modelSearchField="q"
modelDisplayField="label"
modelSearchFilters={{ disabled: "0" }}
/>
Large lists (use search):
<FormFieldModelSearchSelect<AssetModel, OrganizationModel>
record={props.asset}
field="organization_id"
label="Organization"
placeholder="Search Organization"
modelName="organization"
modelSearchField="q"
modelDisplayField="label"
modelSearchFilters={{ disabled: "0" }}
/>
Multi-select (small lists):
<FormFieldModelMultiSelect<AssetModel, TagModel>
record={props.asset}
field="tag_ids"
label="Tags"
placeholder="Select Tags"
modelName="tag"
modelSearchField="q"
modelDisplayField="label"
/>
Multi-select (large lists with search):
<FormFieldModelSearchMultiSelect<AssetModel, TagModel>
record={props.asset}
field="tag_ids"
label="Tags"
placeholder="Search Tags"
modelName="tag"
modelSearchField="q"
modelDisplayField="label"
/>
Model Relationship Best Practices
- Always try to use
labelas modelDisplayField - Always try to use
qas modelSearchField - If model doesn't have a good label, modify it in
XXModel.ts - Assume all models exist - don't create new ones
JSONB Fields Pattern
For nested JSONB objects:
// JSONB class must extend ValidationClass
<FormFieldTextArea
record={props.organization.properties}
field="pricing_text"
label="Pricing Text"
placeholder="Enter pricing text..."
helpText="Default text if no pricing info in plan"
/>
Note: Unlike DetailFields, FormFields don't need parentRecord or SafeBaseModel wrapper
Grouping Fields with Container
<DetailFieldContainer label="Basic Information">
<FormFieldText
record={props.asset}
field="name"
label="Name"
placeholder="Asset Name"
/>
<FormFieldTextArea
record={props.asset}
field="description"
label="Description"
placeholder="Description..."
/>
</DetailFieldContainer>
<DetailFieldContainer label="Organization Details">
<FormFieldModelSearchSelect<AssetModel, OrganizationModel>
record={props.asset}
field="organization_id"
label="Organization"
modelName="organization"
modelSearchField="q"
modelDisplayField="label"
/>
<FormFieldSelect
record={props.asset}
field="status"
label="Status"
options={constants.asset.status}
/>
</DetailFieldContainer>
Complete Form Example
import { observer } from "mobx-react-lite";
import { AssetModel } from "@/models/asset";
import { OrganizationModel } from "@/models/organization";
import { constants } from "@/models/constants";
import {
DetailFieldContainer,
FormFieldText,
FormFieldTextArea,
FormFieldSelect,
FormFieldModelSearchSelect,
FormFieldCheckbox,
FormFieldDate,
} from "@/ui/common/components/form/fields";
import { Button } from "@/ui/shadcn/ui/button";
interface AssetFormProps {
asset: AssetModel;
onSave: () => void;
onCancel: () => void;
}
export const AssetForm = observer((props: AssetFormProps) => {
const handleSave = async () => {
await props.asset.save();
props.onSave();
};
return (
<div className="space-y-6">
<DetailFieldContainer label="Basic Information">
<FormFieldText
record={props.asset}
field="name"
label="Name"
placeholder="Asset Name"
/>
<FormFieldTextArea
record={props.asset}
field="description"
label="Description"
placeholder="Asset description..."
/>
<FormFieldCheckbox
record={props.asset}
field="is_active"
label="Is Active"
/>
</DetailFieldContainer>
<DetailFieldContainer label="Organization & Status">
<FormFieldModelSearchSelect<AssetModel, OrganizationModel>
record={props.asset}
field="organization_id"
label="Organization"
modelName="organization"
modelSearchField="q"
modelDisplayField="label"
modelSearchFilters={{ disabled: "0" }}
/>
<FormFieldSelect
record={props.asset}
field="status"
label="Status"
options={constants.asset.status}
/>
<FormFieldDate
record={props.asset}
field="effective_date"
label="Effective Date"
/>
</DetailFieldContainer>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={props.onCancel}>
Cancel
</Button>
<Button onClick={handleSave}>
Save
</Button>
</div>
</div>
);
});
Validation Integration
Forms automatically connect to model validation rules:
// Model already has validation rules defined
get validationRules() {
return assetValidationRules;
}
// Form fields automatically show errors
<FormFieldText
record={props.asset}
field="name"
label="Name"
// Validation errors show automatically
/>
Save Pattern
const handleSave = async () => {
try {
await props.asset.save();
// Success - redirect or close modal
props.onSave();
} catch (error) {
// Error handling - validation errors show on fields automatically
console.error("Save failed:", error);
}
};
Key Differences from Details View
| Feature | Form Fields | Detail Fields |
|---|---|---|
| Use case | Create/Edit | View/Edit |
| JSONB | Direct record | Needs parentRecord |
| Type casting | Not needed | SafeBaseModel<T> |
| Save button | Required | Optional |
| Validation | Built-in | Built-in |
Key Rules
- Always create components with
#ui_code_tools - Use search variants for large option lists (>50 items)
- Constants are model-specific:
constants.{model}.{field} - Forms handle validation automatically via model rules
- Call
model.save()to persist changes - Ask before implementing if field importance is unclear
- JSONB fields don't need
parentRecordin forms