Subscribe to access all episodes. View plans →

#167: Flop Part 1

Published July 24, 2023

Flop

Flop Phoenix

Follow along with the episode starter on GitHub


Here we have a table that lists a few different albums.

And right now this table is static - we can’t sort it by any of the columns or filter it in any way and we don’t have any kind of pagination setup.

To add all these features would take a fair bit of development work.

Luckily for us, there’s a great package - Flop - that will handle this for us. Flop provides filtering, ordering, and pagination with Ecto.

And with Flop Phoenix, it’s incredibly easy to use with Phoenix LiveView.

Let’s use flop_phoenix for our application.

To get started we’ll grab the Mix config from Hex and add it to our list of dependencies.

mix.exs

...

defp deps do
[
  ...
  {:flop_phoenix, "~> 0.20.0"},
  ...
]
end

...

Then we’ll go to the command line and install Flop with mix deps.get:

$ mix deps.get
...
New:
  flop 0.17.2
  flop_phoenix 0.20.0

With that let’s open the template that contains our album table and we’ll want to update this to use the Flop.Phoenix.table component. The docs give an example of how to use it - we’ll base our table on this.

Let’s remove our current table and then I’ll paste in the Flop.Phoenix.table skeleton.

Here we’re setting the items attribute to be a list of our albums the meta attribute, which is a Flop.Meta struct that we’ll get from Flop. And then the path of the page that has our album table.

Now I’ll paste in some column slots for our table to use.

These are just the existing album fields we are using the in table. Fields for the “Title”, “Artist”, “Summary”, and “Year”.

Template path: lib/teacher_web/live/album_live/index.html.heex

...

<Flop.Phoenix.table items={@albums} meta={@meta} path={~p"/albums"}>
  <:col :let={album} label="Title" field={:title}><%= album.title %></:col>
  <:col :let={album} label="Artist" field={:artist}><%= album.artist %></:col>
  <:col :let={album} label="Summary" field={:summary}><%= album.summary %></:col>
  <:col :let={album} label="Year" field={:year}><%= album.year %></:col>
</Flop.Phoenix.table>

...

Then let’s open the corresponding album.ex schema and we’ll need to update this to work with Flop. Looking at the docs there are some examples of how we can do this.

We’ll need to derive the Flop.Schema and then include some fields for the filterable and sortable attributes. Let’s go back to our album.ex module and we’ll derive Flop.Schema and then include filterable with a list of the :title, :artist, and :year fields. Then let’s include sortable with the same fields. All we’re doing here is letting Flop know what fields we want to be filterable and sortable.

lib/teacher/recordings/album.ex

defmodule Teacher.Recordings.Album do
  use Ecto.Schema
  import Ecto.Changeset

  @derive {
    Flop.Schema,
    filterable: [:title, :artist, :year],
    sortable: [:title, :artist, :year]
  }

  ...
end

Now to fetch our albums from the database, we call Recordings.list_albums - we’ll need to make some changes to that function. Instead of querying for all the albums in the database, like we’re currently doing. We’ll want to use the Flop query for our albums.

Let’s remove what we currently have. And we’ll need to pass our params into the function - so let’s add that. We’ll call Flop.validate_and_run with our Album, the params, and then the for: Album option. With this Flop will validate the parameters and then return the retrieved albums and a Flop.Meta struct. Let’s pattern match on the :ok tuple, getting the albums and the meta struct. And then let’s return a map with the albums and meta. If there’s an error we’ll return an empty albums array and then the meta struct.

lib/teacher/recordings.ex

...

def list_albums(params) do
  case Flop.validate_and_run(Album, params, for: Album) do
    {:ok, {albums, meta}} ->
      %{albums: albums, meta: meta}

    {:error, meta} ->
      %{albums: [], meta: meta}

  end

end

...

Then we’ll need to update the LiveView where we call this - our AlbumLive.Index. Let’s open that. When our handle_params callback is invoked it calls the apply_action function so let’s go to that. With Flop we’ll need the params so let’s stop ignoring those.

Then let’s call Recordings.list_albums passing in the params and let’s pattern match on the returned albums and meta struct. Let’s assign our returned albums to our socket and then the meta struct.

