Writing a Custom Action in ExAdmin

While working on a project utilizing Phoenix and ExAdmin, I found myself needing to customize how ExAdmin created a resource. ExAdmin makes this mostly simple, but that isn't super clear from the documentation.

In my case, I'm building a simple CMS and whenever an admin adds a new post in ExAdmin, a slug, the URL for the post, needs to be generated. All you need to know is that there are 2 models: Posts and Slugs.

I'm only going to override the create action of the posts resource, as I don't need to update the slug when I update a post.

To start out, let's create the controller:

# web/controllers/admin/post_controller.ex
defmodule MyCMS.Admin.PostController do  
  @moduledoc """
  Overrides for Posts in ExAdmin.AdminResourceController
  """
  @resource "posts"

  use ExAdmin.Web, :resource_controller
  alias MyCMS.{Repo, SlugGenerator}

  def create(conn, defn, params) do
  end
end  

With the outline of the controller in place, add a new scope in the controller with the route to setup:

scope "/admin", MyCMS.Admin do  
  pipe_through [:browser, :protected]
  post "/posts", PostController, :create
end  

This scope should be before your ExAdmin scope.

Now that the foundation is in place, we just need to write the internals of MyCMS.Admin.PostController.create/3:

In my case, I just copied what was in ExAdmin.AdminResourceController.create/3:

def create(conn, defn, params) do  
  # Get the model to work with
  model = defn.__struct__
  resource = conn.assigns.resource

  # Build the changeset for the post
  changeset = apply(defn.resource_model, defn.create_changeset, [resource, params[defn.resource_name]])

  case create(changeset) do
    {:error, changeset} ->
      conn |> handle_changeset_error(defn, changeset, params)
    {:ok, resource} ->
      {conn, _, resource} = handle_after_filter(conn, :create, defn, params, resource)
      put_flash(conn, :notice, (gettext "%{model_name} was successfully created.", model_name: (model |> base_name |> titleize) ))
      |> redirect(to: admin_resource_path(resource, :show))
  end
end

defp create(changeset) do  
  # Create the post and generate the slug, then return {:ok, post} or {:error, changeset}
end  

So all that I've changed from ExAdmin's implementation is to pass the changeset to my private create/1 method, which can do whatever I want so long as it returns {:error, changeset} or {ok, %Post{}}

Wrapping Up

So why not just use after_filter/2 in my admin resource to generate the slug? In a lot of cases, that will probably be a good option, but in my case, I wanted to create the post and slug within a transaction so that I don't end up with posts that aren't actually reachable from the frontend.

Dave Long

Read more posts by this author.

Subscribe to Dave Long

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!