PhoenixKitEntities.FormBuilder (PhoenixKitEntities v0.2.8)

Copy Markdown View Source

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:

Summary

Functions

Builds a single form field based on field definition.

Builds form fields HTML for an entire entity.

Gets the current value of a field from a changeset.

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

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).

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

Validates entity data against field definitions.

Functions

build_field(field, changeset, opts \\ [])

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(entity, changeset, opts \\ [])

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

get_field_value(form, field_key)

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(fields_definition, 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(field, lang_code)

@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(field, option_value, lang_code)

@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(entity, data_params, lang_code \\ nil)

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"]}}