lib/teacher_web/live/album_live/index.ex

...

defp apply_action(socket, :index, params) do
  %{albums: albums, meta: meta} = Recordings.list_albums(params)

  socket
  |> assign(:albums, albums)
  |> assign(:meta, meta)
  |> assign(:page_title, "Listing Albums")
end

...

Now let’s go to the command line and try to start our sever and when we do we get an error. We’re not returning a valid map - so let’s go back to our recordings.ex context module and fix that.

We also have one more setup step to get Flop working. We need to give it a Repo to use. Since we’re going to use the same repo for all our queries, it’s easiest to open the config.exs and add a flop config, specifying our Teacher.Repo.

config/config.exs

...

config :flop,
  repo: Teacher.Repo

...

Now let’s try to start our server again.

$ mix phx.sever
...

Everything starts, so let’s go back to our album table and while our album data is being displayed - we don’t have any of our previous stying. In the Flop docs, there’s a list of available options we can use for our table component. We’ll use some of these options to update the table styling.

We’ll create a new module in the “/components” directory called flop_config.ex - this will contain all the styling for our Flop components. Then let’s create a public function named table_opts and in it, I’ll paste a list of attributes to match the table stying we want. This may vary for your application.

lib/teacher_web/components/flop_config.ex

defmodule TeacherWeb.FlopConfig do

  def table_opts do
    [
      table_attrs: [class: "w-full border-collapse border border-slate-400"],
      thead_th_attrs: [class: "p-2 bg-gray-50 border border-slate-300"],
      tbody_td_attrs: [class: "p-2 border border-slate-300"]
    ]
  end

end

Now to include the styling in our table, we’ll go back to our table component, and include the opts attribute returning the styles from our FlopConfig.table_opts function.

Template path: lib/teacher_web/live/album_live/index.html.heex

...

<Flop.Phoenix.table 
  opts={TeacherWeb.FlopConfig.table_opts()} 
  items={@albums} 
  meta={@meta} 
  path={~p"/albums"}>

...

With that let’s go back to the browser - and great - our table is rendered with our updated styles. If we click on the columns we specified as sortable it works - they’re now sortable.

Now let’s add some pagination to the bottom of the page using Flop’s pagination component.

Let’s go back to our album’s index.html.heex that renders our album table. And let’s add the pagination component to the bottom including the meta attribute and then the path attribute with the same path as our album table page. We also want our pagination to have some styling so let’s include the opts attribute. We’ll return all our styles with a new function called pagination_opts.

Template path: lib/teacher_web/live/album_live/index.html.heex

...

<Flop.Phoenix.pagination
  opts={TeacherWeb.FlopConfig.pagination_opts()}
  meta={@meta}
  path={~p"/albums"} />

Now let’s go to our FlopConfig and add the pagination_opts function.

The docs list the different pagination options available. We’ll use these to customize our pagination. Back in our module, I’ll go ahead and paste in some styles for our pagination. These may be different for your application.

lib/teacher_web/components/flop_config.ex

...

def pagination_opts do
  [
    page_links: :hide,
    wrapper_attrs: [
    class: "text-center mt-4"
  ],
    previous_link_content: Phoenix.HTML.raw("← Previous"),
    previous_link_attrs: [
    class: "p-2 mr-2 border rounded border-slate-500"
  ],
    next_link_content: Phoenix.HTML.raw("Next →"),
    next_link_attrs: [
    class: "p-2 ml-2 border rounded border-slate-500"
    ]
  ]
end

With that added let’s go back to our album.ex module and set a default_limit for our pagination to use. Let’s limit this to 4 per page.

lib/teacher/recordings/album.ex

defmodule Teacher.Recordings.Album do
  ...

  @derive {
    Flop.Schema,
    filterable: [:title, :artist, :year],
    sortable: [:title, :artist, :year],
    default_limit: 4
  }

  ...
end

Now if we go back to our album table in the browser. Great - we see only 4 albums returned and the pagination is displayed at the bottom of the page. And if we try to sort our albums and then page through them - everything works as we would expect.

Our table is now set up to use Flop sortable columns and pagination. Stay tuned for part 2 where we’ll make our table filterable.

© 2024 HEXMONSTER LLC