Fields
Metadata-driven field system used by ModelForm, relation dialogs, and inline editors.
Field is the main business-facing entry for app code.
Use this README for:
Fieldprops and overridesrequired/readonly/hiddendependsOn(...)- relation
filters - remote
Field.onChange - runtime value contracts
- field-type-level front-end behavior
Related docs:
- Relation Fields:
RelationTable,SelectTree,OneToMany,ManyToMany - Widgets:
FieldType -> WidgetTypematrix and widget-specific examples - ModelForm: page shell
- ModelTable: read cells and inline edit
- Tree: internal tree primitives used by
sideTreeandSelectTree
Import
Recommended business-facing import:
import { Field } from "@/components/fields";Additional public exports:
import {
Field,
RelationTable,
type FieldCondition,
type FieldConditionContext,
type FieldOnChangeProp,
type RelationFormView,
type RelationTableProps,
} from "@/components/fields";Internal note:
ResolvedFieldsis internal and should stay behind infrastructure code rather than becoming a business-facing field API
Recommended Usage
Use Field as the single entry in app code:
<Field fieldName="name" />
<Field fieldName="description" widgetType="Text" />
<Field fieldName="avatar" widgetType="Image" />
<Field fieldName="notes" widgetType="Markdown" />The runtime resolves:
fieldTypefrom metadata- the default field adapter from
fieldType - the optional widget renderer from
widgetType
Use direct adapter components and low-level widgets only inside field infrastructure.
Quick relation examples:
<Field fieldName="departmentId" widgetType="SelectTree" />const userTableView = (
<RelationTable orders={["username", "ASC"]} pageSize={10}>
<Field fieldName="username" />
<Field fieldName="email" />
</RelationTable>
);
<Field fieldName="userIds" tableView={userTableView} />;Core Props
Field is metadata-driven and supports field-level overrides and runtime conditions.
| Prop | Type | Required | Notes |
|---|---|---|---|
fieldName | string | Yes | Metadata field key in current model. |
fieldType | FieldType | No | Optional field-type override. If omitted, runtime uses metadata fieldType. |
widgetType | WidgetType | No | Optional widget override. Must be compatible with resolved fieldType. |
widgetProps | Record<string, unknown> | No | Widget-specific config only. Form widgets and inline editors use it; table read cells do not. |
placeholder | string | No | Field-level input placeholder. Prefer this over widgetProps.placeholder. |
hideLabel | boolean | No | Hides the whole label block. |
fullWidth | boolean | No | Layout hint for text-like and relation fields. |
labelName | string | No | Metadata label override. |
required | FieldCondition | No | Dynamic required control. Supports boolean, FilterCondition, or dependsOn(...). |
readonly | FieldCondition | No | Dynamic readonly control. Supports boolean, FilterCondition, or dependsOn(...). |
hidden | FieldCondition | No | Dynamic visibility control. Hidden fields are not rendered and their validation is suppressed. |
defaultValue | unknown | No | Create-only default override. Has higher priority than metaField.defaultValue and dialog/page defaultValues. |
filters | string | FilterCondition | No | Relation filter override. Field.filters overrides metaField.filters. Supports JSON-string metadata filters and {{ expr }} (e.g. {{ fieldName }}) references. |
onChange | FieldOnChangeProp | No | Remote field linkage. Supports shorthand string[] or { update?, with? }. |
tableView | ReactElement<RelationTableProps> | No | Relation-table config for OneToMany / ManyToMany. Must be a <RelationTable /> element. See Relation Fields. |
formView | RelationFormView | No | Relation dialog/detail view config. See Relation Fields. |
isPaged | boolean | No | Enables paged relation-table mode for OneToMany / ManyToMany. See Relation Fields. |
FieldCondition:
type FieldCondition =
| boolean
| FilterCondition
| ((ctx: FieldConditionContext) => boolean);FieldConditionContext:
interface FieldConditionContext {
fieldName: string;
metaField: MetaField;
values: Record<string, unknown>;
value: unknown;
scope: "form" | "model-table" | "relation-table";
rowIndex?: number;
rowId?: string;
isEditing: boolean;
recordId?: string;
}Behavior notes:
boolean: simplest and most direct.FilterCondition: recommended declarative form for common business rules.dependsOn([...], evaluator): explicit function-based condition with precise field subscriptions.- invalid
FilterConditionconfigs emit a dev warning and resolve tofalse. - bare function conditions are not supported; wrap them with
dependsOn([...], evaluator). - the same condition model is also used by
Action.disabledandAction.hiddenin form and table toolbars. hiddensuppresses both rendering and validation.- in
ModelTable/RelationTableinline edit, conditionvaluesis the current row object, not the whole form object. - in table declarations,
hiddenonly supportsbooleanand hides the whole column. widgetPropsis not propagated intoModelTable/RelationTableread-mode cell renderers.defaultValueis intended for static field-level create defaults. Use dialog/pagedefaultValuesonly for runtime/contextual prefills such as route params, parent row values, or non-rendered fields.required={false}can relax metadatarequired;readonly={false}can override metadata readonly.
Examples:
import { dependsOn, Field } from "@/components/fields";
<Field fieldName="status" readonly={true} />
<Field fieldName="itemColor" hidden={["active", "=", false]} />
<Field
fieldName="description"
readonly={[
["status", "IN", ["approved", "archived"]],
"OR",
[["type", "=", "SYSTEM"], "AND", ["editable", "!=", true]],
]}
/>
<Field
fieldName="itemName"
required={dependsOn(["active", "itemCode"], ({ values, isEditing }) =>
!isEditing && values.active === true && values.itemCode !== "Temp"
)}
/>dependsOn(...)
Use dependsOn([...], evaluator) when a field rule depends on other values and you want explicit subscriptions.
import { dependsOn, Field } from "@/components/fields";
<Field
fieldName="itemName"
required={dependsOn(["active", "itemCode"], ({ values, isEditing }) =>
!isEditing && values.active === true && values.itemCode !== "Temp"
)}
/>Why prefer dependsOn(...) over a bare function:
- the dependency list is explicit
- runtime subscriptions stay precise
- the evaluator is still fully programmable
Use boolean first, FilterCondition for declarative business rules, and dependsOn(...) when you truly need computed logic.
Relation filters
filters is mainly used by relation fields:
ManyToOne/OneToOnesearchable reference queriesSelectTreerelation picker queriesOneToMany/ManyToManyremote relation-table queriesManyToManypicker dialog queries
Accepted input:
FilterConditionin app code- JSON string form from metadata / backend payloads
Recommended declarative value syntax inside filter values:
{{ fieldName }}: resolve from current frontend scope before request (unified template syntax{{ expr }})TODAY,NOW,USER_ID,USER_EMP_ID,USER_POSITION_ID,USER_DEPT_ID,USER_COMP_ID: pass through unchanged and let backend replace environment variables- literals: use
{{ 'value' }}or backend tokens like{{ NOW }}; reserved field references:{{ @fieldName }}where supported
Example:
<Field
fieldName="departmentId"
filters={[
["companyId", "=", "{{ companyId }}"],
"AND",
["active", "=", true],
"AND",
["effectiveDate", "<=", "TODAY"],
"AND",
["type", "=", "{{ TODAY }}"],
]}
/>Behavior:
Field.filtersoverridesmetaField.filters- if
Field.filtersis omitted, relation widgets fall back tometaField.filters {{ fieldName }}is resolved against current scope values:ModelForm: current form valuesModelTableinline edit: current editing rowRelationTableinline edit: current relation row
- resolved field values are normalized before request:
ManyToOne/OneToOne->idOption->itemCodeMultiOption->itemCode[]
- if any
{{ expr }}dependency is missing, the relation query is treated as not ready and is not sent - frontend does not evaluate backend environment tokens such as
TODAY; they are passed through unchanged
Remote Field.onChange
Field supports remote linkage through a top-level onChange prop:
type FieldOnChangeProp =
| string[]
| {
update?: string[];
with?: string[] | "all";
};Common examples:
<Field fieldName="itemCode" onChange={["itemName", "itemColor"]} />
<Field
fieldName="itemCode"
onChange={{ update: ["itemName"], with: ["active"] }}
/>
<Field
fieldName="itemCode"
onChange={{ with: "all" }}
/>Behavior:
onChange={["a", "b"]}is shorthand foronChange={{ update: ["a", "b"] }}.updatepresent: only those fields are extracted from responsevalues.updateomitted: all responsevalueskeys within current scope are applied.withomitted: request only sendsidin edit mode plus current fieldvalue.with: ["a", "b"]: request addsvalueswith those fields in submit/API shape.with: "all": request adds current scope values in submit/API shape.
Current supported scopes:
ModelFormModelTableinline edit current rowRelationTableinline edit current row
Current non-goals:
- standalone top-level
OneToMany/ManyToManycontainer interactions are not source triggers - standalone dialog forms do not automatically provide this runtime yet
Auto trigger rules:
blur: text-like and editor-like fields such asString,MultiString, numeric inputs,JSON,Filters,Orders,Code,Markdown,RichText,TemplateEditorchange: commit-style fields such asBoolean,Date,DateTime,Time,Option,MultiOption,ManyToOne,OneToOne,File,MultiFile
Backend contract used by frontend:
POST /<modelName>/onChange/<fieldName>Request payload:
{
"id": "123",
"value": "ITEM-001",
"values": {
"active": true
}
}Response payload:
{
"values": {
"itemName": "Open",
"itemColor": "#22c55e"
},
"readonly": {
"itemName": true
},
"required": {
"itemColor": true
}
}Response rules:
valuesonly patches returned keys; missing keys are left unchanged.- returned
nullmeans explicit clear. readonly/requiredare applied independently ofupdate.- remote
readonly/requiredoverride metadata and local conditions until a later response or scope reset.
Scope notes:
- in
ModelForm,with: "all"uses current form submit shape; registered top-level relation fields use relation patch payloads instead of raw UI rows. - in
ModelTable/RelationTableinline edit,valuesandwith: "all"are the current row only, not the whole table or parent form.
Cascaded Fields
MetaField.cascadedField enables implicit auto-fill in edit scopes without requiring the source field to declare Field.onChange.
Example:
deptId.cascadedField = "employeeId.departmentId";
companyId.cascadedField = "employeeId.department.companyId";Behavior:
- supported in
ModelForm,ModelTableinline edit, andRelationTableinline edit - source field must be
ManyToOneorOneToOneand definerelatedModel - when the source field changes, frontend requests
/<relatedModel>/getByIdonce and reads all dependent cascade paths from that response - multiple targets depending on the same source are resolved in one lookup
- if the source field also declares
Field.onChange, both effects run in parallel - if both effects write the same target field,
cascadedFieldwins - clearing the source field clears all dependent cascaded targets without calling
getById - invalid cascade metadata is ignored with a dev warning
Syntax notes:
- format is
<sourceField>.<path> <sourceField>must be a field in the same current scope<path>is read from the source modelgetByIdresponse and may be nested
Field-Type Overview
This section explains the default front-end behavior by fieldType. For widget-specific variants and props tables, see Widgets.
String And Text
String: default single-line text inputMultiString: tag-style input; values are committed byEnter,,, or blur and stored as a comma-separated string in the form state- common
Stringwidget variants:URLEmailColorTextRichTextTemplateEditorMarkdownCode
Examples:
<Field fieldName="name" />
<Field fieldName="homepage" widgetType="URL" />
<Field fieldName="description" widgetType="Text" />
<Field fieldName="notes" widgetType="Markdown" />
<Field fieldName="content" widgetType="RichText" />Numeric Types
Integer,Long,Double: number-like inputsBigDecimal: decimal string semantics are preserved- common numeric widget variants:
MonetaryPercentageSlider
<Field fieldName="amount" widgetType="Monetary" />
<Field fieldName="ratio" widgetType="Percentage" />
<Field fieldName="score" widgetType="Slider" />Boolean And Option Types
Boolean: default isSwitchOption: default is single-select dropdownMultiOption: default is checkbox-group style multi-select
Common widget variants:
CheckBoxRadioStatusBar
<Field fieldName="active" />
<Field fieldName="active" widgetType="CheckBox" />
<Field fieldName="status" widgetType="Radio" />Date And Time Types
Date: standard date pickerDateTime: datetime inputTime: time input
Special format-oriented widgets:
Date:yyyy-MM,MM-ddTime:HH:mm,HH:mm:ss
<Field fieldName="birthday" />
<Field fieldName="period" widgetType="yyyy-MM" />
<Field fieldName="startTime" widgetType="HH:mm" />Reference Types
ManyToOne/OneToOne: default searchable relation selector- use
filtersfor dependent query constraints - use
widgetType="SelectTree"for hierarchical selection
<Field fieldName="departmentId" />
<Field fieldName="departmentId" widgetType="SelectTree" />See Relation Fields for SelectTree, RelationTable, OneToMany, and ManyToMany.
File Types
File: generic upload by default,Imagefor image-oriented modeMultiFile: generic multi-upload by default,MultiImagefor gallery-oriented mode
<Field fieldName="attachment" />
<Field fieldName="avatar" widgetType="Image" />
<Field fieldName="photos" widgetType="MultiImage" />Table/read behavior:
ModelTableandRelationTablereadFileInfo.urldirectly for preview and download rendering- table read cells intentionally ignore form-oriented
widgetProps
Structured Value Types
JSON/DTO: structured object or array values, default editor is JSON-orientedFilters: filter-builder valueOrders: order-builder value
<Field fieldName="config" />
<Field fieldName="filters" />
<Field fieldName="orders" />Runtime Value Contracts
Field.defaultValue, container defaultValues, form.getValues(), and useWatch() all work with field UI values, not raw API payload values.
| Field type | UI / form value | Submit / API shape |
|---|---|---|
File | FileInfo | null | fileId | null |
MultiFile | FileInfo[] | fileId[] | null |
JSON / DTO | structured object / array or null | structured object / array or null |
Filters | FilterCondition | null | FilterCondition | null |
Orders | structured order tuples or null | structured order tuples or null |
OneToMany | relation rows / row drafts | incremental patch map |
ManyToMany | ModelReference[] or relation rows | incremental patch map |
Notes:
- backend payloads and metadata defaults may still arrive as strings; the field runtime normalizes them into UI shapes on load
- when you pass page/dialog
defaultValues, use the UI shapes above directly instead of pre-stringifying values ManyToManywithwidgetType="TagList"still submits the normal incremental patch map- top-level relation field details are documented in Relation Fields
FileInfo
File and MultiFile fields use FileInfo objects in UI state.
Important runtime behavior:
- preview and download rendering use
FileInfo.url Fileread cells fall back to filename linksMultiFileread cells show the first filename plus+N
Layout Notes
fullWidth is meaningful for:
TextRichTextTemplateEditorMarkdownCodeOneToManyManyToMany
Default is true for those layouts.
ReadOnly Notes
Prefer readonly over disabled controls when the user still needs to read or copy values clearly.
General guidance:
readonly: detail page, audit page, inspect-only statedisabled: blocked by workflow, permissions, prerequisites, or submitting state
Examples
<Field fieldName="email" widgetType="Email" />
<Field fieldName="bio" widgetType="Text" fullWidth />
<Field
fieldName="notes"
widgetType="Markdown"
widgetProps={{ mode: "preview", minHeight: 360 }}
/>
<Field
fieldName="script"
widgetType="Code"
widgetProps={{ language: "sql", lineWrapping: false, minHeight: 320 }}
/>
<Field
fieldName="avatar"
widgetType="Image"
widgetProps={{
aspectRatio: "1 / 1",
display: "avatar",
crop: { enabled: true, shape: "round" },
}}
/>