CRUD

Deleting a product

Read time: 6 minutes

Let's skip to the last letter of CRUD: Delete. In this lesson, we'll see how simple it is to create a UX to delete an item using resources from our project.

This lesson is a direct continuation of the previous one.

If you hopped directly into this page it might be confusing because it is a direct continuation of the code from the previous lesson. If you want to skip the previous lesson and start straight with this one, you can clone the initial version for this lesson using the command git clone https://github.com/adopt-liveview/first-crud.git --branch show-data-done.

#You probably guessed that we'd start with the Context.

The first step is to go back to our lib/super_store/catalog.ex file and add a new function:

defmodule SuperStore.Catalog do
  # ...

  def delete_product(%Product{} = product) do
    Repo.delete(product)
  end
end

The delete_product/1 function takes a struct of type %Product{} and simply applies the Repo.delete/2 method to it. The result will be {:ok, %Product{}} which is useful if it is necessary know about the deleted product.

#Testing on iex

Using the reliable Elixir Interactive mode we can fetch the last product with product = SuperStore.Catalog.list_products() |> List.last and delete it using SuperStore.Catalog.delete_product(product):

$ iex -S mix

[info] Migrations already up
Erlang/OTP 26 [erts-14.2.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit]

Interactive Elixir (1.16.1) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> product = SuperStore.Catalog.list_products() |> List.last

[debug] QUERY OK source="products" db=0.2ms queue=0.1ms idle=1192.5ms
SELECT p0."id", p0."name", p0."description" FROM "products" AS p0 []
 :elixir.eval_external_handler/3, at: src/elixir.erl:405

%SuperStore.Catalog.Product{
  __meta__: #Ecto.Schema.Metadata<:loaded, "products">,
  id: 10,
  name: "asda",
  description: "ad"
}

iex(2)> SuperStore.Catalog.delete_product(product)
[debug] QUERY OK source="products" db=1.7ms idle=1366.3ms
DELETE FROM "products" WHERE "id" = ? [10]
 :elixir.eval_external_handler/3, at: src/elixir.erl:405
{:ok,
 %SuperStore.Catalog.Product{
   __meta__: #Ecto.Schema.Metadata<:deleted, "products">,
   id: 10,
   name: "asda",
   description: "ad"
 }}

#Deleting products in the list LiveView

Instead of creating a new LiveView called ProductLive.Delete, we can reuse the product list for this purpose. Open your ProductLive.Index located in lib/super_store_web/live/product_live/index.ex.

#The <:action> slot of the <.table> component

Within your render/1, update your <.table> to the following code:

<.table
  id="products"
  rows={@streams.products}
  row_click={fn {_id, product} -> JS.navigate(~p"/products/#{product}") end}
>
  <:col :let={{_id, product}} label="Name"><%= product.name %></:col>
  <:col :let={{_id, product}} label="Description"><%= product.description %></:col>
  <:action :let={{id, product}}>
    <.link
      phx-click={JS.push("delete", value: %{id: product.id}) |> hide("##{id}")}
      data-confirm="Are you sure?"
    >
      Delete
    </.link>
  </:action>
</.table>

We added a slot called <:action> where we receive both the id and the product using the special attribute :let. This slot is placed as the last column to add action buttons to our row.

#The ID from the :let

This specific id is known as the "DOM ID" or "HTML ID" and, in this case, it should be something like "products-123" because our table has the ID "products" and assuming the ID in the database of the item is 123. It's useful for applying JS commands.

#Confirming actions with data-confirm

The next focus point is data-confirm. We don't want the item to be deleted immediately without any kind of confirmation, right? Phoenix checks that if you click on an element with data-confirm it triggers a confirm dialog in your browser and only applies the phx-click if the user confirms.

#The hide/2 command

Within our phx-click binding, two things occur:

  1. We send an event to our LiveView called "delete" (we still need to define it).
  2. We hide the element of the current row using the HTML ID.

