Tutorial
Table sorting with Ecto and LiveView
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 have a section on filtering and sorting on dynamic attributes. I want to change how I query all products from:
# lib/shop_test/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/shop_test/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 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 is name or price in ascending or descending order. Even if its strictly not needed, I have started to use adhoc schemaless changesets that I specify directly in the LiveView.
I also want to setup so that I pass in the params list_products
that I changed above. So, I will add the changeset and add the new code to handle_params
callback function.
# lib/shop_test_web/live/product_live/index.ex
defmodule ShopTestWeb.ProductLive.Index do
use ShopTestWeb, :live_view
import Ecto.Changeset
alias ShopTest.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:
<%= f = 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"] %>
<%= select f, :order_by, options, class: "tag-select tag-select-sm" %>
</form>
If you are wondering why I actually use a changeset here is because since I dont 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 its valid, use LiveViews 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 its not valid, do nothing.
# lib/shop_test_web/live/product_live/index.ex
defmodule ShopTestWeb.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,
socket
|> push_patch(to: Routes.product_index_path(socket, :index, apply_changes(changeset)))
}
_ ->
{:noreply, socket}
end
end
end
Note I am calling the changeset order_and_filter_changeset
because it will 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
Tag Cloud
Related Tutorials

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..