Packages

A flexible, composable resource management UI library for Ash Framework applications. Features automatic resource introspection, AshComputer-powered reactive state, and fully customizable components and layouts.

Current section

Files

Jump to
ash_panel lib ash_panel computer_generator form_computer.ex
Raw

lib/ash_panel/computer_generator/form_computer.ex

defmodule AshPanel.ComputerGenerator.FormComputer do
@moduledoc """
Generates a form computer from a ResourceSchema.
Creates a computer that handles create and update forms with validation
and field management.
## Generated Computer: `:form_view`
### Inputs
- `record_id` - ID of record to edit (nil for create)
- `form_data` - Map of field values
- `show_form` - Whether to show the form
- `actor` - Current user for authorization
### Vals
- `record` - Existing record (for update)
- `mode` - :create or :update
- `fields` - Form field definitions
- `errors` - Validation errors
- `submitting?` - Whether form is being submitted
- `success?` - Whether last submission succeeded
- `resource_schema` - The ResourceSchema for this resource
### Events
- `open_create` - Open form in create mode
- `open_edit` - Open form in edit mode with record ID
- `set_field` - Update a field value
- `submit` - Submit the form
- `close` - Close the form
- `reset` - Reset form to initial state
"""
defmacro __using__(opts) do
caller = __CALLER__
eval = fn ast ->
{value, _} = Code.eval_quoted(ast, [], caller)
value
end
resource = opts |> Keyword.fetch!(:resource) |> eval.()
domain = opts |> Keyword.fetch!(:domain) |> eval.()
overrides = opts |> Keyword.get(:overrides, quote(do: %{})) |> eval.()
authorize? = opts |> Keyword.get(:authorize?, quote(do: true)) |> eval.()
quote do
# Build the resource schema at compile time
@form_view_schema AshPanel.Introspection.build_resource_schema(
unquote(resource),
unquote(domain),
overrides: unquote(Macro.escape(overrides))
)
@form_view_authorize? unquote(authorize?)
# Get default values from attributes
@form_view_defaults @form_view_schema.attributes
|> Enum.filter(& &1.show_in_form?)
|> Enum.reject(&is_nil(&1.default))
|> Map.new(&{&1.name, &1.default})
computer :form_view do
input :record_id do
initial(nil)
description("ID of record to edit (nil for create)")
end
input :form_data do
initial(@form_view_defaults)
description("Map of field values")
end
input :show_form do
initial(false)
description("Whether to show the form")
end
input :actor do
initial(nil)
description("Current user for authorization")
end
input :errors do
initial(%{})
description("Validation errors by field")
end
input :submitting? do
initial(false)
description("Whether form is being submitted")
end
input :success? do
initial(false)
description("Whether last submission succeeded")
end
val :record do
description("Existing record (for update mode)")
compute(fn %{record_id: record_id, actor: actor} ->
case record_id do
nil ->
nil
id ->
try do
unquote(resource)
|> Ash.get!(id,
domain: unquote(domain),
actor: actor,
authorize?: @form_view_authorize?
)
rescue
_ -> nil
end
end
end)
end
val :mode do
description("Form mode: :create or :update")
compute(fn %{record_id: record_id} ->
if is_nil(record_id), do: :create, else: :update
end)
end
val :fields do
description("Form field definitions with current values")
compute(fn %{form_data: form_data, record: record} ->
@form_view_schema.attributes
|> Enum.filter(& &1.show_in_form?)
|> Enum.map(fn attr ->
# Get current value (from form_data, record, or default)
value =
Map.get(form_data, attr.name) ||
(record && Map.get(record, attr.name)) ||
attr.default
Map.put(attr, :current_value, value)
end)
end)
end
val :resource_schema do
description("The ResourceSchema for this resource")
compute(fn _values ->
@form_view_schema
end)
end
val :create_action do
description("The primary create action")
compute(fn _values ->
@form_view_schema.actions
|> Enum.find(&(&1.type == :create && &1.primary?))
end)
end
val :update_action do
description("The primary update action")
compute(fn _values ->
@form_view_schema.actions
|> Enum.find(&(&1.type == :update && &1.primary?))
end)
end
event :open_create do
handle(fn _values, _payload ->
%{
record_id: nil,
form_data: @form_view_defaults,
show_form: true,
errors: %{},
success?: false
}
end)
end
event :open_edit do
handle(fn _values, %{"id" => id} ->
# Form data will be populated from record when it loads
%{
record_id: id,
form_data: %{},
show_form: true,
errors: %{},
success?: false
}
end)
end
event :set_field do
handle(fn %{form_data: form_data}, %{"field" => field, "value" => value} ->
new_state =
try do
field_atom = String.to_existing_atom(field)
new_form_data = Map.put(form_data, field_atom, value)
%{form_data: new_form_data, errors: %{}}
rescue
ArgumentError ->
%{}
end
new_state
end)
end
event :submit do
handle(fn %{
mode: mode,
record_id: record_id,
form_data: form_data,
actor: actor
},
_payload ->
# This is a simplified version - in reality, you'd want to:
# 1. Validate the form data
# 2. Call the appropriate action
# 3. Handle errors
# 4. Update state accordingly
# For now, we'll mark as submitting and let the LiveView handle the actual submission
%{submitting?: true}
end)
end
event :close do
handle(fn _values, _payload ->
%{
show_form: false,
form_data: @form_view_defaults,
errors: %{},
success?: false
}
end)
end
event :reset do
handle(fn %{record: record}, _payload ->
# Reset to record values (for update) or defaults (for create)
initial_data =
if record do
@form_view_schema.attributes
|> Enum.filter(& &1.show_in_form?)
|> Map.new(&{&1.name, Map.get(record, &1.name)})
else
@form_view_defaults
end
%{form_data: initial_data, errors: %{}}
end)
end
event :new do
handle(fn _values, _payload ->
%{
record_id: nil,
form_data: @form_view_defaults,
show_form: true,
errors: %{},
success?: false
}
end)
end
event :edit do
handle(fn _values, %{"id" => id} ->
%{
record_id: id,
form_data: %{},
show_form: true,
errors: %{},
success?: false
}
end)
end
event :update_field do
handle(fn assigns, payload ->
AshPanel.ComputerGenerator.FormComputer.update_field(assigns, payload)
end)
end
end
end
end
@doc false
def update_field(%{form_data: form_data}, %{"field" => field, "value" => value}) do
try do
field_atom = String.to_existing_atom(field)
new_form_data = Map.put(form_data, field_atom, value)
%{form_data: new_form_data, errors: %{}}
rescue
ArgumentError ->
%{}
end
end
end