Subscribe to access all episodes. View plans →

#194: Backpex Phoenix Admin Panel

Published August 26, 2024

Backpex

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.

© 2024 HEXMONSTER LLC