Premium Tutorial. Learn Stripe Subscription with Phoenix LiveView
Read moreTutorial
View on GithubAdd 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.
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