Athanor.Component behaviour (Athanor v0.1.0-beta.1)

Copy Markdown View Source

Behaviour every Athanor-aware component implements.

At v1 only metadata/0 is required. Every other callback has a default injected by use Athanor.Component that components can override at will.

Defining a component

defmodule MyApp.Components.Promo do
  use Athanor.Component

  @impl Athanor.Component
  def metadata, do: %{type: "promo", label: "Promo", icon: "fa-bullhorn"}

  @impl Athanor.Component
  def required_props, do: ["headline"]

  @impl Athanor.Component
  def render(:live, node, _ctx) do
    assigns = %{headline: node["props"]["headline"]}
    ~H"<h2>{@headline}</h2>"
  end
end

Render targets

Only :live is supported in v1. Future steps add :static, :mjml, :text without breaking the callback signature.

Editor configuration: two ways

Components describe how they are edited in one of two ways.

Preferred: fields/0 (declarative)

Return a list of field tuples describing inputs. Athanor auto-generates the configure panel.

def fields, do: [
  {"title", :text,     label: "Title"},
  {"level", :select,   label: "Level", options: [{"H1", "1"}, {"H2", "2"}]},
  {"image", :custom,   label: "Image", module: MyApp.MediaPicker}
]

Built-in types: :text, :textarea, :number, :select, :color, :checkbox. The :custom type mounts a consumer-supplied module implementing Athanor.Field. See Athanor.Field and Athanor.Fields.

Legacy: editor_form/0 (LiveComponent)

Return a Phoenix.LiveComponent module the editor mounts directly. Use this when you need state, multi-step flows, or behaviour the built-in field types don't cover. Return nil (default) for no configuration.

Dispatch order

When both are defined, fields/0 (returning a non-empty list) wins. See Athanor.Renderer for the full dispatch rules.

Child zones

Container components (e.g., Columns) override child_zones/1 to expose their children to Athanor.Tree walk/find/insert/remove/move. Default is no children.

Summary

Functions

Default validate/1 used when a component does not override.

Types

field()

@type field() :: {String.t(), field_type(), keyword()}

field_type()

@type field_type() ::
  :text | :textarea | :number | :select | :color | :checkbox | :custom

props()

@type props() :: map()

target()

@type target() :: :live

tree_node()

@type tree_node() :: %{required(String.t()) => any()}

Callbacks

child_zones(tree_node)

(optional)
@callback child_zones(tree_node()) :: %{required(String.t()) => [tree_node()]}

default_props()

(optional)
@callback default_props() :: props()

editor_form()

(optional)
@callback editor_form() :: module() | nil

fields()

(optional)
@callback fields() :: [field()]

metadata()

@callback metadata() :: %{
  :type => String.t(),
  :label => String.t(),
  optional(:icon) => String.t(),
  optional(:category) => atom(),
  optional(:description) => String.t(),
  optional(:placeholder) => String.t()
}

render(target, tree_node, t)

(optional)
@callback render(target(), tree_node(), Athanor.Ctx.t()) :: any()

required_props()

(optional)
@callback required_props() :: [String.t()]

resolve_data(old_props, new_props)

(optional)
@callback resolve_data(old_props :: props(), new_props :: props()) :: props()

resolve_fields(props, map)

(optional)
@callback resolve_fields(props(), map()) :: [field()]

validate(props)

(optional)
@callback validate(props()) :: :ok | {:error, term()}

Functions

default_validate(props, required)

Default validate/1 used when a component does not override.

Returns :ok if every key listed in required_props/0 is present and non-blank in props. Otherwise {:error, {:missing, [missing_keys]}}.