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:

  • 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

Phoenix Boilerplate

Generate a Phoenix Boilerplate and save hours on your next project.

Try now

SAAS Starter Kit

Get started and save time and resources by using the SAAS Starter Kit built with Phoenix and LiveView.

Learn More

Related Tutorials

Published 31 Aug - 2020

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

Published 18 Oct

Building a datatable in Phoenix LiveView

liveviewdatatablesortingpagination

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