Packages

Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.

Current section

Files

Jump to
raxol lib raxol ui components modal rendering.ex
Raw

lib/raxol/ui/components/modal/rendering.ex

defmodule Raxol.UI.Components.Modal.Rendering do
@moduledoc """
Rendering logic and form field rendering for the Modal component.
"""
require Raxol.Core.Runtime.Log
require Raxol.View.Elements
require Raxol.Core.Renderer.View
alias Raxol.UI.Components.Selection.Dropdown
@doc "Renders the modal content when visible."
@spec render_modal_content(map()) :: any()
def render_modal_content(state) do
# Get modal style as a Map
box_style_map = get_modal_style(state)
# Convert style Map to Keyword list for Box.new
box_style_keyword = Enum.map(box_style_map, fn {k, v} -> {k, v} end)
Raxol.Core.Renderer.View.Components.Box.new(
id: get_modal_box_id(state),
style: box_style_keyword,
children:
Raxol.View.Elements.column style: %{width: :fill, padding: 1} do
build_modal_elements(
render_title(state.title),
render_content(state),
render_buttons(state.buttons)
)
end
)
end
@doc "Renders the modal title."
@spec render_title(String.t()) :: any()
def render_title(title) do
Raxol.View.Elements.label(content: title, style: %{bold: true})
end
@doc "Renders the modal content based on type."
@spec render_content(map()) :: any()
def render_content(%{content: content} = _state) when is_binary(content) do
Raxol.View.Elements.label(content: content)
end
def render_content(%{type: type} = state) when type in [:prompt, :form] do
render_form_content(state)
end
def render_content(%{content: content}) when not is_nil(content) do
content
end
def render_content(_state) do
nil
end
@doc "Renders modal buttons."
@spec render_buttons(list()) :: list()
def render_buttons(buttons) do
Enum.map(buttons, fn {label, msg} ->
Raxol.View.Elements.button(
label: label,
on_click: {:button_click, msg}
)
end)
end
@doc "Gets modal box ID."
@spec get_modal_box_id(map()) :: any()
def get_modal_box_id(state) do
build_modal_box_id(Map.get(state, :id, nil))
end
defp build_modal_box_id(nil), do: nil
defp build_modal_box_id(id), do: "#{id}-box"
@doc "Gets modal style."
@spec get_modal_style(map()) :: map()
def get_modal_style(state) do
Map.merge(
%{border: :double, width: state.width, align: :center},
state.style
)
end
@doc "Builds modal elements with proper spacing."
@spec build_modal_elements(any(), any(), list()) :: list()
def build_modal_elements(title_element, content_element, button_elements) do
[
title_element,
render_spacer(title_element && content_element),
content_element,
render_spacer(content_element && button_elements != []),
render_button_row(button_elements)
]
|> Enum.reject(&is_nil/1)
end
@doc "Renders spacer element."
@spec render_spacer(boolean()) :: any()
def render_spacer(condition) do
render_spacer_element(condition)
end
defp render_spacer_element(false), do: nil
defp render_spacer_element(_condition),
do: Raxol.View.Elements.label(content: "")
@doc "Renders button row."
@spec render_button_row(list()) :: any()
def render_button_row(button_elements) do
Raxol.View.Elements.row style: %{justify: :center, width: :fill, gap: 2} do
button_elements
end
end
@doc "Renders form content with fields."
@spec render_form_content(map()) :: any()
def render_form_content(state) do
field_elements =
Enum.with_index(state.form_state.fields)
|> Enum.map(&render_field(&1, state))
|> Enum.reject(&is_nil/1)
Raxol.View.Elements.column style: %{width: :fill, gap: 1} do
field_elements
end
end
@doc "Renders a single form field."
@spec render_field({map(), integer()}, map()) :: any()
def render_field({field, index}, state) do
field_full_id =
Raxol.UI.Components.Modal.State.get_field_full_id(field, state)
focused? = index == state.form_state.focus_index
common_props = get_common_props(field, field_full_id, focused?)
input_element = render_input_element(field, common_props)
render_field_container(field, input_element)
end
@doc "Gets common props for form fields."
@spec get_common_props(map(), any(), boolean()) :: map()
def get_common_props(field, field_full_id, focused?) do
Map.merge(field.props || %{}, %{
id: field_full_id,
focused: focused?
})
end
@doc "Renders input element based on field type."
@spec render_input_element(map(), map()) :: any()
def render_input_element(field, common_props) do
case field.type do
:text_input -> render_text_input(field, common_props)
:checkbox -> render_checkbox(field, common_props)
:dropdown -> render_dropdown(field, common_props)
_ -> render_unsupported_field(field)
end
end
@doc "Renders text input field."
@spec render_text_input(map(), map()) :: any()
def render_text_input(field, common_props) do
text_input_props =
Map.merge(common_props, %{
"value" => field.value || "",
"on_change" => {:field_update, field.id}
})
# Convert Map to Keyword list for text_input function
keyword_props =
Enum.map(text_input_props, fn {k, v} ->
{String.to_atom(to_string(k)), v}
end)
Raxol.View.Elements.text_input(keyword_props)
end
@doc "Renders checkbox field."
@spec render_checkbox(map(), map()) :: any()
def render_checkbox(field, common_props) do
checkbox_props =
Map.merge(common_props, %{
"checked" => field.value == true,
"label" => "",
"on_toggle" => {:field_update, field.id}
})
# Convert Map to Keyword list for checkbox function
keyword_props =
Enum.map(checkbox_props, fn {k, v} ->
{String.to_atom(to_string(k)), v}
end)
Raxol.View.Elements.checkbox(keyword_props)
end
@doc "Renders dropdown field."
@spec render_dropdown(map(), map()) :: any()
def render_dropdown(field, common_props) do
dropdown_props =
Map.merge(common_props, %{
"options" => field.options || [],
"initial_value" => field.value,
"width" => :fill,
"on_change" => {:field_update, field.id}
})
%{type: Dropdown, attrs: dropdown_props}
end
@doc "Renders unsupported field type."
@spec render_unsupported_field(map()) :: any()
def render_unsupported_field(field) do
Raxol.Core.Runtime.Log.warning(
"Unsupported form field type in Modal: #{inspect(field.type)}"
)
Raxol.View.Elements.label(content: "[Unsupported Field: #{field.id}]")
end
@doc "Renders field container with label and error."
@spec render_field_container(map(), any()) :: any()
def render_field_container(field, input_element) do
Raxol.View.Elements.column style: %{width: :fill, gap: 0} do
[
render_field_row(field, input_element),
render_field_error(field)
]
|> Enum.reject(&is_nil/1)
end
end
@doc "Renders field row with label and input."
@spec render_field_row(map(), any()) :: any()
def render_field_row(field, input_element) do
Raxol.View.Elements.row style: %{width: :fill, gap: 1} do
[
render_field_label(field.label),
input_element
]
|> Enum.reject(&is_nil/1)
end
end
@doc "Renders field error message."
@spec render_field_error(map()) :: any()
def render_field_error(field) do
render_error_element(field.error)
end
defp render_error_element(nil), do: nil
defp render_error_element(error) do
Raxol.View.Elements.row style: %{width: :fill} do
Raxol.View.Elements.label(
content: error,
style: %{color: :red, padding_left: 16}
)
end
end
@doc "Renders field label if present."
@spec render_field_label(String.t() | nil) :: any()
def render_field_label(nil), do: nil
def render_field_label(label) do
Raxol.View.Elements.label(content: label, style: %{width: 15})
end
end