Resource Customization

Copy Markdown View Source

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
end

use PhoenixFilament.Resource Options

OptionTypeRequiredDescription
schema:moduleyesThe Ecto schema module
repo:moduleyesThe Ecto repo module
label:stringnoSingular display name (auto-derived from schema name)
plural_label:stringnoPlural display name (auto-derived)
icon:stringnoHeroicon name for panel navigation
create_changeset:{Module, :function}noChangeset for create. Default: {schema, :changeset}
update_changeset:{Module, :function}noChangeset 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}
end

Both 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
end

Field 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_id

Form 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
end

Sections 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
end

Form 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
end

Fields 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]
end

visible_when options:

KeyDescription
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
end

Table 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
end

Columns

column :field_name                          # auto-derives label from field name
column :field_name, label: "Custom Label"  # explicit column heading

Column order follows declaration order.

Actions

The actions do...end block defines per-row action buttons.

Built-in action types handled automatically:

TypeBehavior
:viewNavigates to the show page
:editNavigates to the edit page
:deleteDeletes the record (with optional confirmation)

All action types accept:

OptionDescription
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)
end

Filters

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"
end

select_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}
  ]
end

date_filter

A date range filter:

filters do
  date_filter :inserted_at, label: "Created After"
  date_filter :published_at, label: "Published After"
end

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}
end

authorize/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 attempted
  • record — the Ecto struct being acted on (nil for :create and :index)
  • user — the value of socket.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
end

All callbacks are defoverridable — call super to execute the default behavior before your customization.

Available overridable callbacks:

  • mount/3
  • handle_params/3
  • handle_event/3
  • handle_info/2
  • render/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{}, ...]