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

Copy Markdown View Source

Editor surface for Athanor — turn-key LiveView + composable primitives for building consumer page-builders.

Three usage modes

1. Turn-key (use Athanor.Editor.Live)

Consumer module becomes the LiveView. Implement load/3 + save/2, optionally override render_header/1 and render_top_bar_actions/1.

defmodule MyApp.PageBuilderLive do
  use Athanor.Editor.Live,
    page_settings_component: MyApp.PageSettings

  @impl Athanor.Editor
  def load(params, session, socket) do
    page = MyContext.get_page(params["id"])
    {:ok, %{content: page.content, metadata: page.metadata,
            ctx_assigns: %{account_id: session["account_id"]}}}
  end

  @impl Athanor.Editor
  def save(socket, %{content: c, metadata: m}) do
    MyContext.save_page(socket.assigns.page, content: c, metadata: m)
  end
end

2. Composable (build your own LiveView)

Use Athanor.Editor.shell/1 as the layout primitive and fill its slots with library LiveComponents (Athanor.Editor.Canvas, Athanor.Editor.ComponentsPanel, Athanor.Editor.ConfigPanel, Athanor.Editor.ZonePickerModal) or your own widgets.

3. Page-level settings

Pass any Athanor.Component as page_settings_component: and it renders at the top of the left sidebar via Athanor.AutoEditorForm. Reuses every field-schema feature (fields/0, resolve_fields, resolve_data, custom field LCs).

Summary

Callbacks

Load initial editor state. Called by the library during mount/3.

Render the top header bar. Optional — library provides a barebones default. Consumers override for branding (back button, brand logo, page title display).

Render the right-side action area of the top bar (Save button, viewport switcher, etc.). Optional — library provides a default with just Save + viewport.

Persist the current editor state. Called by the library on "save" event.

Hook for consumers to seed extra props when a component is first added to the canvas. Called as seed_default_props(component, type, socket) immediately after the library builds the new node from default_props/0. Optional — default no-op.

Functions

Renders the editor canvas — iterates the content tree, wraps each top-level node with edit chrome (Configure button + selection border), dispatches per-node rendering via Athanor.Renderer.node_component/1.

Left-sidebar content. Renders the components palette from Athanor.Registry.components_metadata/0 and, when page_settings_component is provided, renders that component's form via Athanor.AutoEditorForm ABOVE the palette.

Right-sidebar content. Renders the selected component's config form via Athanor.AutoEditorForm when the selected node's module declares fields/0. Falls back to the legacy editor_form/0 LC when set, or to a "no configuration needed" placeholder when neither applies. Renders nothing when nothing is selected (parent shell hides the sidebar region in that case).

Layout primitive for the editor — top bar + 3 columns + modal layer.

Floats a modal layer for adding a component into a Columns zone. Rendered into the shell's :modals slot by consumer LVs when column_picker is set to {parent_id, zone_name}. On submit emits "add_component_to_zone" with the parent + zone + chosen type.

Callbacks

load(params, session, socket)

@callback load(params :: map(), session :: map(), socket :: Phoenix.LiveView.Socket.t()) ::
  {:ok, %{content: map(), metadata: map(), ctx_assigns: map()}}
  | {:error, term()}

Load initial editor state. Called by the library during mount/3.

Return: {:ok, %{content: tree, metadata: map, ctx_assigns: map}} or: {:error, term} to abort mount (consumer can short-circuit via push_navigate etc. in their own mount/3 before delegating).

render_header(assigns)

(optional)
@callback render_header(assigns :: map()) :: Phoenix.LiveView.Rendered.t()

Render the top header bar. Optional — library provides a barebones default. Consumers override for branding (back button, brand logo, page title display).

render_top_bar_actions(assigns)

(optional)
@callback render_top_bar_actions(assigns :: map()) :: Phoenix.LiveView.Rendered.t()

Render the right-side action area of the top bar (Save button, viewport switcher, etc.). Optional — library provides a default with just Save + viewport.

save(socket, state)

