Creating a Backpex LiveResource
You are an expert at creating LiveResources for Backpex. When the user wants to create a new admin resource, follow this process:
- Identify the Ecto schema the resource will manage
- Generate the LiveResource module with adapter_config, fields, and callbacks
- Add the route to the router
LiveResource Module Structure
defmodule MyAppWeb.PostLive do
use Backpex.LiveResource,
adapter_config: [
schema: MyApp.Post,
repo: MyApp.Repo,
update_changeset: &MyApp.Post.changeset/3,
create_changeset: &MyApp.Post.changeset/3
]
@impl Backpex.LiveResource
def layout(_assigns), do: {MyAppWeb.Layouts, :admin}
@impl Backpex.LiveResource
def singular_name, do: "Post"
@impl Backpex.LiveResource
def plural_name, do: "Posts"
@impl Backpex.LiveResource
def fields do
[
title: %{
module: Backpex.Fields.Text,
label: "Title",
searchable: true
}
]
end
end
use Backpex.LiveResource Options
| Option |
Type |
Default |
Description |
adapter_config |
keyword |
required |
Ecto adapter configuration (see below) |
adapter |
atom |
Backpex.Adapters.Ecto |
Data layer adapter |
primary_key |
atom |
:id |
Primary key field |
per_page_options |
list |
[15, 50, 100] |
Selectable page sizes |
per_page_default |
integer |
15 |
Default page size |
init_order |
map or fn |
%{by: :id, direction: :asc} |
Initial sort order |
fluid? |
boolean |
false |
Full-width layout |
full_text_search |
atom |
nil |
PostgreSQL tsvector column name |
save_and_continue_button? |
boolean |
false |
Show "Save & Continue" button |
pubsub |
keyword |
nil (falls back to :backpex, :pubsub_server app config, topic defaults to module name) |
[server: MyApp.PubSub] |
on_mount |
atom/list |
nil |
LiveView on_mount hooks |
adapter_config (Ecto)
| Key |
Required |
Description |
schema |
yes |
Ecto schema module |
repo |
yes |
Ecto repo module |
update_changeset |
no |
fn item, attrs, metadata -> changeset |
create_changeset |
no |
fn item, attrs, metadata -> changeset |
item_query |
no |
fn query, live_action, assigns -> query |
The metadata keyword list contains :assigns and :target (the form field that triggered the change).
The item_query function must build on the incoming query argument (use from p in query, ...), not the schema directly.
Required Callbacks
| Callback |
Returns |
Description |
singular_name/0 |
string |
e.g. "Post" |
plural_name/0 |
string |
e.g. "Posts" |
fields/0 |
keyword list |
Field definitions |
layout/1 |
{module, :function} or fn assigns -> ... |
Layout to use |
Optional Callbacks
| Callback |
Default |
Description |
can?/3 |
always true |
fn assigns, action, item -> bool |
filters/0 |
[] |
Filter definitions |
filters/1 |
delegates to filters/0 |
Assigns-aware variant for dynamic filters |
panels/0 |
[] |
Field grouping: [key: "Label"] |
metrics/0 |
[] |
Index page metrics |
resource_actions/0 |
[] |
Resource-level actions |
item_actions/1 |
returns default_actions unchanged |
Modify default item actions |
on_item_created/2 |
noop |
fn socket, item -> socket |
on_item_updated/2 |
noop |
fn socket, item -> socket |
on_item_deleted/2 |
noop |
fn socket, item -> socket |
return_to/5 |
index page |
Custom redirect after save |
index_row_class/4 |
nil |
Custom CSS for table rows |
render_resource_slot/3 |
default HTML |
Override UI slots |
translate/1 |
delegates to Backpex.translate/1 |
Override UI strings |
Router Setup
import Backpex.Router
scope "/admin", MyAppWeb do
pipe_through :browser
backpex_routes()
live_session :admin, on_mount: Backpex.InitAssigns do
live_resources "/posts", PostLive
live_resources "/users", UserLive
live_resources "/categories", CategoryLive, only: [:index, :show]
end
end
backpex_routes() must appear once per scope. live_resources/3 generates routes for Index, Form (new/edit), and Show views.
Options for live_resources/3:
only: [:index, :show, :new, :edit] to restrict routes
except: [:new] to exclude specific routes
Complete Example
defmodule MyAppWeb.ProductLive do
use Backpex.LiveResource,
adapter_config: [
schema: MyApp.Product,
repo: MyApp.Repo,
update_changeset: &MyApp.Product.changeset/3,
create_changeset: &MyApp.Product.changeset/3,
item_query: &__MODULE__.item_query/3
],
per_page_default: 25,
init_order: %{by: :inserted_at, direction: :desc}
import Ecto.Query
@impl Backpex.LiveResource
def layout(_assigns), do: {MyAppWeb.Layouts, :admin}
@impl Backpex.LiveResource
def singular_name, do: "Product"
@impl Backpex.LiveResource
def plural_name, do: "Products"
@impl Backpex.LiveResource
def panels do
[details: "Details", metadata: "Metadata"]
end
@impl Backpex.LiveResource
def fields do
[
name: %{
module: Backpex.Fields.Text,
label: "Name",
searchable: true,
panel: :details
},
price: %{
module: Backpex.Fields.Currency,
label: "Price",
panel: :details
},
category: %{
module: Backpex.Fields.BelongsTo,
label: "Category",
display_field: :name,
live_resource: MyAppWeb.CategoryLive,
panel: :details
},
published: %{
module: Backpex.Fields.Boolean,
label: "Published",
index_editable: true
},
inserted_at: %{
module: Backpex.Fields.DateTime,
label: "Created At",
only: [:index, :show],
panel: :metadata
}
]
end
@impl Backpex.LiveResource
def can?(_assigns, :delete, item), do: not item.published
def can?(_assigns, _action, _item), do: true
def item_query(query, _live_action, _assigns) do
from p in query, where: is_nil(p.archived_at)
end
end
Conventions
- File location:
lib/my_app_web/live/<resource>_live.ex
- Module naming:
MyAppWeb.<Resource>Live (e.g. MyAppWeb.ProductLive)
- Changeset functions should accept 3 arguments:
item, attrs, metadata
- Use
layout/1 callback instead of the layout: option (avoids compile-time dependencies)
- Always build on the incoming query in
item_query/3
1---2name: create-live-resource3description: Use when scaffolding a new Backpex LiveResource, setting up admin CRUD views, or configuring adapter_config, fields, filters, and routing for a resource.4---56# Creating a Backpex LiveResource78You are an expert at creating LiveResources for Backpex. When the user wants to create a new admin resource, follow this process:9101. **Identify the Ecto schema** the resource will manage112. **Generate the LiveResource module** with adapter_config, fields, and callbacks123. **Add the route** to the router1314## LiveResource Module Structure1516```elixir17defmodule MyAppWeb.PostLive do18 use Backpex.LiveResource,19 adapter_config: [20 schema: MyApp.Post,21 repo: MyApp.Repo,22 update_changeset: &MyApp.Post.changeset/3,23 create_changeset: &MyApp.Post.changeset/324 ]2526 @impl Backpex.LiveResource27 def layout(_assigns), do: {MyAppWeb.Layouts, :admin}2829 @impl Backpex.LiveResource30 def singular_name, do: "Post"3132 @impl Backpex.LiveResource33 def plural_name, do: "Posts"3435 @impl Backpex.LiveResource36 def fields do37 [38 title: %{39 module: Backpex.Fields.Text,40 label: "Title",41 searchable: true42 }43 ]44 end45end46```4748## `use Backpex.LiveResource` Options4950| Option | Type | Default | Description |51|--------|------|---------|-------------|52| `adapter_config` | keyword | **required** | Ecto adapter configuration (see below) |53| `adapter` | atom | `Backpex.Adapters.Ecto` | Data layer adapter |54| `primary_key` | atom | `:id` | Primary key field |55| `per_page_options` | list | `[15, 50, 100]` | Selectable page sizes |56| `per_page_default` | integer | `15` | Default page size |57| `init_order` | map or fn | `%{by: :id, direction: :asc}` | Initial sort order |58| `fluid?` | boolean | `false` | Full-width layout |59| `full_text_search` | atom | `nil` | PostgreSQL tsvector column name |60| `save_and_continue_button?` | boolean | `false` | Show "Save & Continue" button |61| `pubsub` | keyword | nil (falls back to `:backpex, :pubsub_server` app config, topic defaults to module name) | `[server: MyApp.PubSub]` |62| `on_mount` | atom/list | nil | LiveView on_mount hooks |6364## adapter_config (Ecto)6566| Key | Required | Description |67|-----|----------|-------------|68| `schema` | yes | Ecto schema module |69| `repo` | yes | Ecto repo module |70| `update_changeset` | no | `fn item, attrs, metadata -> changeset` |71| `create_changeset` | no | `fn item, attrs, metadata -> changeset` |72| `item_query` | no | `fn query, live_action, assigns -> query` |7374The `metadata` keyword list contains `:assigns` and `:target` (the form field that triggered the change).7576The `item_query` function must build on the incoming `query` argument (use `from p in query, ...`), not the schema directly.7778## Required Callbacks7980| Callback | Returns | Description |81|----------|---------|-------------|82| `singular_name/0` | string | e.g. `"Post"` |83| `plural_name/0` | string | e.g. `"Posts"` |84| `fields/0` | keyword list | Field definitions |85| `layout/1` | `{module, :function}` or `fn assigns -> ...` | Layout to use |8687## Optional Callbacks8889| Callback | Default | Description |90|----------|---------|-------------|91| `can?/3` | always true | `fn assigns, action, item -> bool` |92| `filters/0` | `[]` | Filter definitions |93| `filters/1` | delegates to `filters/0` | Assigns-aware variant for dynamic filters |94| `panels/0` | `[]` | Field grouping: `[key: "Label"]` |95| `metrics/0` | `[]` | Index page metrics |96| `resource_actions/0` | `[]` | Resource-level actions |97| `item_actions/1` | returns default_actions unchanged | Modify default item actions |98| `on_item_created/2` | noop | `fn socket, item -> socket` |99| `on_item_updated/2` | noop | `fn socket, item -> socket` |100| `on_item_deleted/2` | noop | `fn socket, item -> socket` |101| `return_to/5` | index page | Custom redirect after save |102| `index_row_class/4` | nil | Custom CSS for table rows |103| `render_resource_slot/3` | default HTML | Override UI slots |104| `translate/1` | delegates to `Backpex.translate/1` | Override UI strings |105106## Router Setup107108```elixir109import Backpex.Router110111scope "/admin", MyAppWeb do112 pipe_through :browser113114 backpex_routes()115116 live_session :admin, on_mount: Backpex.InitAssigns do117 live_resources "/posts", PostLive118 live_resources "/users", UserLive119 live_resources "/categories", CategoryLive, only: [:index, :show]120 end121end122```123124`backpex_routes()` must appear once per scope. `live_resources/3` generates routes for Index, Form (new/edit), and Show views.125126Options for `live_resources/3`:127- `only: [:index, :show, :new, :edit]` to restrict routes128- `except: [:new]` to exclude specific routes129130## Complete Example131132```elixir133defmodule MyAppWeb.ProductLive do134 use Backpex.LiveResource,135 adapter_config: [136 schema: MyApp.Product,137 repo: MyApp.Repo,138 update_changeset: &MyApp.Product.changeset/3,139 create_changeset: &MyApp.Product.changeset/3,140 item_query: &__MODULE__.item_query/3141 ],142 per_page_default: 25,143 init_order: %{by: :inserted_at, direction: :desc}144145 import Ecto.Query146147 @impl Backpex.LiveResource148 def layout(_assigns), do: {MyAppWeb.Layouts, :admin}149150 @impl Backpex.LiveResource151 def singular_name, do: "Product"152153 @impl Backpex.LiveResource154 def plural_name, do: "Products"155156 @impl Backpex.LiveResource157 def panels do158 [details: "Details", metadata: "Metadata"]159 end160161 @impl Backpex.LiveResource162 def fields do163 [164 name: %{165 module: Backpex.Fields.Text,166 label: "Name",167 searchable: true,168 panel: :details169 },170 price: %{171 module: Backpex.Fields.Currency,172 label: "Price",173 panel: :details174 },175 category: %{176 module: Backpex.Fields.BelongsTo,177 label: "Category",178 display_field: :name,179 live_resource: MyAppWeb.CategoryLive,180 panel: :details181 },182 published: %{183 module: Backpex.Fields.Boolean,184 label: "Published",185 index_editable: true186 },187 inserted_at: %{188 module: Backpex.Fields.DateTime,189 label: "Created At",190 only: [:index, :show],191 panel: :metadata192 }193 ]194 end195196 @impl Backpex.LiveResource197 def can?(_assigns, :delete, item), do: not item.published198 def can?(_assigns, _action, _item), do: true199200 def item_query(query, _live_action, _assigns) do201 from p in query, where: is_nil(p.archived_at)202 end203end204```205206## Conventions207208- **File location**: `lib/my_app_web/live/<resource>_live.ex`209- **Module naming**: `MyAppWeb.<Resource>Live` (e.g. `MyAppWeb.ProductLive`)210- **Changeset functions** should accept 3 arguments: `item`, `attrs`, `metadata`211- **Use `layout/1` callback** instead of the `layout:` option (avoids compile-time dependencies)212- **Always build on the incoming query** in `item_query/3`