Tutorial

Table sorting with Ecto and LiveView

This post was updated 27 Mar

sorting liveview ecto

A very common or even mandatory feature in e-commerce stores is the ability to sort a list of products by attributes. This is easy enough and a good fit for Phoenix LiveView. In this tutorial, I will build on an existing page that displays a list of products and implement sorting on product name and prices.

STEP 1 - Setup dynamic sorting in Ecto

The Ecto documentation has a section on filtering and sorting on dynamic attributes. I want to change how I query all products from:

# lib/my_app/products.ex
def list_products do
Repo.all(Product)
end

To make it possible to add the params from the controller or in this case the LiveView.

# lib/my_app/products.ex
def list_products(params \\ %{}) do
from(
p in Product,
order_by: ^filter_order_by(params["order_by"])
)
|> Repo.all()
end
defp filter_order_by("name_desc"), do: [desc: :name]
defp filter_order_by("name_asc"), do: [asc: :name]
defp filter_order_by("price_desc"), do: [desc: :price]
defp filter_order_by("price_asc"), do: [asc: :price]
defp filter_order_by(_), do: []

NOTE that I allow-list the possible combinations so I don’t end up in a situation where a user can attempt to get the system to behave in an unwanted way.

STEP 2 - Add the form to the LiveView

I want to have a simple select field where a user can specify the sorting. For this example it is name or price in ascending or descending order. Even if it’s strictly not needed, I have started to use ad hoc schemaless changesets that I specify directly in the LiveView.

I also want to set it up so that I pass in the params to list_products that I changed above. So, I will add the changeset and add the new code to the handle_params callback function.

# lib/my_app_web/live/product_live/index.ex
defmodule MyAppWeb.ProductLive.Index do
use MyAppWeb, :live_view
import Ecto.Changeset
alias MyApp.Products
@impl true
def handle_params(params, _url, socket) do
{:noreply,
socket
|> assign(:order_and_filter_changeset, order_and_filter_changeset(params))
|> assign(:products, Products.list_products(params))}
end
# OTHER CODE ...
defp order_and_filter_changeset(attrs \\ %{}) do
cast(
{%{}, %{order_by: :string}},
attrs,
[:order_by]
)
end
end

Then I can add the form to the template:

<.form
for={@order_and_filter_changeset}
phx-change="order_and_filter"
as={:order_and_filter}
>
<% options = ["Order by": "", "Name": "name_asc", "Name (Desc)": "name_desc",
"Price": "price_asc", "Price (Desc)": "price_desc"] %>
<.input type="select" field={@order_and_filter_changeset[:order_by]} options={options} class="tag-select tag-select-sm" />
</.form>

If you are wondering why I actually use a changeset here, it is because since I don’t trust params coming from the big scary Internet, I can validate before I actually use the input.

To tie this up, I need to handle the on change event in the LiveView. So basically I do this by:

  • Cast the params in the changeset
  • If it’s valid, use LiveView’s push_patch to the same route, but with the added params. This will invoke handle_params again and I will do the new database query.
  • If it’s not valid, do nothing.
# lib/my_app_web/live/product_live/index.ex
defmodule MyAppWeb.ProductLive.Index do
# OTHER CODE ..
@impl true
def handle_event("order_and_filter", %{"order_and_filter" => order_and_filter_params}, socket) do
order_and_filter_params
|> order_and_filter_changeset()
|> case do
%{valid?: true} = changeset ->
{:noreply,
push_patch(socket, to: ~p"/products?#{apply_changes(changeset)}")}
_ ->
{:noreply, socket}
end
end
end

Note I am calling the changeset order_and_filter_changeset because it will be fairly trivial to allow other params like max_price and min_price and have a second form that will have range sliders. But I will leave that for another day.

Related Tutorials

Published 18 Oct - 2021
Updated 27 Mar

Building a datatable in Phoenix LiveView

To display a static table on webpage that contains a lot of data is a pretty bad user experience. There are popular javascript libraries that implem..

Published 25 Nov - 2022
Updated 27 Mar
Phoenix 1.7

Sortable lists with Phoenix LiveView and SortableJS

A very common user interface pattern on the web is to have sortable elements. In this tutorial I will go through how to accomplish sortable lists wi..