Tutorial

Add bulk actions in Phoenix LiveView

Phoenix 1.7 table stream liveview bulk

In this tutorial, I have a list of customers. One common feature for a table of records is to perform some sort of bulk actions. As an example in this tutorial, I want to be able to toggle all records with a checkbox in the table header and perform some sort of bulk action on the selected ones.

This tutorial is updated to account for Phoenix 1.7 and the new LiveView Streams feature. The difference with streams, is that they are not kept in the LiveView state, but is more a fire and forget.

STEP 1 - Add toggle all feature

The toggle records will be a list of customer id:s so it make sense to start of with mounting the live view with an empty list of ids. So, I will assign the toggle_ids as an empty list to the socket.

# lib/shop_test_web/live/product_live/index.ex
socket
|> assign(:toggle_ids, [])

To store the state of a customer row is toggled or not, I am keeping track of it virtual field on the customer schema. The idea is that when its toggled, I update that virtual field.

defmodule Tutorial.Customers.Customer do
  use Ecto.Schema
  import Ecto.Changeset

  schema "customers" do
		...
    field :toggled, :boolean, default: false, virtual: true
  end
end

In the template, I want to add the toggle-all checkbox in the table header and one checkbox for every row. The idea is that I should be able to either toggle a single customer id or toggle the entire list. So, I will start with adding the main checkbox in the header.

I am adding a phx-click="toggle-all" on the checkbox to be able to send that event to the LiveView.

I have opted for a small hack and pass in the toggle-all checkbox as raw HTML in the header label. I did this instead of updating the table component.

# lib/tutorial_web/live/customer_live/index.html.heex
<% check_all_html = ~s(<input type="checkbox" name="toggle-all" id="toggle-all" phx-click="toggle-all" phx-update="ignore" />) %>

<.table
  id="customers"
  rows={@streams.customers}
  row_click={fn {_id, customer} -> JS.navigate(~p"/customers/#{customer}") end}
>
  <:col :let={{_id, customer}} label={raw(check_all_html)}>
    <% checked = if customer.toggled, do: [checked: "checked"], else: [] %>
    <input type="checkbox" name="toggle"
      phx-click="toggle" phx-value-toggle-id={customer.id} {checked} />
  </:col>

	...other :cols

</.table>

NOTE that I have a phx-update="ignore" so the checkbox keeps state after the view is updated.

Then for every customer row, I insert a new td with checkbox. I am also adding a little helper here to figure out if the checkbox should be checked or not. If they are in the list of @toggle_ids, they should be checked.

Since the checkboxes are there, and I have added the phx-click to trigger events, I need to add those event handlers in the LiveView.

The code I need for adding the functionality of toggling and keeping track of the toggled ids looks lik

# lib/shop_test_web/live/product_live/index.ex

def handle_event("toggle-all", %{"value" => "on"}, socket) do
  product_ids = socket.assigns.products |> Enum.map(& &1.id)
  {:noreply, assign(socket, :toggle_ids, product_ids)}
end

def handle_event("toggle-all", %{}, socket) do
  {:noreply, assign(socket, :toggle_ids, [])}
end

def handle_event("toggle", %{"toggle-id" => id}, socket) do
  id = String.to_integer(id)
  toggle_ids = socket.assigns.toggle_ids

  toggle_ids =
    if (id in toggle_ids) do
      Enum.reject(toggle_ids, & &1 == id)
    else
      [id|toggle_ids]
    end

  {:noreply, assign(socket, :toggle_ids, toggle_ids)}
end

Step 2 - Update and delete toggled record

Just toggling records on and off is not super useful. I want to be able to bulk update an attribute and also bulk delete records. The scenario is that I want to bulk update phone numbers all at once so I need to add a form for that.

In the template, I add the form below the table:

<div class="mt-2 flex items-center space-x-2 border-t border-zinc-200">
  <.button type="button" class="mt-2" phx-click="bulk-delete">Delete checked</.button>
  
  <div class="text-sm font-bold leading-6 text-zinc-600 px-2 mt-2">OR</div>
  
  <.form for={to_form(%{}, as: "customer")} :let={f} phx-submit="bulk-update" class="flex items-center space-x-2">
    <.input field={f[:phone]} placeholder="Phone" label=""/>
    <.button type="submit" class="mt-2">Update checked</.button>
  </.form>
</div>

NOTE that I also add the delete button that I will add code for below.

In the bulk-trigger event, I must first check if the price form is valid. If its valid, I want to apply the changes on all toggles customers.

# lib/tutorial_web/live/customer_live/index.ex

def handle_event("bulk-update", %{"customer" => customer_params}, socket) do
  toggle_ids = socket.assigns.toggle_ids

  customers_to_be_updated =
    Customers.list_customers()
    |> Enum.filter(& &1.id in toggle_ids)

  socket =
    customers_to_be_updated
    |> Enum.reduce(socket, fn customer, memo ->
      case Customers.update_customer(customer, customer_params) do
        {:ok, customer} -> stream_insert(memo, :customers, customer)
        _ -> memo
      end
    end)

  {
    :noreply,
    socket
    |> assign(:toggle_ids, [])
  }
end

This should now work to bulk update the phone numbers. But what about bulk delete? I already added the button with phx-click="bulk-delete". Now its just a matter of handling this in the backend:

# lib/tutorial_web/live/customer_live/index.ex

def handle_event("bulk-delete", %{}, socket) do
  toggle_ids = socket.assigns.toggle_ids

  customers_to_be_deleted =
    Customers.list_customers()
    |> Enum.filter(& &1.id in toggle_ids)

  socket =
    customers_to_be_deleted
    |> Enum.reduce(socket, fn customer, memo ->
      Customers.delete_customer(customer)
      stream_delete(memo, :customers, customer)
    end)

  {
    :noreply,
    socket
    |> assign(:toggle_ids, [])
  }
end

NOTE that I stream delete the customers that should be deleted.

Now, it should I be able to bulk delete all the checked records.

Related Tutorials

Published 31 May - 2023
Phoenix 1.7

CSV Import file upload with preview in LiveView

In this tutorial, I will go through how to upload and import a CSV file with Phoenix LiveView and, show how easy it is to preview the imports before..

Published 03 Apr

Set session values from LiveView

Working with session data can significantly improve the feel of web applications, making interactions feel more connected and dynamic. However, Phoe..