Psst. It would be super cool if you could try the new Phoenix Boilerplate!

Try now

Tutorial

View on Github

Table sorting with Ecto and LiveView

ectoliveviewsorting

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:

# 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


What are you working on?

If you want, you can send me a link to your Phoenix or Phoenix LiveView project so. Lets connect on Twitter or Linkedin.

- Andreas Eriksson, web developer since 2005

Related Tutorials

Published 15 Feb

Create ghost loading cards in Phoenix LiveView

ectoliveviewphoenix

Unless you already didn't know, when a LieView component is mounted on a page, it runs the mount/2 function twice. One when the page is rendered fro..

Published 31 Aug

Fuzzy find with Ecto in Phoenix LiveView

ectoliveviewfuzzyseachsearch

Fuzzy find is both a simple and a complex thing. Even though though it's simple to implement, its hard to get right from a UX perspective. Luckily, ..