Subscribe to access all episodes. View plans →
Published August 26, 2024
Follow along with the episode starter on GitHub
Backpex is a customizable admin panel built for Phoenix LiveView applications. It allows you to quickly create customizable views for your existing data. And has some great features like search and filters, authorization, and supports associations. In this episode, let’s add Backpex to our application here that lists different albums, giving us an interface to manage our album database.
Now before we begin, Backpex has a couple of prerequisites. Your application will need Phoenix LiveView installed, Alpine.js, TailwindCSS, DaisyUI, and it will need Ecto as the database layer. It’s also worth noting that Backpex requires an id
field in your database schema.
Our example application already has LiveView, TailwindCSS, and Ecto installed. We’ll need to add Alpine.js and Daisy UI. To install Alpine.js we’ll go to the command line move into the “assets” directory and run npm install alpinejs
.
$ cd assets && npm install alpinejs
...
Then let’s open our app.js
and we’ll import alpinejs
and initialize it. Then let’s update our LiveSocket
to use it.
assets/js/app.js
...
import Alpine from "alpinejs";
window.Alpine = Alpine;
Alpine.start();
...
let liveSocket = new LiveSocket('/live', Socket, {
dom: {
onBeforeElUpdated (from, to) {
if (from._x_dataStack) {
window.Alpine.clone(from, to)
}
},
},
params: { _csrf_token: csrfToken },
})
...
Now let’s install DaisyUI. We’ll go back to the command line and install DaisyUI in the same “assets” directory.
$ npm i -D daisyui@latest
...
Then we’ll open our tailwind.config.js
and under the “plugins” we’ll require “daisyui”.
Now there’s a conflict between the @tailwindcss/forms
plugin and daisyUI. So let’s remove the @tailwindcss/forms
plugin to prevent styling issues.
assets/tailwind.config.js
...
plugins: [
require('daisyui'),
// require("@tailwindcss/forms"),
...
]
...
Great, now that we have our prerequisites installed, we can setup Backpex.
Let’s to go Hex and copy the backpex
config then we’ll open our Mixfile and add it to our list of dependencies.
mix.exs
...
defp deps do
...
{:backpex, "~> 0.6.0"},
...
end
...
With that added, let’s go back to the command line and move to the root of our application.
From here run mix deps.get
.
$ mix deps.get
...
New:
backpex 0.6.0
...
Then let’s open the tailwind.config.js
and add the Backpex files to the “content” section in order to use the Backpex styles.
assets/tailwind.config.js
...
module.exports = {
content: [
"./js/**/*.js",
"../lib/teacher_web.ex",
"../lib/teacher_web/**/*.*ex",
"../deps/backpex/**/*.*ex"
],
...
Backpex also ships with a formatter configuration. To use it we’ll open our .formatter.exs
and include it in the list of dependencies.
.formatter.exs
[
import_deps: [..., :backpex],
...
]
Now when you’re getting started with Backpex, it doesn’t ship with a predefined layout, so we’ll need to create one. Let’s create a new template in “layouts” called admin.html.heex
- I’ll go ahead and paste in the code for our layout, but let’s walkthrough it.
We’re using a few different Backpex components to build our layout. First, Backpex.HTML.Layout.app_shell
renders an application shell that representing the base of the layout. This accepts a boolean fluid
to determine if the resource should be rendered full width. Then topbar with the Backpex logo. The sidebar, this is we’ll put the link to our “Albums” admin page. We’ve got a link to navigate to, which we’ll implement in just a bit and a current_url
, which Backpex needs to highlight the current sidebar item in the navigation. Then to render any flash messages we have the “flash_messages” component. And finally the @inner_content
assign to render our content.
Template path: lib/teacher_web/components/layouts/admin.html.heex
<Backpex.HTML.Layout.app_shell fluid={@fluid?}>
<:topbar>
<Backpex.HTML.Layout.topbar_branding />
<Backpex.HTML.Layout.topbar_dropdown>
<:label>
<label tabindex="0" class="btn btn-square btn-ghost">
<.icon name="hero-user" class="h-8 w-8" />
</label>
</:label>
<li>
<.link navigate={~p"/"} class="flex justify-between text-red-600 hover:bg-gray-100">
<p>Logout</p>
<.icon name="hero-arrow-right-on-rectangle" class="h-5 w-5" />
</.link>
</li>
</Backpex.HTML.Layout.topbar_dropdown>
</:topbar>
<:sidebar>
<Backpex.HTML.Layout.sidebar_item current_url={@current_url} navigate={~p"/admin/albums"}>
<.icon name="hero-book-open" class="h-5 w-5" /> Albums
</Backpex.HTML.Layout.sidebar_item>
</:sidebar>
<Backpex.HTML.Layout.flash_messages flash={@flash} />
<%= @inner_content %>
</Backpex.HTML.Layout.app_shell>
Now to render a resource, Backpex provides a LiveResource. A LiveResource is simply a module that contains the configuration for each resource we want to use with Backpex. It defines things like the schema, the actions that can be performed and the fields we want to display. Let’s create a LiveResource for our albums.
We’ll create a new module in the “live” directory called album_live.ex
.
Then we can use the Backpex.LiveResource
macro with some options. I’ll go ahead and paste in the required options for our LiveResource.
We have the layout
for the resource the schema, the repo
, then the update_changeset
and create_changeset
that we want to use. pubsub
and then topic
and event_prefix
. These are used to configure PubSub for your LiveResources, which is a really cool feature of Backpex. Then we need to implement a few different callback functions in our module.
We’ll implement singular_name
returning the value “Album” and the plural_name
returning the plural name of our resource “Albums”. Then fields
this will return a list of the different resource fields we want to display in our admin UI. If we open the Backpex docs, we can see the different built in field types that ship with Backpex and an example of what the fields
function should look like.
Let’s add a configuration for the “artist” field for the type we’ll use the Backpex.Fields.Text
module and then the label “Albums”. Great, now I’ll go ahead and paste in the configuration for our other fields, “title”, “year”, and “description”. With those added let’s go back to the update_changeset
and create_changeset
options. We’re using Album.changeset/3
.
lib/teacher_web/live/album_live.ex
defmodule TeacherWeb.Live.AlbumLive do
use Backpex.LiveResource,
layout: {TeacherWeb.Layouts, :admin},
schema: Teacher.Music.Album,
repo: Teacher.Repo,
update_changeset: &Teacher.Music.Album.changeset/3,
create_changeset: &Teacher.Music.Album.changeset/3,
pubsub: Teacher.PubSub,
topic: "albums",
event_prefix: "album_"
@impl Backpex.LiveResource
def singular_name, do: "Album"
@impl Backpex.LiveResource
def plural_name, do: "Albums"
@impl Backpex.LiveResource
def fields do
[
artist: %{
module: Backpex.Fields.Text,
label: "Artist"
},
title: %{
module: Backpex.Fields.Text,
label: "Title"
},
year: %{
module: Backpex.Fields.Number,
label: "Year"
},
description: %{
module: Backpex.Fields.Textarea,
label: "Description"
}
]
end
end
But if open our Album
module we see the the default changeset
generated by Phoenix has two parameters. Backpex includes a 3rd for the metadata. Let’s update our module to handle that. We’ll define a new changeset
function, ignoring the metadata since we wont need it for this example. And then we’ll call our original changeset function.
lib/teacher/music/album.ex
defmodule Teacher.Music.Album do
...
def changeset(album, attrs, _metadata) do
changeset(album, attrs)
end
...
end
Great, now let’s setup our router to use Backpex. We’ll add import Backpex.Router
. Then let’s setup our admin routes to be prefixed with “/admin” and we’ll pipe them through the “browser” pipeline. Inside this we can add the backpex_routes
macro to add a Backpex route to set the cookies needed for the LiveResource.
Earlier, we set a current_url
in our admin.html.heex
layout. Backpex provides a Backpex.InitAssigns
module that will attach that to the LiveView for us, so let’s include a live_session
with that here. Then we can add routes for our resource with the live_resources
macro for our AlbumLive
.
lib/teacher_web/router.ex
...
import Backpex.Router
...
scope "/admin", TeacherWeb do
pipe_through :browser
backpex_routes()
live_session :default, on_mount: Backpex.InitAssigns do
live_resources "/albums", Live.AlbumLive
end
end
...
With that, we should be able to access the Backpex admin panel from the new routes in our application.
So let’s go to the command line and start the server.
$ mix phx.server
...
Now we can access the album LiveResource from the “/admin/albums” route and everything is loaded, but we do have an issue with the background color of the page.
If we open our root.html.heex
template there’s a conflict with the background color set in our body tag. We have two options to fix this. We can either remove this style or we can create another root layout to use with Backpex. Let’s create another root layout.
In our “layouts” directory we’ll create another template called root_admin.html.heex
. And then I’ll paste in the code for it this is the same code we have for our root.html.heex
layout minus the background color on the body.
Template path:lib/teacher_web/components/layouts/root_admin.html.heex
<!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title suffix=" · Phoenix Framework">
<%= assigns[:page_title] || "Teacher" %>
</.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
</head>
<body class="antialiased">
<%= @inner_content %>
</body>
</html>
Now we just need to go to the router.ex
and let’s create another pipeline
called “admin” and in it we’ll call put_root_layout
with our new root_admin
layout.
Then we can update our “/admin” scope to use the “admin” pipeline. This should now use our new root layout for our Backpex routes.
lib/teacher_web/router.ex
...
pipeline :admin do
plug :put_root_layout, html: {TeacherWeb.Layouts, :root_admin}
end
...
scope "/admin", TeacherWeb do
pipe_through [:browser, :admin]
...
end
...
Let’s go back to the browser and if we refresh the page - great - it worked. Our page is loading with the correct styling and we can see the admin UI is working. We can view an album and if we edit it, it’s updated!
This looks great. But if we go back to the our “Albums” index page we see there’s a small issue with the way the year value is being displayed. Let’s fix this by updating the “field” that’s used to render our year.
Doing that in Backpex is easy with a Custom Field. We can see from the docs what a basic custom field looks like. We need to two functions: render_value
and render_form
. Let’s create a new directory in “teacher_web” called “fields” and inside it we’ll create a year.ex
module for our custom field. We’ll define the module and it will use BackpexWeb, :field
. We’ll implement render_value
and from it we’ll just return some HEEX with the value. Then we’ll need to add the render_form
function. And since we don’t need to make any changes to the form, let’s use the Backpex.Fields.Number.render_form
function to render the form that this is currently using.
Module path: lib/teacher_web/fields/year.ex
defmodule TeacherWeb.Fields.Year do
use BackpexWeb, :field
@impl Backpex.Field
def render_value(assigns) do
~H"""
<p>
<%= @value %>
</p>
"""
end
@impl Backpex.Field
def render_form(assigns) do
Backpex.Fields.Number.render_form(assigns)
end
end
Then let’s go back to the AlbumLive
and update the module we’re using for the “year” to use our new custom field.
lib/teacher_web/live/album_live.ex
...
@impl Backpex.LiveResource
def fields do
[
...
year: %{
module: TeacherWeb.Fields.Year,
label: "Year"
},
...
]
...
Great, now when we go back to the browser - we see our year is displayed correctly! Now, let’s look at one last feature of Backpex - filters, which let you filter the data that’s displyed on a resources index page.
Let’s create a filter that will allow us to filter albums by their year. In “teacher_web”, let’s create another directory called “filters” then we’ll create a module for our filter. Let’s call it album_year_range.ex
.
We’ll define the module. Then we’ll need to pick a filter to use. Backpex provides a few different ones to choose from. Let’s use the Range
filter for our example. Then we need to implement the type callback, to return the type for the filter to use. Here we’ll return the :number
type. And then let’s add the Backpex.Filter
label
callback, to add a label to our filter.
lib/teacher_web/filters/album_year_range.ex
defmodule TeacherWeb.Filters.AlbumYearRange do
use Backpex.Filters.Range
@impl Backpex.Filters.Range
def type, do: :number
@impl Backpex.Filter
def label, do: "Year range"
end
With our AlbumYearRange
filter module ready, let’s go back to our album live resource and define the filters
callback and this should return a keyword list of all the filters we want to use.
Let’s add our year and then the module for our filter.
lib/teacher_web/live/album_live.ex
...
@impl Backpex.LiveResource
def filters do
[
year: %{
module: TeacherWeb.Filters.AlbumYearRange
}
]
end
...
With that let’s go back to the admin panel and now we see a “Filters” section. If we click it, we see we it renders a form that we can use to filter the albums that are displayed by their year.
And perfect - we can see the albums being filtered as we type. This is great. Backpex is now setup to work with our application. We can now manage our albums all from the Backpex admin panel.