# Binding an element to real data

*Open when an element must show or save record data: `dataSource.fieldPath`, form variables, filtered option lists, enum values.*

## The path is rooted at a variable, not at a field

```json
{ "type": "input", "name": "email", "dataSource": { "fieldPath": "Users.email" } }
```

Segment 1 is a **variable name**. On an entity-bound form the auto-created default variable's name **is the entity's system name verbatim** — so `Users.email`, never `email`, never `record.email`. Every further segment is a field on the current entity.

A form with no bound entity (always kind `custom`, not `workspace`) still has a default variable: a JSON container, conventionally `submitData`, whose **keys are the submit payload** — bind `submitData.email`, one identifier-only key per element. **Always write `dataSource.fieldPath` yourself.** The "key = element name" rule is a builder *interaction* — it fires when a human drops an element on the canvas, and never on a JSON write: *"Ключі завжди явні в схемі… контрактом є саме `fieldPath`"* ([datasource reference](https://docs.modern-expo.com/frontend-fb2/formBuilder/features/datasource-backend-reference.html)). A hand-authored element with no `fieldPath` carries no submit key at all, and the backend's own contract extraction (`components[].dataSource.fieldPath` with this root, where `isSubmitData !== false`) reads nothing for it. Keys are flat identifiers — `^[A-Za-z_][A-Za-z0-9_]*$`, no dots; nesting is not used. `isSubmitData: false` is the deliberate opt-out: the field stays on the form and its value is not sent. When the form's variables are in hand, use the `isDefault` variable's exact name.

Variables live in the draft's `variables` array; the keys are closed — `{ id, name, type, multiple, isDefault, entity?, select?, hydrate?, enumValues?, defaultValue? }`. A relation variable's target is `entity: "Candidates"`, the system name as a plain string; `relationEntity` / `relationTarget` / `entityName` are entity-**field** metadata and inert here. A variable's identity is its `name` (the root of every `fieldPath`, and the merge key when a child form inherits); `id` is a client-minted uuid that nothing resolves.

**`entity` does not survive a save on a non-default variable.** The `FormVariables` table has no
such column — ask the platform for it and it answers `Field "entity" not found in entity
"FormVariables"` — and the rows cross the wire through a blind cast, so the key is simply dropped.
Only the **default** variable keeps its target, because `DefaultVariableFactory.ensure()` re-derives
it from the form's own `draft.entity` on every rebuild; for any other relation variable the target
is gone on the next load and the field palette falls back to *"Bind an entity to use its fields"*.
Until the backend column exists, a design that needs a second entity should reach for a second
**bound element** (a `table` / `entityList` with its own `dataSource.entity`) rather than a second
relation variable.

## The relation hop: loads, accepts the edit, saves nothing

A relation segment hops to its target entity, so `Tasks.customer.name` resolves and **displays**. It is never written back on save. An editable `input` bound across a hop renders, accepts typing, and discards it — no error, no warning at runtime. **Editable controls bind a top-level field of the variable's entity only.** Crossing a to-many relation makes the path a list: display-only.

## What nothing checks

The offline gate has no entity metadata. It validates only the **root** segment against the declared variables (`unknown_binding`); a misspelled or label-cased leaf passes everything. At runtime the record's load projection is `{ id, updatedAt }` plus the fields the form actually needs, so an unresolvable path resolves to `undefined`: the control renders **empty on a form that otherwise loads fine**.

The projection is collected from **four author-controlled** sources, not just the bindings — which is also where the remedies are: `dataSource` bindings on components; every field a `visible`/`readonly`/`required`/`disabled` condition references (so a field a *condition* needs is fetched automatically — never add a hidden element for it); every `{{path}}` interpolation inside a localized text, including the form title's; and the explicit `variable.select` array. On top of those, each element type declares its own decorative paths (a select's `labelField`/`colorField`, an avatar's initials fields, …) — automatic, nothing to author. If **nothing** contributes beyond the `{ id, updatedAt }` floor, the record is not fetched at all. **`select` is how you fetch a field nothing on the canvas shows** — the form-level counterpart of a table's `settings.requestFields` — e.g. `select: ['assignee.email']` on the `task` variable so form code can read it.

## Grounding: `suppa describe-entity --entity-name <E>`

It returns the raw platform shape. Translate it:

| describe-entity | use it as |
|---|---|
| `name` | the path segment — this is what you bind |
| `title.{en,uk,…}` | the label the user and the screenshot say; never bindable |
| `relationEntity.name` | the entity the next segment belongs to |
| `representativeField.name` | the relation's display field → `dataSource.labelField` |
| `type` `one-to-many` / `many-to-many` / `many-to-many-backref` | to-many ⇒ display-only path |
| `type` `enum` / `multi-enum` | the qualified enum name is `<Entity>.<field>` |
| `subType` | `email` ⇒ `input` with `inputType: 'email'` |

Users name fields by label: match their wording against **both** `name` and every `title.<locale>`, then bind the matched `name` — a screenshot's "Followers" is the field `watchers`.

## Do not retype a bound field's label

On an element bound to an entity field the label defaults to **linked**: `_labelLinked: true` (or
absent) plus `_labelGlobalKey` = the field's own key (`entity.<Entity>.field.<field>`). The text is
baked into `schema.label` from the field's title in every tenant locale so it renders at once, it
is read-only in the builder, and it **auto-follows** the field's translation — change the entity
field's title and every linked label moves with it. On save it stores as a bare reference,
`{ "key": _labelGlobalKey }`, with no duplicated strings.

Write a literal `{ en, uk }` label only when you mean a **custom** one — the author's own wording,
decoupled from the field (`_labelLinked: false`, `_labelGlobalKey` cleared, strings under
`<componentId>.label`) — or where the field has no key to link to. Retyping a field's own title as
a literal is the anti-pattern the form guide names: *"Duplicating a linked label's strings"*. And
the general rule behind it: **underscore-prefixed keys are UI meta** (`_labelLinked`,
`_labelGlobalKey`, …) — preserve them when you edit an element, and never invent new ones.

## A localized field is a flat map, until it is a list

`LocalizedField = LocalizedText | LocalizationVariant[]`. The flat `{ "en": …, "uk": … }` map is
the shape to write and the shape the builder collapses back to on save. The array form exists for
**conditional text** — each entry is `{ localization, conditions, globalKey? }`, where `conditions`
is an ordinary `ConditionGroup` evaluated by the same engine as `visible`/`required`, and a variant
with `conditions: null` is the unconditional fallback. It is stored only when there are several
variants or a condition, **or** when the text is bound to a global translation: a global binding is
one unconditional variant carrying `globalKey`, and it is deliberately not collapsed, because
collapsing would lose the key. So when you edit an element whose `label` is already an array,
**echo the array** — flattening it silently drops a condition or a global binding.

Two reserved things: `key` is **not a locale** — it is the localization-registry reference, and an
unresolved `{ key }` renders as an empty string rather than showing the raw key (which is why a
missing registry looks like a blank label, not a broken one). And `globalTranslationId` and an
inline `localization` are mutually exclusive: where the first is set, the second is ignored.

## Enum options: the stored value, not the title

```json
{ "type": "select",
  "dataSource": { "kind": "enum", "enum": "Users.gender", "fieldPath": "Users.gender" } }
```

`dataSource.enum` is the qualified name as a string and the **field's** stored value is the enum
value; the title is display only. An enum column read back arrives as `{ value, title, id }`.

**In a CONDITION the shape is different, and getting it wrong is silent.** A backend enum — a
`custom_enum` entity field, or any element whose `dataSource.kind` is `'enum'` with a qualified
name — stores the whole option object in the condition's `value`: `{ id, value, title }` (an array
of them for `in` / `not in`). *"Бекенд матчить кастомні енумки по `id`… а строковий нейм не
матчиться ніколи — саме так фільтри графіків генерували робочу на вигляд, але порожню умову."* A
condition written with the bare code reads correctly in the editor, runs without error, and matches
nothing. Scalars are right in exactly two places: an element's **own static options** and an **enum
variable** — there the string *is* the stored value. Client-side (display) conditions compare the
intersection of every comparable scalar, so they tolerate both shapes; the backend does not. When
you have the option object, write the object.

## Narrowing an entity dropdown

On `kind: 'entity'`, `dataSource.entity` is the system name string, `valueField` defaults to `'id'`, `labelField` to the representative field, and relation sources default `valueIsObject: true` — the stored value is `{ id, name }`.

`dataSource.filter` is a ConditionGroup whose fields are rooted at the **target** entity. Make the right-hand side live with `valueSource: 'variable'` + `valueField: '<dot-path>'` — cities filtered by the picked country. **A dynamic leaf whose value resolves empty is pruned from the query, so the list widens to everything.** Right for a cascading dropdown, wrong for a scoping filter: there the leaf needs a `fallbackValue` to survive the empty value.

## isSubmitData

Defaults to **true** whenever `dataSource` is set. Set it `false` on a bound element that must show a value without writing it back.
