A PhoenixFilament Resource is a module that declares how an Ecto schema is exposed in the admin panel — its form fields, table columns, filters, actions, and authorization rules.
Declaring a Resource
defmodule MyAppWeb.Admin.PostResource do
use PhoenixFilament.Resource,
schema: MyApp.Blog.Post,
repo: MyApp.Repo
enduse PhoenixFilament.Resource Options
| Option | Type | Required | Description |
|---|---|---|---|
schema: | module | yes | The Ecto schema module |
repo: | module | yes | The Ecto repo module |
label: | string | no | Singular display name (auto-derived from schema name) |
plural_label: | string | no | Plural display name (auto-derived) |
icon: | string | no | Heroicon name for panel navigation |
create_changeset: | {Module, :function} | no | Changeset for create. Default: {schema, :changeset} |
update_changeset: | {Module, :function} | no | Changeset for update. Default: {schema, :changeset} |
Custom changesets
When your schema has separate create and update changesets:
defmodule MyAppWeb.Admin.UserResource do
use PhoenixFilament.Resource,
schema: MyApp.Accounts.User,
repo: MyApp.Repo,
label: "User",
create_changeset: {MyApp.Accounts.User, :registration_changeset},
update_changeset: {MyApp.Accounts.User, :profile_changeset}
endBoth options take a {Module, :function_name} tuple. The function is called as:
- Create:
Module.function_name(%User{}, params) - Update:
Module.function_name(existing_user, params)
Form DSL
Add a form do...end block inside your resource module to customize the create/edit form.
If no form block is present, PhoenixFilament auto-generates fields from the schema.
defmodule MyAppWeb.Admin.PostResource do
use PhoenixFilament.Resource,
schema: MyApp.Blog.Post,
repo: MyApp.Repo
form do
text_input :title
textarea :body
toggle :published
date :published_at
end
endField types
text_input
Single-line text field. Use for short strings.
text_input :title
text_input :title, label: "Post Title", placeholder: "Enter a title"textarea
Multi-line text field. Use for long text content.
textarea :body
textarea :body, label: "Content"number_input
Numeric input. Suitable for integer and float fields.
number_input :price
number_input :views, label: "View Count"select
Drop-down menu. Requires an options: list.
select :status, options: ["draft", "published", "archived"]
# With labels:
select :status, options: [{"Draft", "draft"}, {"Published", "published"}]
# With label override:
select :category_id, label: "Category", options: [{"Tech", 1}, {"Business", 2}]checkbox
Standard HTML checkbox. Suitable for boolean fields.
checkbox :featured
checkbox :accept_terms, label: "Accept Terms of Service"toggle
Toggle switch for boolean fields. Renders as a daisyUI toggle.
toggle :published
toggle :email_notifications, label: "Email Notifications"date
Date picker input. Works with Ecto :date and :naive_datetime fields.
date :published_at
date :expires_on, label: "Expiration Date"datetime
Date and time picker. Works with Ecto :naive_datetime and :utc_datetime fields.
datetime :published_at
datetime :scheduled_for, label: "Scheduled For"hidden
Hidden input. Useful for sending values without showing them to the user.
hidden :author_id
hidden :tenant_idForm layout: sections
section groups related fields under a labeled heading:
form do
section "Basic Information" do
text_input :title
text_input :slug
end
section "Content" do
textarea :body
textarea :excerpt
end
section "Publishing" do
toggle :published
select :status, options: ["draft", "review", "published"]
date :published_at
end
endSections can be nested inside columns:
form do
columns 2 do
section "Left Column" do
text_input :first_name
text_input :last_name
end
section "Right Column" do
text_input :email
text_input :phone
end
end
endForm layout: columns
columns renders its children in a CSS grid with the specified number of columns:
form do
columns 2 do
text_input :first_name
text_input :last_name
end
columns 3 do
text_input :city
text_input :state
text_input :zip_code
end
endFields within columns are evenly distributed across the grid.
Conditional visibility: visible_when
Show or hide a field (or an entire section) based on another field's current value:
form do
toggle :published
# Only visible when :published is true
date :published_at, visible_when: [field: :published, eq: true]
endvisible_when options:
| Key | Description |
|---|---|
field: | The field name to watch |
eq: | The value that must be present to show this field |
This works on individual fields:
select :discount_type, options: ["none", "percent", "fixed"]
number_input :discount_amount, visible_when: [field: :discount_type, eq: "percent"]
number_input :discount_fixed, visible_when: [field: :discount_type, eq: "fixed"]And on entire sections:
form do
toggle :is_scheduled
section "Schedule Options", visible_when: [field: :is_scheduled, eq: true] do
datetime :scheduled_for
select :timezone, options: ["UTC", "America/New_York", "Europe/Berlin"]
end
endTable DSL
Add a table do...end block to customize the index listing. If omitted, PhoenixFilament
auto-generates columns from the schema.
defmodule MyAppWeb.Admin.PostResource do
use PhoenixFilament.Resource,
schema: MyApp.Blog.Post,
repo: MyApp.Repo
table do
column :title
column :published
column :inserted_at, label: "Created"
actions do
action :view, label: "View", icon: "hero-eye"
action :edit, label: "Edit", icon: "hero-pencil"
action :delete, label: "Delete", icon: "hero-trash", confirm: "Delete this post?"
end
filters do
boolean_filter :published
select_filter :status, options: [{"Draft", "draft"}, {"Published", "published"}]
end
end
endColumns
column :field_name # auto-derives label from field name
column :field_name, label: "Custom Label" # explicit column headingColumn order follows declaration order.
Actions
The actions do...end block defines per-row action buttons.
Built-in action types handled automatically:
| Type | Behavior |
|---|---|
:view | Navigates to the show page |
:edit | Navigates to the edit page |
:delete | Deletes the record (with optional confirmation) |
All action types accept:
| Option | Description |
|---|---|
label: | Button text |
icon: | Heroicon name |
confirm: | Confirmation dialog message before executing the action |
Custom actions
Actions with types other than :view, :edit, :delete dispatch
{:table_action, action_type, record_id} to your resource's handle_info/2.
Override it to handle custom actions:
defmodule MyAppWeb.Admin.PostResource do
use PhoenixFilament.Resource,
schema: MyApp.Blog.Post,
repo: MyApp.Repo
table do
column :title
column :status
actions do
action :edit
action :delete, confirm: "Delete this post?"
action :publish, label: "Publish", icon: "hero-check", confirm: "Publish this post?"
end
end
@impl true
def handle_info({:table_action, :publish, id}, socket) do
post = MyApp.Repo.get!(MyApp.Blog.Post, id)
{:ok, _} = MyApp.Blog.publish_post(post)
{:noreply, Phoenix.LiveView.put_flash(socket, :info, "Post published")}
end
def handle_info(msg, socket), do: super(msg, socket)
endFilters
The filters do...end block renders a filter toolbar above the table.
boolean_filter
A toggle filter for boolean fields:
filters do
boolean_filter :published
boolean_filter :featured, label: "Featured Only"
endselect_filter
A dropdown filter for fields with a known set of values:
filters do
select_filter :status, options: [
{"Draft", "draft"},
{"Published", "published"},
{"Archived", "archived"}
]
select_filter :category_id, label: "Category", options: [
{"Technology", 1},
{"Business", 2},
{"Design", 3}
]
enddate_filter
A date range filter:
filters do
date_filter :inserted_at, label: "Created After"
date_filter :published_at, label: "Published After"
endSearch
Full-text search is always enabled. The search box filters records by matching
across all :string fields in the schema using ILIKE queries.
To disable search on a per-resource basis, this is not yet configurable — search is always active when the schema has string fields.
Pagination and sorting
Pagination and column sorting are enabled automatically:
- Clicking a column header toggles ascending/descending sort
- Pagination controls appear below the table
- Default page size is 20 records
Authorization
Define an authorize/3 function on your resource to gate CRUD operations:
defmodule MyAppWeb.Admin.PostResource do
use PhoenixFilament.Resource,
schema: MyApp.Blog.Post,
repo: MyApp.Repo
# Admins can do everything
def authorize(_action, _record, %{role: "admin"}), do: :ok
# Editors can create and edit but not delete
def authorize(:delete, _record, %{role: "editor"}), do: {:error, :forbidden}
def authorize(_action, _record, %{role: "editor"}), do: :ok
# Viewers can only view
def authorize(:index, _record, %{role: "viewer"}), do: :ok
def authorize(:show, _record, %{role: "viewer"}), do: :ok
def authorize(_action, _record, %{role: "viewer"}), do: {:error, :forbidden}
# Default deny
def authorize(_action, _record, _user), do: {:error, :unauthorized}
endauthorize/3 signature:
@spec authorize(action, record, user) :: :ok | {:error, reason}
when action: :index | :create | :edit | :delete | :show,
record: struct() | nil,
user: any()action— the operation being attemptedrecord— the Ecto struct being acted on (nil for:createand:index)user— the value ofsocket.assigns.current_user
Returning {:error, reason} raises PhoenixFilament.Resource.UnauthorizedError.
If authorize/3 is not defined on the resource, all operations are allowed.
Overriding LiveView Callbacks
use PhoenixFilament.Resource injects default implementations of all LiveView callbacks.
You can override any of them:
defmodule MyAppWeb.Admin.PostResource do
use PhoenixFilament.Resource,
schema: MyApp.Blog.Post,
repo: MyApp.Repo
# Override mount to add custom assigns
@impl true
def mount(params, session, socket) do
{:ok, socket} = super(params, session, socket)
{:ok, assign(socket, :categories, MyApp.Blog.list_categories())}
end
endAll callbacks are defoverridable — call super to execute the default behavior before
your customization.
Available overridable callbacks:
mount/3handle_params/3handle_event/3handle_info/2render/1
Inspecting Resource Metadata
Each resource exposes its configuration via __resource__/1:
MyAppWeb.Admin.PostResource.__resource__(:schema) # => MyApp.Blog.Post
MyAppWeb.Admin.PostResource.__resource__(:repo) # => MyApp.Repo
MyAppWeb.Admin.PostResource.__resource__(:opts) # => keyword list
MyAppWeb.Admin.PostResource.__resource__(:form_fields) # => [%PhoenixFilament.Field{}, ...]
MyAppWeb.Admin.PostResource.__resource__(:table_columns) # => [%PhoenixFilament.Column{}, ...]
MyAppWeb.Admin.PostResource.__resource__(:table_actions) # => [%PhoenixFilament.Table.Action{}, ...]
MyAppWeb.Admin.PostResource.__resource__(:table_filters) # => [%PhoenixFilament.Table.Filter{}, ...]