As you can see, we're not directly using JS.hide/2 but rather just the hide/1 function. This is because Phoenix already provides this simplified function within CoreComponents, which applies transitions using CSS classes! Look at your CoreComponents:

def hide(js \\ %JS{}, selector) do
  JS.hide(js,
    to: selector,
    time: 200,
    transition:
      {"transition-all transform ease-in duration-200",
       "opacity-100 translate-y-0 sm:scale-100",
       "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
  )
end

Whenever possible, prefer to use hide/1 from CoreComponents. However, if you need to customize the transition, opt for JS.hide/2.

#Creating the delete event

To be able to test this code we need to create our handle_event/3. In your LiveView. Below the mount/3 function add this callback:

def handle_event("delete", %{"id" => id}, socket) do
  product = Catalog.get_product!(id)
  {:ok, _} = Catalog.delete_product(product)

  {:noreply, stream_delete(socket, :products, product)}
end

In this event we only receive the ID, we immediately check in the database if the product exists using the Catalog.get_product/1 function that we built in the previous lesson. We then delete the product. As we already have the product variable we ignore the second result of the delete function.

#The stream_delete/3 function

In previous lessons we had already seen how to create streams using stream/4 to render lists in an efficient way. Now we can see the stream_delete/3 function to delete an item from the stream.

Remembering that streams do not store any data in memory about its items the stream_detele/3 function receives the name of the stream which is :products as we defined in our mount/3 and the product. Using these two variables, it infers that the HTML ID of the element will be #products-123 and sends simple data to the client indicating that LiveView should delete this element from the HTML.

#LiveView Code

With all the pieces together your ProductLive.Index should be close to this code:

defmodule SuperStoreWeb.ProductLive.Index do
  use SuperStoreWeb, :live_view
  alias SuperStore.Catalog

  def mount(_params, _session, socket) do
    socket = stream(socket, :products, Catalog.list_products())
    {:ok, socket}
  end

  def handle_event("delete", %{"id" => id}, socket) do
    product = Catalog.get_product!(id)
    {:ok, _} = Catalog.delete_product(product)

    {:noreply, stream_delete(socket, :products, product)}
  end

  def render(assigns) do
    ~H"""
    <.header>
      Listing Products
      <:actions>
        <.link patch={~p"/products/new"}>
          <.button>New Product</.button>
        </.link>
      </:actions>
    </.header>

    <.table
      id="products"
      rows={@streams.products}
      row_click={fn {_id, product} -> JS.navigate(~p"/products/#{product}") end}
    >
      <:col :let={{_id, product}} label="Name"><%= product.name %></:col>
      <:col :let={{_id, product}} label="Description"><%= product.description %></:col>
      <:action :let={{id, product}}>
        <.link
          phx-click={JS.push("delete", value: %{id: product.id}) |> hide("##{id}")}
          data-confirm="Are you sure?"
        >
          Delete
        </.link>
      </:action>
    </.table>
    """
  end
end

#Final code

Ready! Now you just need to test your LiveView and verify that the current flow is working.

If you had any issues you can see the final code for this lesson using git checkout deleting-data-done or cloning it in another folder using git clone https://github.com/adopt-liveview/first-crud.git --branch deleting-data-done.

#Recap!

  • The function Repo.delete/2 receives a struct from an Ecto schema and deletes it from the database.
  • The <:action> slot is useful for adding action buttons to your tables.
  • The IDs that come from the special attribute :let in component slots <.table> are called DOM ID or HTML ID and follow the format name-of-your-stream-ID (where ID is the ID in the database element data).
  • The DOM ID is useful for applying JS commands.
  • The CoreComponents of Phoenix projects comes with a hide/1 function that is just JS.hide/2 but with a beautiful transition.
  • We can use data-confirm to confirm with the user before triggering an action like a phx-click.
  • The stream_delete/3 function is a way to delete elements from a stream. This function optimizes sending the minimum amount of data to LiveView so it follows the idea that streams are an efficient way to manage lists in LiveView.

Feedback

Got any feedback about this page? Let us know!