Tutorial
Add bulk actions in Phoenix LiveView
In this tutorial, I have a list of products. 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 action on the selected ones.
STEP 1 - Add toggle all feature
The toggle records will be a list of product 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, [])
In the view, 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 product 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.
# lib/shop_test_web/live/product_live/index.html.leex
<!-- In the <thead> -->
<th>
<input type="checkbox" name="toggle-all" id="toggle-all" phx-click="toggle-all" phx-update="ignore" />
</th>
NOTE that I have a phx-update="ignore"
so the checkbox keeps state after the view is updated.
Then for every product 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.
# lib/shop_test_web/live/product_live/index.html.leex
<% checked = if (product.id in @toggle_ids), do: "checked", else: "" %>
<tr id="product-<%= product.id %>">
<td>
<input type="checkbox" name="toggle"
phx-click="toggle" phx-value-toggle-id="<%= product.id %>" <%= checked %> />
</td>
<!-- The other table cells here -->
</tr>
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 like:
# 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 prices and also bulk delete records. The scenario is that I want to bulk update prices all at once so I need to add a form for that.
In the LiveView I need to add a changeset. Its an anonymous changeset and I add it directly in the LiveView. I only want to be able to do bulk updates if the form is valid to I need to make sure that the price is present.
# lib/shop_test_web/live/product_live/index.ex
def mount(_params, _session, socket) do
# other code
|> assign(:price_changeset, price_changeset())
end
# other code
defp price_changeset(attrs \\ %{}) do
cast(
{%{}, %{price: :decimal}},
attrs,
[:price]
)
|> validate_required([:price])
end
In the view, I add the form in a table footer:
<tfoot>
<tr>
<td colspan="5">
<div class="flex items-center">
<div class="flex-initial">
<button type="button" class="btn btn-link btn-sm" phx-click="delete">Delete checked</button>
</div>
<div class="ml-4">
<%= f = form_for @price_changeset, "#", phx_change: "bulk-action", as: "product" %>
<%= text_input f, :price, class: "tag-input tag-input-sm", phx_debounce: 500, placeholder: "Price" %>
</form>
</div>
<div class="ml-4">
<button class="btn btn-sm btn-light" phx-click="bulk-trigger">Update checked</button>
</div>
</div>
</td>
</tr>
</tfoot>
NOTE that I also add the delete button that I will add code for below.
As soon as a update the input field in the price field, I also do the live validation. In this tutorial, I dont do a validation error though.
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 products.
# lib/shop_test_web/live/product_live/index.ex
def handle_event("bulk-action", %{"product" => params}, socket) do
{:noreply, assign(socket, :price_changeset, price_changeset(params))}
end
def handle_event("bulk-trigger", _, socket) do
changeset = socket.assigns.price_changeset
toggle_ids = socket.assigns.toggle_ids
products =
if changeset.valid? do
socket.assigns.products
|> Enum.map(fn product ->
# Check if the product should update
if product.id in toggle_ids do
{:ok, product} = Products.update_product(product, changeset.changes)
product
else
product
end
end)
else
socket.assigns.products
end
{
:noreply,
socket
|> assign(:price_changeset, price_changeset())
|> assign(:products, products)
|> assign(:toggle_ids, [])
}
end
@impl true
def handle_info(_, socket), do: {:noreply, socket}
This should now work to bulk update the prices. But what about bulk delete? I already added the button with phx-click="delete"
. Now its just a matter of handling this in the backend:
# lib/shop_test_web/live/product_live/index.ex
def handle_event("delete", %{}, socket) do
toggle_ids = socket.assigns.toggle_ids
products = Enum.reject(socket.assigns.products, & &1.id in toggle_ids)
products_to_be_deleted = Enum.filter(socket.assigns.products, & &1.id in toggle_ids)
Task.async(fn ->
products_to_be_deleted
|> Enum.each(& Products.delete_product/1)
end)
{
:noreply,
socket
|> assign(:toggle_ids, [])
|> assign(:products, products)
}
end
NOTE that I opted for deleting the products async to speed up the responsiveness of the page a little.
Now, it should I be able to bulk delete all the checked records.
Tag Cloud
Related Tutorials

LiveView and page specific javascript
In most applications you have some page specific javascript that is only used in one or just a few pages. The solution for this is to either setup..