AshSDUI is a server-driven UI layer for Phoenix LiveView applications backed by Ash. It combines metadata-driven generated screens, persisted or code-authored layout trees, and a shared runtime contract for building Ash-aware LiveView interfaces.

Why AshSDUI

AshSDUI is useful when you want more than scaffolded CRUD, but you still want a declarative path that stays close to your Ash resource model.

  • It separates metadata, view resolution, recipes, and render trees into clear layers.
  • It gives humans and agents a smaller authoring surface through standalone UI modules.
  • It keeps generated screens, custom layouts, and live runtime components on one contract.
  • It can grow from generated pages into product UI without forcing a rewrite of the whole stack.

When to Use AshSDUI

AshSDUI is designed as an authoring ladder for Ash-backed LiveView applications.

Use it when you want:

  • repeated Ash-backed screens that share field, form, action, and query metadata
  • generated or semi-generated forms with relationship selectors and nested forms
  • custom page shells that still keep data loading on one runtime contract
  • server-driven layouts that vary by actor, tenant, audience, device, or runtime state
  • dynamic, ephemeral, or persisted layout trees
  • a smaller, safer authoring surface for humans and LLM agents

Prefer raw LiveView when you are building:

  • one-off bespoke pages with little reusable resource metadata
  • interaction-heavy product UI with custom event flows at every layer
  • screens whose main complexity is not forms, actions, queries, or dynamic layout and reuse

Rule of thumb: AshSDUI starts paying off when several screens share resource metadata such as fields, actions, queries, relationships, bindings, or layout structure. For one highly custom page, raw LiveView may be simpler. For repeated Ash-backed UI, AshSDUI reduces drift and boilerplate.

See When AshSDUI Pays Off for a grounded comparison with demo-backed LOC ranges and adoption guidance.

Features

  • Metadata-driven generated screens through AshSDUI.LiveResource
  • A shared runtime contract for generated views and SDUI layouts: view, bindings, state, and context
  • Layout authoring with AshSDUI.Layout.Builder plus persisted layouts through AshSDUI.Layout
  • Ephemeral runtime layouts through AshSDUI.LiveScreen.assign_layout/3
  • Metadata-driven forms and actions from ui_field, ui_nested_form, ui_attribute, and ui_intent
  • Live bindings with poll, PubSub, and stream-style update paths
  • Reusable runtime-aware components for lists, metrics, status, activity, and selection
  • Storybook and demo surfaces that exercise the generated and layout-rendered paths
  • ETS-backed layout caching with automatic invalidation for stored-node changes

Installation

def deps do
  [
    {:ash_sdui, "~> 0.1"},
    {:phoenix_live_view, "~> 1"}
  ]
end

The built-in AshSDUI.UINode uses ETS storage and is suitable for tests, demos, and local prototypes. Production applications that need database-backed layouts should provide a compatible Ash resource and pass it as node_resource:.

Quickstart

The easiest end-to-end path is:

  1. define UI metadata for an Ash resource
  2. mount it with AshSDUI.LiveResource
  3. expose the generated screens from your router
defmodule MyApp.UI.PostUI do
  use AshSDUI.Resource.Standalone

  sdui do
    for_resource MyApp.Blog.Post
    view :index, recipe: :collection, read_action: :read
    view :new, recipe: :form, action: :create

    ui_field :title, label: "Headline", widget: :text_input, order: 0
    ui_field :body, label: "Body", widget: :textarea, order: 1
    ui_field :author_id, label: "Author", order: 2

    ui_intent :create,
      label: "Write post",
      target: {:navigate, "/posts/new"}
  end
end

defmodule MyAppWeb.PostsLive do
  use AshSDUI.LiveResource,
    ui: MyApp.UI.PostUI,
    view: :index,
    domain: MyApp.Blog
end

defmodule MyAppWeb.PostNewLive do
  use AshSDUI.LiveResource,
    ui: MyApp.UI.PostUI,
    view: :new,
    domain: MyApp.Blog
end
scope "/", MyAppWeb do
  pipe_through :browser

  live "/posts", PostsLive
  live "/posts/new", PostNewLive
end

This gives you:

  • a generated collection screen at /posts
  • a generated form screen at /posts/new
  • form widgets driven by ui_field metadata
  • action labels and targets driven by ui_intent metadata
  • room to add relationship-aware widgets such as generated selects without replacing the generated host

Prefer this path before stepping up to custom recipes or hand-authored LiveViews.

If a generated form should let the user pick an existing related record, keep that in metadata too:

ui_field :author_id,
  label: "Author",
  order: 2

When :author_id matches a belongs_to relationship source attribute such as author, the generated form now renders a select automatically and loads the options from the related resource's read action. For relationship arguments such as :tag_ids, use relationship: and let the form render a multiselect.

If the form should create or edit related records inline, model that separately with ui_nested_form:

create :create do
  accept [:title]
  argument :comments, {:array, :map}, allow_nil?: true

  change manage_relationship(:comments, :comments, type: :direct_control)
end

sdui do
  ui_field :title, label: "Headline", order: 1
  ui_nested_form :comments, label: "Comments", order: 2
end

That keeps relationship picking on ui_field and inline related-record editing on ui_nested_form.

For layout authoring, prefer:

Avoid new code that depends on AshSDUI.Layout.Persistence directly.

Documentation

Tutorial

How-to Guides

Reference

Explanation

Demo and proof surfaces

examples/sdui_demo is the public proof surface for promoted features. It maps generated screens, runtime bindings, hybrid layouts, and persisted layouts to a demo route, Storybook surface, and regression test.

Storybook is part of that proof surface. Prefer generated-view and reusable building-block stories over raw low-level component stories when documenting or reviewing package behavior.

See examples/sdui_demo/README.md for the current coverage matrix.