Packages

phoenix_kit

1.7.31
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules entities form_builder.ex
Raw

lib/modules/entities/form_builder.ex

defmodule PhoenixKit.Modules.Entities.FormBuilder do
@moduledoc """
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 = PhoenixKit.Modules.Entities.FormBuilder.build_fields(entity, changeset)
# Generate a single field
field_html = PhoenixKit.Modules.Entities.FormBuilder.build_field(field_definition, changeset)
# Validate entity data against field definitions
{:ok, validated_data} = PhoenixKit.Modules.Entities.FormBuilder.validate_data(entity, data_params)
## Field Type Support
The FormBuilder supports all field types defined in `PhoenixKit.Modules.Entities.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.
"""
import Phoenix.Component
import PhoenixKitWeb.Components.Core.Icon, only: [icon: 1]
import PhoenixKitWeb.Components.Core.FormFieldLabel, only: [label: 1]
use Gettext, backend: PhoenixKitWeb.Gettext
@doc """
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
## Examples
iex> entity = %Entities{fields_definition: [
...> %{"type" => "text", "key" => "title", "label" => "Title", "required" => true}
...> ]}
iex> changeset = Ecto.Changeset.cast(%{}, %{}, [])
iex> PhoenixKit.Modules.Entities.FormBuilder.build_fields(entity, changeset)
# Returns Phoenix.Component form HTML
"""
def build_fields(entity, changeset, opts \\ []) do
fields_definition = entity.fields_definition || []
assigns = %{
fields_definition: fields_definition,
changeset: changeset,
opts: opts
}
~H"""
<div class="space-y-6">
<%= for field <- @fields_definition do %>
<div class={["form-field-wrapper", @opts[:wrapper_class]]}>
{build_field(field, @changeset, @opts)}
</div>
<% end %>
</div>
"""
end
@doc """
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> PhoenixKit.Modules.Entities.FormBuilder.build_field(field, changeset)
# Returns Phoenix.Component field HTML
"""
def build_field(field, changeset, opts \\ [])
# Text Input
def build_field(%{"type" => "text"} = field, changeset, opts) do
assigns = %{field: field, changeset: changeset, opts: opts}
~H"""
<div>
<.label>{@field["label"]}{if @field["required"], do: " *"}</.label>
<input
type="text"
name={"#{@changeset.data.__struct__.__schema__(:source)}[data][#{@field["key"]}]"}
value={get_field_value(@changeset, @field["key"])}
placeholder={@field["placeholder"] || ""}
class={["input input-bordered w-full", @opts[:input_class]]}
maxlength={@field["max_length"]}
required={@field["required"]}
disabled={@opts[:disabled]}
/>
<%= if @field["description"] do %>
<.label class="label">
<span class="label-text-alt">{@field["description"]}</span>
</.label>
<% end %>
</div>
"""
end
# Textarea
def build_field(%{"type" => "textarea"} = field, changeset, opts) do
assigns = %{field: field, changeset: changeset, opts: opts}
~H"""
<div>
<.label for={@field["key"]}>{@field["label"]}{if @field["required"], do: " *"}</.label>
<textarea
name={"#{@changeset.data.__struct__.__schema__(:source)}[data][#{@field["key"]}]"}
placeholder={@field["placeholder"] || ""}
class={["textarea textarea-bordered w-full", @opts[:input_class]]}
rows={@field["rows"] || 4}
maxlength={@field["max_length"]}
required={@field["required"]}
disabled={@opts[:disabled]}
>{get_field_value(@changeset, @field["key"])}</textarea>
<%= if @field["description"] do %>
<.label class="label">
<span class="label-text-alt">{@field["description"]}</span>
</.label>
<% end %>
</div>
"""
end
# Email Input
def build_field(%{"type" => "email"} = field, changeset, opts) do
assigns = %{field: field, changeset: changeset, opts: opts}
~H"""
<div>
<.label for={@field["key"]}>{@field["label"]}{if @field["required"], do: " *"}</.label>
<input
type="email"
name={"#{@changeset.data.__struct__.__schema__(:source)}[data][#{@field["key"]}]"}
value={get_field_value(@changeset, @field["key"])}
placeholder={@field["placeholder"] || gettext("user@example.com")}
class={["input input-bordered w-full", @opts[:input_class]]}
required={@field["required"]}
disabled={@opts[:disabled]}
/>
<%= if @field["description"] do %>
<.label class="label">
<span class="label-text-alt">{@field["description"]}</span>
</.label>
<% end %>
</div>
"""
end
# URL Input
def build_field(%{"type" => "url"} = field, changeset, opts) do
assigns = %{field: field, changeset: changeset, opts: opts}
~H"""
<div>
<.label for={@field["key"]}>{@field["label"]}{if @field["required"], do: " *"}</.label>
<input
type="url"
name={"#{@changeset.data.__struct__.__schema__(:source)}[data][#{@field["key"]}]"}
value={get_field_value(@changeset, @field["key"])}
placeholder={@field["placeholder"] || gettext("https://example.com")}
class={["input input-bordered w-full", @opts[:input_class]]}
required={@field["required"]}
disabled={@opts[:disabled]}
/>
<%= if @field["description"] do %>
<.label class="label">
<span class="label-text-alt">{@field["description"]}</span>
</.label>
<% end %>
</div>
"""
end
# Rich Text Editor
def build_field(%{"type" => "rich_text"} = field, changeset, opts) do
assigns = %{field: field, changeset: changeset, opts: opts}
~H"""
<div>
<.label for={@field["key"]}>{@field["label"]}{if @field["required"], do: " *"}</.label>
<textarea
name={"#{@changeset.data.__struct__.__schema__(:source)}[data][#{@field["key"]}]"}
placeholder={@field["placeholder"] || gettext("Enter rich text content...")}
class={["textarea textarea-bordered w-full h-32", @opts[:input_class]]}
rows="8"
required={@field["required"]}
disabled={@opts[:disabled]}
>{get_field_value(@changeset, @field["key"])}</textarea>
<.label class="label">
<span class="label-text-alt">{gettext("Rich text editor (HTML supported)")}</span>
</.label>
<%= if @field["description"] do %>
<.label class="label">
<span class="label-text-alt">{@field["description"]}</span>
</.label>
<% end %>
</div>
"""
end
# Number Input
def build_field(%{"type" => "number"} = field, changeset, opts) do
assigns = %{field: field, changeset: changeset, opts: opts}
~H"""
<div>
<.label for={@field["key"]}>{@field["label"]}{if @field["required"], do: " *"}</.label>
<input
type="number"
name={"#{@changeset.data.__struct__.__schema__(:source)}[data][#{@field["key"]}]"}
value={get_field_value(@changeset, @field["key"])}
placeholder={@field["placeholder"] || ""}
class={["input input-bordered w-full", @opts[:input_class]]}
min={@field["min"]}
max={@field["max"]}
step={@field["step"] || 1}
required={@field["required"]}
disabled={@opts[:disabled]}
/>
<%= if @field["description"] do %>
<.label class="label">
<span class="label-text-alt">{@field["description"]}</span>
</.label>
<% end %>
</div>
"""
end
# Boolean Toggle
def build_field(%{"type" => "boolean"} = field, changeset, opts) do
field_value = get_field_value(changeset, field["key"])
is_checked = field_value in [true, "true", "1", 1]
assigns = %{field: field, changeset: changeset, opts: opts, is_checked: is_checked}
~H"""
<div>
<.label>{@field["label"]}{if @field["required"], do: " *"}</.label>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<input
type="hidden"
name={"#{@changeset.data.__struct__.__schema__(:source)}[data][#{@field["key"]}]"}
value="false"
/>
<input
type="checkbox"
name={"#{@changeset.data.__struct__.__schema__(:source)}[data][#{@field["key"]}]"}
value="true"
checked={@is_checked}
class={["toggle toggle-primary", @opts[:input_class]]}
disabled={@opts[:disabled]}
/>
<span class="label-text">
{if @is_checked, do: gettext("Enabled"), else: gettext("Disabled")}
</span>
</label>
</div>
<%= if @field["description"] do %>
<.label class="label">
<span class="label-text-alt">{@field["description"]}</span>
</.label>
<% end %>
</div>
"""
end
# Date Input
def build_field(%{"type" => "date"} = field, changeset, opts) do
assigns = %{field: field, changeset: changeset, opts: opts}
~H"""
<div>
<.label for={@field["key"]}>{@field["label"]}{if @field["required"], do: " *"}</.label>
<input
type="date"
name={"#{@changeset.data.__struct__.__schema__(:source)}[data][#{@field["key"]}]"}
value={get_field_value(@changeset, @field["key"])}
class={["input input-bordered w-full", @opts[:input_class]]}
required={@field["required"]}
disabled={@opts[:disabled]}
/>
<%= if @field["description"] do %>
<.label class="label">
<span class="label-text-alt">{@field["description"]}</span>
</.label>
<% end %>
</div>
"""
end
# Select Dropdown
def build_field(%{"type" => "select"} = field, changeset, opts) do
assigns = %{field: field, changeset: changeset, opts: opts}
~H"""
<div>
<.label for={@field["key"]}>{@field["label"]}{if @field["required"], do: " *"}</.label>
<select
name={"#{@changeset.data.__struct__.__schema__(:source)}[data][#{@field["key"]}]"}
class={["select select-bordered w-full", @opts[:input_class]]}
required={@field["required"]}
disabled={@opts[:disabled]}
>
<%= if @field["allow_empty"] || !@field["required"] do %>
<option value="">{@field["placeholder"] || gettext("Select an option...")}</option>
<% end %>
<%= for option <- (@field["options"] || []) do %>
<option
value={option}
selected={get_field_value(@changeset, @field["key"]) == option}
>
{option}
</option>
<% end %>
</select>
<%= if @field["description"] do %>
<.label class="label">
<span class="label-text-alt">{@field["description"]}</span>
</.label>
<% end %>
</div>
"""
end
# Radio Buttons
def build_field(%{"type" => "radio"} = field, changeset, opts) do
assigns = %{field: field, changeset: changeset, opts: opts}
~H"""
<div>
<.label>{@field["label"]}{if @field["required"], do: " *"}</.label>
<div class="flex flex-col gap-2">
<%= for {option, index} <- Enum.with_index(@field["options"] || []) do %>
<label class="flex items-center cursor-pointer">
<input
type="radio"
name={"#{@changeset.data.__struct__.__schema__(:source)}[data][#{@field["key"]}]"}
value={option}
class={["radio radio-primary mr-2", @opts[:input_class]]}
checked={get_field_value(@changeset, @field["key"]) == option}
required={@field["required"]}
disabled={@opts[:disabled]}
/>
<span class="label-text">{option}</span>
</label>
<% end %>
</div>
<%= if @field["description"] do %>
<.label class="label">
<span class="label-text-alt">{@field["description"]}</span>
</.label>
<% end %>
</div>
"""
end
# Checkbox Group
def build_field(%{"type" => "checkbox"} = field, changeset, opts) do
assigns = %{field: field, changeset: changeset, opts: opts}
~H"""
<div>
<.label>{@field["label"]}{if @field["required"], do: " *"}</.label>
<div class="flex flex-col gap-2">
<%= for {option, index} <- Enum.with_index(@field["options"] || []) do %>
<label class="flex items-center cursor-pointer">
<input
type="checkbox"
name={"#{@changeset.data.__struct__.__schema__(:source)}[data][#{@field["key"]}][]"}
value={option}
class={["checkbox checkbox-primary mr-2", @opts[:input_class]]}
checked={option in (get_field_value(@changeset, @field["key"]) || [])}
disabled={@opts[:disabled]}
/>
<span class="label-text">{option}</span>
</label>
<% end %>
</div>
<%= if @field["description"] do %>
<.label class="label">
<span class="label-text-alt">{@field["description"]}</span>
</.label>
<% end %>
</div>
"""
end
# Image Upload (placeholder - not yet implemented)
def build_field(%{"type" => "image"} = field, changeset, opts) do
assigns = %{field: field, changeset: changeset, opts: opts}
~H"""
<div>
<.label>{@field["label"]}{if @field["required"], do: " *"}</.label>
<div class="border-2 border-dashed border-base-300 rounded-lg p-6 text-center bg-base-200/50">
<.icon name="hero-photo" class="w-12 h-12 mx-auto text-base-content/40 mb-3" />
<p class="text-base-content/60 text-sm mb-2">
{gettext("Image upload coming soon")}
</p>
<p class="text-base-content/40 text-xs">
{gettext("This feature is not yet available")}
</p>
</div>
<%= if @field["description"] do %>
<.label class="label">
<span class="label-text-alt">{@field["description"]}</span>
</.label>
<% end %>
</div>
"""
end
# File Upload (placeholder - not yet implemented)
def build_field(%{"type" => "file"} = field, changeset, opts) do
assigns = %{field: field, changeset: changeset, opts: opts}
~H"""
<div>
<.label>{@field["label"]}{if @field["required"], do: " *"}</.label>
<div class="border-2 border-dashed border-base-300 rounded-lg p-6 text-center bg-base-200/50">
<.icon name="hero-document-arrow-up" class="w-12 h-12 mx-auto text-base-content/40 mb-3" />
<p class="text-base-content/60 text-sm mb-2">
{gettext("File upload coming soon")}
</p>
<p class="text-base-content/40 text-xs">
{gettext("This feature is not yet available")}
</p>
</div>
<%= if @field["description"] do %>
<.label class="label">
<span class="label-text-alt">{@field["description"]}</span>
</.label>
<% end %>
</div>
"""
end
# Relation Field (placeholder - not yet implemented)
def build_field(%{"type" => "relation"} = field, changeset, opts) do
assigns = %{field: field, changeset: changeset, opts: opts}
~H"""
<div>
<.label>{@field["label"]}{if @field["required"], do: " *"}</.label>
<div class="border-2 border-dashed border-base-300 rounded-lg p-6 text-center bg-base-200/50">
<.icon name="hero-link" class="w-12 h-12 mx-auto text-base-content/40 mb-3" />
<p class="text-base-content/60 text-sm mb-2">
{gettext("Entity relations coming soon")}
</p>
<p class="text-base-content/40 text-xs">
{gettext("This feature is not yet available")}
</p>
</div>
<%= if @field["description"] do %>
<.label class="label">
<span class="label-text-alt">{@field["description"]}</span>
</.label>
<% end %>
</div>
"""
end
# Fallback for unknown field types
def build_field(field, changeset, opts) do
assigns = %{field: field, changeset: changeset, opts: opts}
~H"""
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<span>{gettext("Unknown field type: %{type}", type: @field["type"])}</span>
</div>
"""
end
@doc """
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> PhoenixKit.Modules.Entities.FormBuilder.validate_data(entity, %{"title" => "Test"})
{:ok, %{"title" => "Test"}}
iex> PhoenixKit.Modules.Entities.FormBuilder.validate_data(entity, %{})
{:error, %{"title" => ["is required"]}}
"""
def validate_data(entity, data_params) do
fields_definition = entity.fields_definition || []
errors = %{}
validated_data = %{}
result =
Enum.reduce(fields_definition, {validated_data, errors}, fn field, {data_acc, errors_acc} ->
field_key = field["key"]
field_value = Map.get(data_params, field_key)
case validate_field_value(field, field_value) do
{:ok, validated_value} ->
{Map.put(data_acc, field_key, validated_value), errors_acc}
{:error, field_errors} ->
{data_acc, Map.put(errors_acc, field_key, field_errors)}
end
end)
case result do
{validated_data, errors} when map_size(errors) == 0 ->
{:ok, validated_data}
{_data, errors} ->
{:error, errors}
end
end
@doc """
Gets the current value of a field from a changeset.
Helper function to extract field values from changesets or forms for form rendering.
"""
def get_field_value(%Phoenix.HTML.Form{} = form, field_key) do
# When passed a form, access the underlying changeset
# Use Ecto.Changeset.get_field to get the value from changes or fallback to struct
case Ecto.Changeset.get_field(form.source, :data) do
nil -> nil
data when is_map(data) -> Map.get(data, field_key)
_ -> nil
end
end
def get_field_value(changeset, field_key) do
# When passed a changeset directly
case Ecto.Changeset.get_field(changeset, :data) do
nil -> nil
data when is_map(data) -> Map.get(data, field_key)
_ -> nil
end
end
# Private Functions
defp validate_field_value(field, value) do
with {:ok, value} <- validate_required(field, value) do
validate_type(field, value)
end
end
defp validate_required(%{"required" => true}, value) when value in [nil, ""] do
{:error, [gettext("is required")]}
end
defp validate_required(_field, value), do: {:ok, value}
defp validate_type(%{"type" => "email"}, value) when is_binary(value) and value != "" do
if String.contains?(value, "@") do
{:ok, value}
else
{:error, [gettext("must be a valid email address")]}
end
end
defp validate_type(%{"type" => "url"}, value) when is_binary(value) and value != "" do
normalized_value =
if String.starts_with?(value, ["http://", "https://"]) do
value
else
"https://#{value}"
end
{:ok, normalized_value}
end
defp validate_type(%{"type" => "number"}, value) when is_binary(value) and value != "" do
case Float.parse(value) do
{num, ""} -> {:ok, num}
_ -> {:error, [gettext("must be a valid number")]}
end
end
defp validate_type(%{"type" => "boolean"}, value) do
cond do
value in [true, "true", "1", 1] -> {:ok, true}
value in [false, "false", "0", 0, nil, ""] -> {:ok, false}
true -> {:error, [gettext("must be true or false")]}
end
end
defp validate_type(%{"type" => "select", "options" => options}, value) when is_list(options) do
cond do
value in [nil, ""] -> {:ok, nil}
value in options -> {:ok, value}
true -> {:error, [gettext("must be one of: %{options}", options: Enum.join(options, ", "))]}
end
end
defp validate_type(%{"type" => "radio", "options" => options}, value) when is_list(options) do
cond do
value in [nil, ""] -> {:ok, nil}
value in options -> {:ok, value}
true -> {:error, [gettext("must be one of: %{options}", options: Enum.join(options, ", "))]}
end
end
defp validate_type(%{"type" => "checkbox", "options" => options}, values)
when is_list(options) and is_list(values) do
invalid_values = values -- options
if Enum.empty?(invalid_values) do
{:ok, values}
else
{:error,
[gettext("contains invalid options: %{invalid}", invalid: Enum.join(invalid_values, ", "))]}
end
end
defp validate_type(_field, value), do: {:ok, value}
end