How to create Phoenix LiveView tables with search filter

July 09, 2022

A Phoenix project with liveview set up in order to do this tutorial, if you don't have one run in your terminal:

mix phx.new Tutorial --app tutorial --module Tutorial --database postgres mix ecto.create

Now that you have a working project. Start by scaffolding migration, schema, context and liveviews for a product schema that has a name and category column.

mix phx.gen.live Store Product products name:string category:string mix ecto.migrate

Add the routes to router.ex

# router.ex scope "/", TutorialWeb do pipe_through :browser get "/", PageController, :index # Add this section live "/products", ProductLive.Index, :index live "/products/new", ProductLive.Index, :new live "/products/:id/edit", ProductLive.Index, :edit live "/products/:id", ProductLive.Show, :show live "/products/:id/show/edit", ProductLive.Show, :edit end

Navigating to localhost:4000/products should render a page looking like this

Change category to a static enum in order to restrict the kind of products the user can create.

# product.ex defmodule Tutorial.Store.Product do use Ecto.Schema import Ecto.Changeset schema "products" do field :category, Ecto.Enum, values: [:shirts, :pants, :shoes] field :name, :string timestamps() end @doc false def changeset(product, attrs) do product |> cast(attrs, [:name, :category]) |> validate_required([:name, :category]) end end

A way to select a value from this enum is required when creating a product. Phoenix a built-in way of doing this.

Change the text_input to the select line in your product_live/form_component.html.heex

# product_live/form_component.html.heex <div> <h2><%= @title %></h2> <.form let={f} for={@changeset} id="product-form" phx-target={@myself} phx-change="validate" phx-submit="save"> <%= label f, :name %> <%= text_input f, :name %> <%= error_tag f, :name %> <%= label f, :category %> <%= select(f, :category, ["Shirts": :shirts, "Pants": :pants, "Shoes": :shoes]) %> <%= error_tag f, :category %> <div> <%= submit "Save", phx_disable_with: "Saving..." %> </div> </.form> </div>

The form will now look like this.

Create a product.

Add a filter to only visualize the products of a certain category.

Change the rowspan of the Name and Edit column to 2. Then add a second table header row with a form inside it. This form is used to filter the table by category. Feel free to call humanize on the product.category too to have prettier display of the value.

# product_live/index.html.heex <h1>Listing Products</h1> <%= if @live_action in [:new, :edit] do %> <.modal return_to={Routes.product_index_path(@socket, :index)}> <.live_component module={TutorialWeb.ProductLive.FormComponent} id={@product.id || :new} title={@page_title} action={@live_action} product={@product} return_to={Routes.product_index_path(@socket, :index)} /> </.modal> <% end %> <table> <thead> <tr> <th rowspan="2">Name</th> <th>Category</th> <th rowspan="2"></th> </tr> <tr> <th> <form phx-change="filter-category"> <select name="category"> <option value="All">All</option> <%= for category <- [:shirts, :pants, :shoes] do %> <option value={category} selected={category == @category_filter}><%= humanize(category) %></option> <% end %> </select> </form> </th> </tr> </thead> <tbody id="products"> <%= for product <- @products do %> <tr id={"product-#{product.id}"}> <td><%= product.name %></td> <td><%= humanize(product.category) %></td> <td> <span><%= live_redirect "Show", to: Routes.product_show_path(@socket, :show, product) %></span> <span><%= live_patch "Edit", to: Routes.product_index_path(@socket, :edit, product) %></span> <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: product.id, data: [confirm: "Are you sure?"] %></span> </td> </tr> <% end %> </tbody> </table> <span><%= live_patch "New Product", to: Routes.product_index_path(@socket, :new) %></span>

Handle the form events and store the value of the category filter inside the socket's assigns. Change the mount function to add a default value to the category_filter assign. Add an event handler for the filter-category event which will be triggered everytime there's a change inside the form.

And finally adapt the list_products function to include take into account category_filter.

# product_live/index.ex defmodule TutorialWeb.ProductLive.Index do use TutorialWeb, :live_view alias Tutorial.Store alias Tutorial.Store.Product @impl true def mount(_params, _session, socket) do {:ok, socket |> assign(:category_filter, "All") |> assign(:products, list_products("All"))} end @impl true def handle_params(params, _url, socket) do {:noreply, apply_action(socket, socket.assigns.live_action, params)} end defp apply_action(socket, :edit, %{"id" => id}) do socket |> assign(:page_title, "Edit Product") |> assign(:product, Store.get_product!(id)) end defp apply_action(socket, :new, _params) do socket |> assign(:page_title, "New Product") |> assign(:product, %Product{}) end defp apply_action(socket, :index, _params) do socket |> assign(:page_title, "Listing Products") |> assign(:product, nil) end @impl true def handle_event("delete", %{"id" => id}, socket) do product = Store.get_product!(id) {:ok, _} = Store.delete_product(product) {:noreply, assign(socket, :products, list_products(socket.assigns.category_filter))} end def handle_event("filter-category", %{"category" => category}, socket) do {:noreply, socket |> assign(:category_filter, category) |> assign(:products, list_products(category))} end defp list_products("All") do Store.list_products() end defp list_products(category_filter) do Store.list_products() |> Enum.filter(fn product -> {:safe, category} = html_escape(product.category) category == category_filter end) end end

There you have it, a liveview table with a select filter!

Full commit here for reference