Tutorial

Table sorting with Ecto and LiveView

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/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 it's 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 listproducts that I changed above. So, I will add the changeset and add the new code to handleparams 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 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 pushpatch to the same route, but with the added params. This will invoke handleparams again and I will do the new database query.
  • If it's 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 orderandfilterchangeset because it will fairly trivial to allow other params like maxprice 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

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 29 Nov - 2022
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..