AshBackpex.LiveResource (Ash Backpex v0.3.3)
View SourceThe primary entry point to AshBackpex - an integration library that brings together Ash Framework's powerful resource system with Backpex's admin interface capabilities.
This module provides a clean DSL for creating admin interfaces directly from your Ash resources.
When you use AshBackpex.LiveResource, it automatically generates a Backpex LiveResource
at compile time based on your DSL configuration.
Basic Usage
defmodule MyAppWeb.Admin.PostLive do
use AshBackpex.LiveResource
backpex do
resource MyApp.Blog.Post
layout {MyAppWeb.Layouts, :admin}
fields do
field :title
field :content do
module Backpex.Fields.Textarea
end
field :published
field :author do
display_field :name
live_resource MyAppWeb.Admin.UserLive
end
end
end
endFeatures
Automatic Field Type Detection
AshBackpex automatically derives Backpex field modules from your Ash resource attribute types:
| Ash Type | Backpex Field |
|---|---|
Ash.Type.String | Backpex.Fields.Text |
Ash.Type.Boolean | Backpex.Fields.Boolean |
Ash.Type.Integer | Backpex.Fields.Number |
Ash.Type.Float | Backpex.Fields.Number |
Ash.Type.Date | Backpex.Fields.Date |
Ash.Type.Time | Backpex.Fields.Time |
Ash.Type.DateTime | Backpex.Fields.DateTime |
Ash.Type.UtcDatetime | Backpex.Fields.DateTime |
:belongs_to relationship | Backpex.Fields.BelongsTo |
:has_many relationship | Backpex.Fields.HasMany |
:many_to_many relationship | Backpex.Fields.HasMany |
{:array, _} with one_of | Backpex.Fields.MultiSelect |
Atom with one_of constraint | Backpex.Fields.Select |
Relationship fields automatically derive a Backpex options_query from Ash
relationship filter, sort, and default_sort configuration. This keeps
selectable related records aligned with the relationship itself. You can still
override this by setting options_query explicitly on the field.
Large belongs_to relationships can opt into a server-backed single-select
typeahead:
field :author do
display_field :name
typeahead true
typeahead_limit 10
debounce 300
prompt "Choose an author"
endYou can always override the derived module by specifying it explicitly:
field :content do
module Backpex.Fields.Textarea
endCustom Field Type Mappings
You can configure custom mappings from Ash types to Backpex field modules globally
or per-application. This is useful when you have custom Ash types (like Money or
RichText) that should consistently map to specific Backpex fields.
Configuration Options
Global config (applies to all apps using AshBackpex):
# config/config.exs
config :ash_backpex,
field_type_mappings: %{
MyApp.Types.Money => Backpex.Fields.Currency,
MyApp.Types.RichText => Backpex.Fields.Textarea
}App-scoped config (overrides global for a specific app):
# config/config.exs
config :my_app, AshBackpex,
field_type_mappings: %{
MyApp.Types.Money => MyApp.Fields.MoneyField
}Precedence Rules
Field type derivation follows this precedence order (highest to lowest):
- Explicit
moduleoption in DSL - Always wins - App-scoped config -
config :my_app, AshBackpex, field_type_mappings: ... - Global config -
config :ash_backpex, field_type_mappings: ... - Default mappings - Built-in Ash type to Backpex field mappings
Map Format
Use a map when you have simple type-to-field mappings:
config :ash_backpex,
field_type_mappings: %{
# Override default String -> Text mapping
Ash.Type.String => Backpex.Fields.Textarea,
# Map custom Ash types
MyApp.Types.Money => Backpex.Fields.Currency,
MyApp.Types.RichText => MyApp.Fields.RichTextField,
# Map array types
{:array, Ash.Type.String} => Backpex.Fields.MultiSelect
}Function Format
Use a function when you need to inspect constraints or implement conditional logic:
config :ash_backpex,
field_type_mappings: fn type, constraints ->
case {type, constraints} do
# Map Money type to currency field
{MyApp.Types.Money, _} ->
Backpex.Fields.Currency
# Map strings with max_length > 255 to Textarea
{Ash.Type.String, constraints} when is_list(constraints) ->
if Keyword.get(constraints, :max_length, 0) > 255 do
Backpex.Fields.Textarea
else
nil # Fall back to default
end
# Return nil to use default mapping
_ ->
nil
end
endThe function must have arity 2, receiving (type, constraints), and return either:
- A Backpex field module (atom) - Use this field for the type
nil- Fall back to the default mapping
Common Use Cases
Custom Rich Text field:
config :ash_backpex,
field_type_mappings: %{
MyApp.Types.RichText => MyApp.Fields.TiptapEditor
}Custom Money/Currency field:
config :ash_backpex,
field_type_mappings: %{
Money.Ecto.Composite.Type => Backpex.Fields.Currency
}Override all strings to use Textarea:
config :ash_backpex,
field_type_mappings: %{
Ash.Type.String => Backpex.Fields.Textarea
}See AshBackpex.Config for more details on configuration options and validation.
Ash Authorization Integration
AshBackpex respects your Ash authorization policies. The generated can?/3 callback
checks Ash authorization for CRUD operations:
:new- ChecksAsh.can?({Resource, create_action}, actor):index/:show- ChecksAsh.can?({Resource, read_action}, actor):edit- ChecksAsh.can?({item, update_action}, actor):delete- ChecksAsh.can?({item, destroy_action}, actor)
There are two ways to restrict what an admin can do, and they answer different questions:
- "This admin does not offer the operation to anyone." Set
create_action,update_action, ordestroy_actiontofalse. The generatedcan?/3then denies:new,:edit, or:deletewithout consulting Ash, and the resource keeps its actions for the rest of the application. Use it for a resource that only background jobs write, or a log that must not be edited by hand. - "It depends on who is asking." Write Ash policies on the resource. The
generated
can?/3checks them withassigns.current_useras the actor.
backpex do
resource MyApp.Audit.Event
layout {MyAppWeb.Layouts, :admin}
# Events are written by the application, never in the admin.
create_action false
update_action false
destroy_action false
endRouting a LiveResource with except: [:new] is not enough on its own. Backpex
decides whether to show the "New" button on an empty index page by calling
can?(assigns, :new, nil); it does not look at the routes, so the button links
to a route that does not exist and the page raises. Set create_action false
as well.
Custom item actions fall back to checking if a matching Ash action exists and verifying authorization against it. Actions without a matching Ash action are allowed.
Backpex enforces can?/3 centrally. Backpex.Resource authorizes every
mutation before it builds a changeset, and item actions re-read the selected
records through AshBackpex.Adapter (as the current actor) before authorizing
them. A selection containing a record the actor cannot act on raises
Backpex.ForbiddenError; a record that no longer exists or is not readable by
the actor raises Backpex.NoResultsError. A custom item action that writes the
records it receives through Backpex.Resource should pass authorize?: false,
because Backpex has already authorized them:
def handle(socket, items, _data) do
Backpex.Resource.update_all(items, [set: [archived: true]], socket.assigns,
socket.assigns.live_resource, authorize?: false)
{:ok, socket}
endRelationships, Calculations, and Aggregates
All Ash relationships, calculations, and aggregates can be displayed as fields:
backpex do
resource MyApp.Blog.Post
layout {MyAppWeb.Layouts, :admin}
load [:author, :comments] # Preload relationships
fields do
field :title
field :author do # belongs_to relationship
display_field :name
live_resource MyAppWeb.Admin.UserLive
end
field :word_count # calculation
field :comment_count # aggregate
end
endIf a relationship limits relatable records, for example with
filter expr(type == :public), the generated relationship field uses that
filter when Backpex builds the available options.
Search
Enable search on string fields by setting searchable: true:
fields do
field :title do
searchable true
end
field :content do
searchable true
end
endSearch uses Ash's contains expression and supports multiple searchable fields
(combined with OR logic).
Filters
Add filters to your admin interface:
filters do
filter :published do
module Backpex.Filters.Boolean
end
filter :status do
module MyApp.Filters.StatusFilter
label "Post Status"
end
endCustom Item Actions
Add custom actions to individual items:
item_actions do
action :promote, MyApp.ItemActions.Promote
strip_default [:delete] # Remove default delete action
endCustom Actions
Specify which Ash actions to use for CRUD operations:
backpex do
resource MyApp.Blog.Post
layout {MyAppWeb.Layouts, :admin}
create_action :admin_create
read_action :admin_read
update_action :admin_update
destroy_action :soft_delete
endCustom Changesets
Provide custom changeset functions for create and update operations:
backpex do
resource MyApp.Blog.Post
layout {MyAppWeb.Layouts, :admin}
create_changeset &MyApp.Blog.Post.admin_create_changeset/3
update_changeset &MyApp.Blog.Post.admin_update_changeset/3
endThe changeset function receives (item, params, metadata) where metadata contains:
:assigns- The LiveView assigns:target- The form field that triggered the changeset (ornil)
Defining Backpex Callbacks
AshBackpex generates can?/3, fields/0, filters/0, item_actions/1, and
layout/1 from the DSL and your Ash policies. Defining any of them in a
LiveResource is a compile error: the generated callback would otherwise
replace yours without a warning. Use the fields, filters, item_actions,
and layout DSL entries instead. For a rule that used to live in can?/3:
- to turn creating, editing, or deleting off in this admin for everyone, set
create_action false,update_action false, ordestroy_action false; - for a rule that depends on the actor or the record, write Ash policies on the resource.
See "Ash Authorization Integration" above.
Other Backpex.LiveResource callbacks, such as on_item_updated/2,
return_to/5, form_actions/2, and index_row_class/4, can be defined in the
module as usual. Prefer the DSL where an option exists (singular_name,
plural_name, panels, load); a function definition takes precedence over
it.
Because AshBackpex adds Backpex's defaults after your module body, write
form_actions/2 and index_row_class/4 as pattern-matching clauses without a
catch-all. Unmatched calls fall through to Backpex's default. A catch-all
clause also works, but Elixir reports Backpex's default clause as redundant,
which fails builds that use --warnings-as-errors.
defmodule MyAppWeb.PostLive do
use AshBackpex.LiveResource
backpex do
resource MyApp.Blog.Post
layout {MyAppWeb.Layouts, :admin}
end
@impl Backpex.LiveResource
def form_actions(%{item: %{status: :draft}}, _default_actions) do
[save: %{label: "Save as draft", soft: true}, publish: %{label: "Publish"}]
end
@impl Backpex.LiveResource
def index_row_class(_assigns, %{status: :archived}, _selected, _index), do: "opacity-60"
endRouter Setup
Add routes for your LiveResource in your router:
scope "/admin", MyAppWeb.Admin do
pipe_through [:browser, :admin_auth]
live "/posts", PostLive
endDSL Reference
See AshBackpex.LiveResource.Dsl for complete DSL documentation including all
available options for backpex, fields, filters, and item_actions sections.
Options
:extensions(list of module that adoptsSpark.Dsl.Extension) - A list of DSL extensions to add to theSpark.Dsl:otp_app(atom/0) - The otp_app to use for any application configurable options:fragments(list ofmodule/0) - Fragments to include in theSpark.Dsl. See the fragments guide for more.