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
Current section
Files
lib/raxol/ui/components/base.ex
defmodule Raxol.UI.Components.Base do
@moduledoc """
Provides base functionality and utilities for Raxol components.
This module contains shared functionality used across different components,
including common validation, styling, and event handling patterns.
"""
@type state :: map()
@doc """
Validates component props against a schema.
## Example
def validate_props(props) do
Base.validate_props(props, %{
required: [:label],
optional: [:style, :disabled],
types: %{
label: :string,
style: {:one_of, [:primary, :secondary, :danger]},
disabled: :boolean
}
})
end
"""
@spec validate_props(map(), map()) :: :ok | {:error, any()}
def validate_props(props, schema) do
with :ok <- validate_required(props, schema[:required] || []),
:ok <- validate_types(props, schema[:types] || %{}) do
:ok
else
{:error, reason} -> {:error, reason}
end
end
@doc """
Creates a base style for components with common properties.
"""
@spec base_style(Keyword.t()) :: any()
def base_style(opts \\ []) do
Raxol.Style.new(
padding: Keyword.get(opts, :padding, [1, 2]),
margin: Keyword.get(opts, :margin, [0, 0]),
border: Keyword.get(opts, :border, :none),
width: Keyword.get(opts, :width, :auto),
height: Keyword.get(opts, :height, :auto)
)
end
@doc """
Handles common events like focus, blur, and keyboard navigation.
Should be called from a component's handle_event callback.
"""
@spec handle_common_events(map(), map()) :: {map(), list()}
def handle_common_events(%{type: :focus} = _event, state) do
{Map.put(state, :focused, true), []}
end
def handle_common_events(%{type: :blur} = _event, state) do
{Map.put(state, :focused, false), []}
end
def handle_common_events(
%{type: :key, data: %{key: key}} = _event,
state
) do
case key do
:tab -> {state, [{:focus_next, state.focus_key}]}
{:shift, :tab} -> {state, [{:focus_previous, state.focus_key}]}
_ -> {state, []}
end
end
def handle_common_events(_event, state), do: {state, []}
# Private functions
defp validate_required(props, required) do
missing = Enum.filter(required, &(not Map.has_key?(props, &1)))
handle_missing_props(missing)
end
defp handle_missing_props([]), do: :ok
defp handle_missing_props(missing) do
{:error, "Missing required props: #{Enum.join(missing, ", ")}"}
end
defp validate_types(props, types) do
Enum.reduce_while(props, :ok, fn {key, value}, :ok ->
case validate_type(value, types[key]) do
:ok ->
{:cont, :ok}
{:error, reason} ->
{:halt, {:error, "Invalid type for #{key}: #{reason}"}}
end
end)
end
defp validate_type(_value, nil), do: :ok
defp validate_type(value, :string) when is_binary(value), do: :ok
defp validate_type(value, :boolean) when is_boolean(value), do: :ok
defp validate_type(value, :integer) when is_integer(value), do: :ok
defp validate_type(value, :atom) when is_atom(value), do: :ok
defp validate_type(value, {:one_of, options}),
do:
if(value in options,
do: :ok,
else:
{:error, "expected one of #{inspect(options)}, got #{inspect(value)}"}
)
defp validate_type(value, type),
do: {:error, "expected #{inspect(type)}, got #{inspect(value)}"}
end