# `PhoenixKitEntities.FormBuilder`
[🔗](https://github.com/BeamLabEU/phoenix_kit_entities/blob/v0.4.6/lib/phoenix_kit_entities/form_builder.ex#L1)

Dynamic form builder for entity data forms.

This module generates Phoenix.Component forms based on entity field definitions,
enabling dynamic data entry forms that adapt to the entity's schema.

## Usage

    # Generate form fields for an entity
    fields_html = PhoenixKitEntities.FormBuilder.build_fields(entity, changeset)

    # Generate a single field
    field_html = PhoenixKitEntities.FormBuilder.build_field(field_definition, changeset)

    # Validate entity data against field definitions
    {:ok, validated_data} = PhoenixKitEntities.FormBuilder.validate_data(entity, data_params)

## Field Type Support

The FormBuilder supports all field types defined in `PhoenixKitEntities.FieldTypes`:

- **Basic Types**: text, textarea, email, url, rich_text
- **Numeric Types**: number
- **Boolean Types**: boolean (toggle/checkbox)
- **Date Types**: date
- **Choice Types**: select, radio, checkbox (with options)
- **Media Types**: image, file (upload)
- **Relational Types**: relation (entity references)

## Form Generation

Forms are generated as Phoenix.Component HTML with proper validation,
error handling, and styling consistent with the PhoenixKit design system.

## Field-Level Translations (display-only)

A field definition may carry an optional `"translations"` key, resolved
by `translated_label/2` and `translated_option_label/3`:

    %{
      "key" => "color",
      "label" => "Värv",
      "type" => "radio",
      "options" => ["Must", "Valge"],
      "translations" => %{
        "ru" => %{"label" => "Цвет", "options" => %{"Must" => "Чёрный", "Valge" => "Белый"}},
        "en" => %{"label" => "Colour", "options" => %{"Must" => "Black", "Valge" => "White"}}
      }
    }

This is **display-only**: the canonical, stored/submitted value is
always the original `"label"` / option string (`"Must"`, never
`"Чёрный"`) — `"translations"` never affects validation,
`merge_other_params/2`, or what lands in `EntityData.data`. `"heading"`
fields are translated the same way (their `"translations"` map only
ever has a `"label"` entry, since headings have no options).

Resolution falls back to the original text whenever a lookup can't be
satisfied: `lang_code` is `nil`, the field has no `"translations"` key
at all, the language isn't present, or (for options) that specific
option has no translated entry. Language-code lookups tolerate
base/dialect mismatches the same way entity-level `settings["translations"]`
does (see `PhoenixKitEntities.get_entity_by_name/2`'s dialect-tolerant
lookup) — a field translated under `"ru-RU"` still resolves for a
caller passing the bare `"ru"`, and vice versa.

### Which surfaces resolve this

Resolved wherever `build_fields/3` / `build_field/3` receive a non-nil
`opts[:lang_code]` (every rendering clause: labels; `select`/`radio`/
`checkbox` also translate their option text), and by
`PhoenixKitEntities.Components.LiveDataForm` in both modes — `:edit`
via `build_fields/3` (it passes its `lang` attr straight through as
`lang_code`), `:readonly` directly via `translated_label/2` /
`translated_option_label/3`.

**Not** resolved — these surfaces render the canonical field text
regardless of locale, by design; this is a scope boundary, not a bug:

- `PhoenixKitEntities.Web.DataForm` (the admin record editor) — the
  non-multilang branch calls `build_fields/3` with `lang_code: nil`
  hardcoded; the multilang branch only passes a real locale when the
  multilang module itself is enabled, otherwise also `nil`.
- `PhoenixKitEntities.Components.EntityForm` (the public entity
  submission form) — doesn't pass `lang_code` to `build_fields/3` at
  all.
- Validation error messages (`EntityData.changeset/2` and
  `validate_data/3` here) — always interpolate the canonical
  `field_def["label"]` / `field["label"]`, never a translation.

# `build_field`

Builds a single form field based on field definition.

## Parameters

- `field` - Field definition map
- `changeset` - The changeset for validation and values
- `opts` - Optional configuration

## Examples

    iex> field = %{"type" => "text", "key" => "title", "label" => "Title"}
    iex> changeset = Ecto.Changeset.cast(%{}, %{}, [])
    iex> PhoenixKitEntities.FormBuilder.build_field(field, changeset)
    # Returns Phoenix.Component field HTML

# `build_fields`

Builds form fields HTML for an entire entity.

Takes an entity with its field definitions and generates the complete
form HTML for data entry.

## Parameters

- `entity` - The entity struct with fields_definition
- `changeset` - The changeset for the entity data
- `opts` - Optional configuration (default: [])

## Options

- `:wrapper_class` - CSS class for field wrapper divs
- `:input_class` - CSS class for input elements
- `:label_class` - CSS class for label elements
- `:id_prefix` - extra segment folded into each field wrapper's DOM id
  (`"entity-field-#{id_prefix}-#{key}-#{lang}"`). Every page that has
  historically called this function renders exactly one form per entity
  (admin `DataForm`, the public entity form), so the default (`nil`,
  omitted from the id) is unchanged. Pages embedding more than one
  instance of the *same* entity's fields at once — e.g.
  `PhoenixKitEntities.Components.LiveDataForm` used once per record in a
  list — MUST pass something unique per instance (the record's uuid) or
  LiveView raises "Duplicate id found while testing LiveView" (in tests)
  / silently misdirects DOM patches between instances (at runtime).

## Examples

    iex> entity = %Entities{fields_definition: [
    ...>   %{"type" => "text", "key" => "title", "label" => "Title", "required" => true}
    ...> ]}
    iex> changeset = Ecto.Changeset.cast(%{}, %{}, [])
    iex> PhoenixKitEntities.FormBuilder.build_fields(entity, changeset)
    # Returns Phoenix.Component form HTML

# `cast_field`

```elixir
@spec cast_field(map(), term()) :: {:ok, term()} | {:error, [String.t()]}
```

Casts ONE raw (form-submitted) value against its field definition —
the per-field core of `validate_data/2`, public for hosts that own
their own save events (inline row editors embedding
`PhoenixKitEntities.Components.FieldInput`).

Returns `{:ok, coerced}` (numbers parsed, booleans normalized, choice
membership enforced, media references uuid-checked…) or
`{:error, messages}`.

Normalizations for bare-control submissions: `""` casts to `nil`
(blur-clearing an input clears the stored key — required fields still
error), and a `checkbox` group's hidden-input `""`/`nil` casts to
`[]`.

# `get_field_value`

Gets the current value of a field from a changeset.

Helper function to extract field values from changesets or forms for form rendering.

# `merge_other_params`

Resolves `allow_other` sentinel values in submitted params back into free text.

Radio/select/checkbox fields with `"allow_other" => true` render an extra
"Other" option whose value is the sentinel `"__other__"`, paired with a
companion `<key>__other` text input. This function replaces the sentinel
with the companion field's text (defaulting to `""` if absent) and drops
every `<key>__other` companion key from the result. A no-op for fields
without `allow_other`, or for values that aren't the sentinel.

## Examples

    iex> field = %{"type" => "radio", "key" => "color", "allow_other" => true}
    iex> PhoenixKitEntities.FormBuilder.merge_other_params(
    ...>   [field],
    ...>   %{"color" => "__other__", "color__other" => "Crimson"}
    ...> )
    %{"color" => "Crimson"}

# `translated_label`

```elixir
@spec translated_label(map(), String.t() | nil) :: String.t() | nil
```

Resolves a field's (or a `"heading"` field's) display label for
`lang_code`, honoring an optional `"translations"` key on the field
definition (see the moduledoc for the full contract).

Falls back to `field["label"]` whenever `lang_code` is `nil`, the
field has no `"translations"` map, the language isn't present in it,
or its `"label"` entry is missing/blank. Display-only — never affects
the canonical `field["label"]` itself.

## Examples

    iex> field = %{"label" => "Värv", "translations" => %{"ru" => %{"label" => "Цвет"}}}
    iex> PhoenixKitEntities.FormBuilder.translated_label(field, "ru")
    "Цвет"

    iex> PhoenixKitEntities.FormBuilder.translated_label(field, "et")
    "Värv"

    iex> PhoenixKitEntities.FormBuilder.translated_label(field, nil)
    "Värv"

# `translated_option_label`

```elixir
@spec translated_option_label(map(), String.t(), String.t() | nil) :: String.t()
```

Resolves the display label for a single radio/select/checkbox option
value, honoring the field's optional `"translations" => %{lang =>
%{"options" => %{option_value => translated}}}` map.

`option_value` is always the canonical, stored/submitted string —
this only affects what text renders next to it (e.g. `<option
value={option_value}>{translated_option_label(...)}</option>`). Falls
back to `option_value` itself whenever `lang_code` is `nil`, the field
has no `"translations"` map, the language isn't present, or this
specific option has no translated entry (this is also what makes a
free-text `allow_other` value display as-is: it was never one of the
fixed options, so it never has a translation entry to find).

## Examples

    iex> field = %{"translations" => %{"ru" => %{"options" => %{"Must" => "Чёрный"}}}}
    iex> PhoenixKitEntities.FormBuilder.translated_option_label(field, "Must", "ru")
    "Чёрный"

    iex> PhoenixKitEntities.FormBuilder.translated_option_label(field, "Valge", "ru")
    "Valge"

# `validate_data`

Validates entity data against field definitions.

Takes entity field definitions and validates submitted data parameters
according to the field types, requirements, and constraints.

## Parameters

- `entity` - The entity with field definitions
- `data_params` - Map of submitted data parameters

## Returns

- `{:ok, validated_data}` - Successfully validated data
- `{:error, errors}` - Validation errors

## Examples

    iex> entity = %Entities{fields_definition: [
    ...>   %{"type" => "text", "key" => "title", "required" => true}
    ...> ]}
    iex> PhoenixKitEntities.FormBuilder.validate_data(entity, %{"title" => "Test"})
    {:ok, %{"title" => "Test"}}

    iex> PhoenixKitEntities.FormBuilder.validate_data(entity, %{})
    {:error, %{"title" => ["is required"]}}

---

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