@callback save(
  socket :: Phoenix.LiveView.Socket.t(),
  state :: %{content: map(), metadata: map()}
) :: {:ok, any()} | {:error, term()}

Persist the current editor state. Called by the library on "save" event.

Receives %{content: tree_map, metadata: flat_map}. Returns {:ok, anything} for success (library shows success toast) or {:error, term} for failure (library shows error toast).

seed_default_props(component, type, socket)

(optional)
@callback seed_default_props(
  component :: map(),
  type :: String.t(),
  socket :: Phoenix.LiveView.Socket.t()
) :: map()

Hook for consumers to seed extra props when a component is first added to the canvas. Called as seed_default_props(component, type, socket) immediately after the library builds the new node from default_props/0. Optional — default no-op.

Use cases: injecting brand_id/account_id into per-page-defaults for legacy components that read those props at render time.

Functions

canvas(assigns)

Renders the editor canvas — iterates the content tree, wraps each top-level node with edit chrome (Configure button + selection border), dispatches per-node rendering via Athanor.Renderer.node_component/1.

Children of container nodes (Columns zones) get their OWN edit chrome from the container's render(:live, edit_mode=true) — the library Columns renders the per-zone "Add Component" button and per-child Configure button when ctx.edit_mode? + ctx.select_component_callback are set.

Attributes

  • content (:map) (required) - editor_content map (must have "content" key).
  • ctx (Athanor.Ctx) (required)
  • selected_component_id (:string) - Defaults to nil.
  • viewport (:atom) - :desktop | :tablet | :mobile. Defaults to :desktop.

components_panel(assigns)

Left-sidebar content. Renders the components palette from Athanor.Registry.components_metadata/0 and, when page_settings_component is provided, renders that component's form via Athanor.AutoEditorForm ABOVE the palette.

Attributes

  • ctx (Athanor.Ctx) (required)
  • page_settings_component (:atom) - Defaults to nil.
  • metadata (:map) - Defaults to %{}.

config_panel(assigns)

Right-sidebar content. Renders the selected component's config form via Athanor.AutoEditorForm when the selected node's module declares fields/0. Falls back to the legacy editor_form/0 LC when set, or to a "no configuration needed" placeholder when neither applies. Renders nothing when nothing is selected (parent shell hides the sidebar region in that case).

Attributes

  • selected_component_id (:string) - Defaults to nil.
  • content (:map) (required)
  • ctx (Athanor.Ctx) (required)

shell(assigns)

Layout primitive for the editor — top bar + 3 columns + modal layer.

All slot regions are present in the DOM (with stable testids) so consumers can mount the same layout shape regardless of which slots they fill. Slot contents receive a context map with the editor's current display state (page_title, selected_component_id, viewport) via :let={ctx}.

Slots

  • :header — top bar content. Consumer puts back button, title, save button, viewport switcher.
  • :sidebar_left — left panel. Typical content: page settings form (top) + components palette (below).
  • :sidebar_right — right panel. Typical content: selected component's config form (when a node is selected).
  • :modals — floating modal overlay. Library's own zone-picker modal renders here from the consumer LV; consumers can stack additional modals.

Attributes

  • page_title (:string) - Current page title, forwarded to slots. Defaults to nil.
  • selected_component_id (:string) - Selected node id, forwarded to slots. Defaults to nil.
  • viewport (:atom) - Current preview viewport (:desktop|:tablet|:mobile). Defaults to :desktop.
  • show_components_panel (:boolean) - When false, hides the left sidebar and renders an expand button in the canvas margin. Defaults to true.

Slots

  • header
  • sidebar_left
  • sidebar_right
  • modals
  • inner_block - Canvas region content.

zone_picker_modal(assigns)

Floats a modal layer for adding a component into a Columns zone. Rendered into the shell's :modals slot by consumer LVs when column_picker is set to {parent_id, zone_name}. On submit emits "add_component_to_zone" with the parent + zone + chosen type.

Attributes

  • column_picker (:any) (required) - nil OR {parent_id :: String.t(), zone_name :: String.t()}.