Xenia.Component (Xenia v0.1.1)

Copy Markdown View Source

Renders the LiveView-owned container for a Xenia adapter.

The JavaScript controller creates an adapter-owned child element inside this container.

The component manages:

  • The LiveView hook binding, using the registered adapter name.
  • The LiveView update binding, set to "ignore".
  • data-xenia-props, containing JSON-encoded adapter props.
  • data-xenia-events, mapping adapter events to LiveView events.
  • data-xenia-shortcuts, mapping key combinations to LiveView events.
  • For form fields, an invisible input that carries the field's value. The adapter keeps it up to date with ctx.setFormValue/2.

Additional HTML attributes such as class, style, aria-, data-, and phx-click can be passed through.

Binding prefix

Xenia defaults to the LiveView binding prefix "phx-".

To use a custom prefix, configure:

config :xenia, binding_prefix: "pop-"

This must match the bindingPrefix option passed to LiveSocket on the JavaScript side.

Examples

import Xenia.Component

<.xenia
  id="editor"
  adapter="Monaco"
  props={%{language: "elixir", value: %{value: @code, version: @version}}}
  events={%{change: "code_changed"}}
  class="editor"
  style="height: 400px"
/>

As a form field, the id and the initial value come from the field:

<.form for={@form} phx-change="validate" phx-submit="save">
  <.xenia field={@form[:code]} adapter="Monaco" props={...} />
</.form>

Keyboard shortcuts, while focus is inside the component, send LiveView events (see Xenia.Shortcuts):

<.xenia id="editor" adapter="Monaco" shortcuts={%{"mod+s" => "save"}} />

A co-located adapter, defined in the same module, is named with colocated/1 (see its docs):

<.xenia id="sales" adapter={colocated(".Chart")} props={...} />

Loading and errors

<:loading> is shown until the island's first mount finishes, for example while a large library downloads. The server renders it, so it is there from the first paint, before any JavaScript runs. Give the component a height (style or class), and the placeholder fills it.

<:error> is shown instead of the island if its adapter throws: in mount, a prop handler, a command or a remount. An element inside it with data-xenia-retry starts the island again. on_crash names a LiveView event that receives %{"id" => id, "phase" => phase, "message" => message}, where phase is "mount", "update", "command" or "remount".

<.xenia id="editor" adapter="Monaco" props={...} on_crash="editor_crashed" style="height: 400px">
  <:loading>Loading the editor…</:loading>
  <:error>
    The editor couldn't start. <button type="button" data-xenia-retry>Try again</button>
  </:error>
</.xenia>

Neither needs any adapter code.

Summary

Functions

The adapter name of a co-located adapter: one defined in this module, in a <script :type={Phoenix.LiveView.ColocatedHook} name=".Chart"> that exports adapterHook({...}) from "xenia".

Renders a container for a registered Xenia adapter.

Functions

binding_prefix()

colocated(name)

(macro)

The adapter name of a co-located adapter: one defined in this module, in a <script :type={Phoenix.LiveView.ColocatedHook} name=".Chart"> that exports adapterHook({...}) from "xenia".

<.xenia id="sales" adapter={colocated(".Chart")} props={%{data: @data}} />

<script :type={Phoenix.LiveView.ColocatedHook} name=".Chart">
  import Chart from "chart.js/auto"
  import { adapterHook } from "xenia"

  export default adapterHook({
    mount(el, props) {
      const canvas = document.createElement("canvas")
      el.appendChild(canvas)
      return new Chart(canvas, { type: "bar", data: props.data })
    },
    destroy(chart) {
      chart.destroy()
    },
  })
</script>

It expands at compile time to the module-qualified name LiveView gives that hook, "MyAppWeb.SalesPage.Chart", so two modules can each define .Chart. <.xenia> can't resolve ".Chart" itself: it runs at render time and never learns which module called it. So colocated/1 and the <script> must be in the same module.

The app needs no change per adapter. It calls createHooks once, and passes the hooks LiveView extracts (import { hooks } from "phoenix-colocated/my_app") to its LiveSocket, as Phoenix's generated app.js does.

xenia(assigns)

Renders a container for a registered Xenia adapter.

The container remains owned by LiveView. The JavaScript controller manages the child element and adapter lifecycle.

Raises ArgumentError for invalid attributes or values that cannot be JSON-encoded.

Attributes

  • adapter (:string) (required) - Adapter name registered in JavaScript createHooks(), or colocated(".Name").
  • id (:string) - Stable DOM id used for commands and adapter instances; defaults to the field's id. Defaults to nil.
  • field (Phoenix.HTML.FormField) - Phoenix form field, for example @form[:code]. Defaults to nil.
  • props (:map) - JSON-encodable properties passed to the adapter. Defaults to %{}.
  • events (:map) - Adapter event name => LiveView event name. Defaults to %{}.
  • shortcuts (:map) - Key combination => LiveView event, while focus is inside, e.g. %{"mod+s" => "save"}. Defaults to %{}.
  • on_crash (:string) - LiveView event sent when the adapter throws, with id, phase and message. Defaults to nil.
  • Global attributes are accepted. Additional HTML attributes not managed by Xenia.

Slots

  • loading - Shown until the island's first mount finishes.
  • error - Shown if the adapter throws; an element with data-xenia-retry starts it again.