# `PhoenixKitEntities.Components.LiveDataForm`
[🔗](https://github.com/BeamLabEU/phoenix_kit_entities/blob/v0.4.6/lib/phoenix_kit_entities/components/live_data_form.ex#L1)

Embeddable `LiveComponent` for viewing/editing a single `EntityData`
record's custom fields — without the admin `DataForm` LiveView's parent
picker, status control, presence/locking, or multilang tabs.

The host LiveView owns everything outside the fields themselves:

- Loading `record` (an `%EntityData{}`) — **expected to arrive with its
  `:entity` association preloaded**. When it is, this component reuses
  it and never touches the database — `:readonly` render is always
  DB-free, and so is the initial `:edit` render as long as `lang` is
  `nil` (a non-nil `lang` makes `FormBuilder.build_fields/3` do one
  cacheable `Multilang`/settings read; see the `lang` attribute below).
  When `:entity` isn't preloaded, it's instead loaded via
  `PhoenixKitEntities.get_entity!/2` (one query per `update/2`, unless
  the entity was already resolved for the same `entity_uuid` on a
  previous update) — a correctness fallback, not the intended calling
  convention.
- PubSub — this component neither subscribes nor publishes.
  `PhoenixKitEntities.EntityData.update/3` already broadcasts changes.
- `record.status` — autosave never changes it; a draft record stays a
  draft, a published one stays published.
- Guarding who can persist — `handle_event/3` only runs the
  autosave/submit path when `mode == :edit`; every other mode silently
  ignores both events. This isn't just UI polish: a `LiveComponent`'s
  `handle_event/3` is reachable for any event it defines via the
  component's `cid`, independent of what the rendered template actually
  wires up, so a non-`:edit` instance would otherwise still accept a
  crafted "autosave"/"submit" push and persist attacker-controlled data
  into a record the UI is displaying as read-only.
- `render/1` mirrors the same fail-closed rule: only `mode == :edit`
  renders the live, autosaving form. Every other value — `:readonly`,
  anything else, or `mode` omitted entirely (a caller bug) — renders
  the static readonly view. The alternative (rendering the form
  whenever `mode` isn't literally `:readonly`) would let a forgotten
  `mode` produce a form that *looks* editable while `handle_event/3`'s
  own guard silently drops everything typed into it.
- The `mode == :edit` guard above is a *process-level* check — it says
  nothing about whether the record ITSELF has moved on since this
  particular component instance last read it. Two browser tabs can
  both hold a live `:edit` instance of the same record; if one tab
  submits (moving the record to, say, `"published"`) and the other's
  still-pending autosave lands afterwards, `mode == :edit` is
  satisfied in both — the second tab genuinely IS in edit mode, it's
  just editing a record that has since moved on. Closing that
  *status-transition* race — NOT general concurrent-edit/optimistic
  locking, see the `persist_statuses` attribute's own doc for the
  distinction — is what `persist_statuses` (below) is for: it's
  optional (`nil` by default, matching every caller before it
  existed) precisely because it needs the host LiveView to state
  which statuses this instance is allowed to write into.
- Required-field completeness — **incremental autosave is guaranteed
  only for entities without required fields; entities with required
  fields save all-or-nothing per attempt, by design**. Every save runs
  the merged data through
  `PhoenixKitEntities.FormBuilder.validate_data/2` on a best-effort
  basis (for type coercion — number strings, URL normalization — never
  as a hard gate: see `coerce_or_pass_through/3`). *However*,
  `EntityData.update/3` → `EntityData.changeset/2` runs its own,
  unconditional required-field check across the **entire** entity on
  every single save, independent of `FormBuilder` and deliberately left
  untouched here — relaxing it would also affect the admin `DataForm`
  and the public entity form, which both rely on it staying strict. In
  practice: for an entity with required fields, an autosave attempt
  while any required field is still empty logs an error and keeps the
  previous `record` unpersisted, exactly like any other save rejection;
  filling in the last required field then persists everything typed
  before it in one shot (thanks to merging over `record.data`). Entities
  with no required fields autosave incrementally with no such gap.
  Checking whether a record is "complete enough" to submit is the
  parent's responsibility either way.

## Usage

    <.live_component
      module={PhoenixKitEntities.Components.LiveDataForm}
      id={"survey-" <> record.uuid}
      record={record}
      mode={:edit}
      lang="et"
      actor={@current_user}
      submit_label={gettext("Kinnitan")}
    />

## Attributes

- `record` (required) — the `%PhoenixKitEntities.EntityData{}` being
  displayed or edited. Preload its `:entity` association before passing
  it in — that's what keeps this component DB-free to render; see
  "The host LiveView owns" above.
- `mode` (required) — `:edit` renders a live, autosaving form. Any
  other value renders static "label: value" output with no inputs;
  this includes `:readonly` (the intended non-editing value) but also
  a caller bug that omits `mode` altogether — the component fails
  closed to the safe, non-interactive view rather than rendering an
  editable-looking form that silently drops every edit.
- `lang` (optional, defaults to `nil` — safe to omit entirely) — locale
  passed straight through to `PhoenixKitEntities.FormBuilder.build_fields/3`
  as `lang_code` (and to entity loading, when the entity isn't already
  preloaded). In `:edit` mode this drives `FormBuilder`'s own field-level
  "translations" resolution (labels + option text; see `FormBuilder`'s
  moduledoc for the display-only contract). The static `:readonly` view
  resolves the same `field["translations"]` map directly via
  `FormBuilder.translated_label/2` / `translated_option_label/3` — a
  stored value (e.g. `"Must"`) always renders its translation for
  `lang` (e.g. "Чёрный"), falling back to the original label/option text
  whenever `lang` is `nil` or no translation entry exists. Either way,
  the value actually persisted in `record.data` is never touched. This
  component is one of the surfaces that DOES resolve field-level
  `"translations"` — see `FormBuilder`'s moduledoc, "Which surfaces
  resolve this", for the full list of which callers do and don't.
- `actor` (optional, defaults to `nil` — safe to omit entirely) — the
  acting user (a struct with a `:uuid` field) or `nil`. Threaded through
  to `EntityData.update/3` as `actor_uuid:` for activity logging.
- `submit_label` (optional, defaults to `nil` — safe to omit entirely) —
  `nil` hides the submit button entirely; any other string renders it
  as the button's text.
- `persist_statuses` (optional, defaults to `nil` — safe to omit
  entirely) — a list of status strings (e.g. `["draft"]`), threaded
  through to `EntityData.update/3` as `require_status:` on both the
  autosave and submit persist paths. **Recommended for any host
  LiveView that can have more than one live `:edit` instance of the
  same record open at once** (e.g. two browser tabs/sessions) — see
  the cross-session race note above. `nil` (the default) reproduces
  the exact pre-`persist_statuses` behavior: no status check, save
  proceeds regardless of the record's current status. When set, a
  save whose freshly-read record status has moved outside the list
  fails exactly like any other rejected save (see "Messages sent to
  the parent" below) — logged at `debug` (not `error`; the guard
  doing its job isn't a failure), previous `record` kept, no
  `:saved`/`:submitted` message sent. Be precise about what this
  guards: it's a **status-transition** check, not optimistic locking
  on `data`. Two sessions both legitimately editing the same record
  while its status stays within `persist_statuses` (e.g. both still
  "draft") are NOT protected from each other — whichever save lands
  last still wins, silently overwriting the other's edits. Closing
  *that* race (concurrent edits within the same allowed status) is
  out of scope here; this attribute only stops a save from landing
  once the record has moved to a status the caller didn't allow.

## Messages sent to the parent

- `{:live_data_form, :saved, updated_record}` — after a successful
  autosave (fired on every change, debounced 500ms).
- `{:live_data_form, :submitted, updated_record}` — after the submit
  button is pressed. Submit persists the params `phx-submit` just sent
  (the same merge-over-`record.data` path as autosave, so the last
  edit is never lost to the debounce window) and only sends this
  message once that save succeeds — `updated_record` reflects it. The
  component never changes `status` itself — the parent decides what
  "submitted" means for its workflow.

On a failed save (autosave or submit) the component logs the error and
keeps the previous `record` — it never crashes the LiveView, and on a
failed submit specifically it does not send `:submitted` (the parent
must not treat unpersisted data as agreed-upon).

Ordering caveat: a debounced autosave that was already in flight when
Submit was pressed can still land afterwards, so the parent may see
`{:live_data_form, :saved, _}` arrive *after*
`{:live_data_form, :submitted, _}` for the same record. Both messages
carry the freshly-persisted record, so this is harmless as long as the
parent treats each message as "here is the current server copy" rather
than assuming `:submitted` is always the last word — read the record's
own `status`/fields rather than inferring lifecycle order from message
arrival, the way the host app (andi) does. This is a *single*
component instance racing its own two persist paths — distinct from
the *cross-session* race (a second tab's stale `:edit` instance of the
same record) that `persist_statuses` guards against; the two are
independent and both worth being aware of.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
