Subscribe to access all episodes. View plans →
Published July 24, 2023
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.