CRUD
Deletando um produto
Tempo de leitura: 6 minutos
Vamos pular para a última letra do CRUD: o Delete. Nesta aula vamos ver como é simples gerar uma UX de deletar um elemento usando recursos do próprio projeto.
Esta aula é uma continuação direta da aula anterior
git clone https://github.com/adopt-liveview/first-crud.git --branch show-data-done
.
#Você já imagivana que começaríamos com o Context
O primeiro passo é voltarmos para nosso lib/super_store/catalog.ex
e adicionarmos uma nova função:
defmodule SuperStore.Catalog do
# ...
def delete_product(%Product{} = product) do
Repo.delete(product)
end
end
A função delete_product/1
recebe um struct do tipo %Product{}
e apenas aplica o método Repo.delete/2
nele. O resultado será {:ok, %Product{}}
caso seja necessário usar o produto novamente.
#Testando no iex
Usando o confiável modo de Elixir Interativo podemos pegar o último produto com product = SuperStore.Catalog.list_products() |> List.last
e o deletar usando 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"
}}
#Deletando produtos na lista
Ao invés de criarmos uma nova LiveView chamada ProductLive.Delete
podemos reusar a lista de produtos para isso. Abra sua ProductLive.Index
localizada em lib/super_store_web/live/product_live/index.ex
.
#O slot <:action>
do componente <.table>
Dentro da sua render/1
atualize sua <.table>
para o seguinte código:
<.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>
Adicionamos um slot chamado <:action>
onde recebemos tanto o id
quanto o product
usando o atributo especial :let
. Este slot fica como última coluna para adicionarmos botões de ação na nossa linha.
#O ID do :let
Este id
em específico é conhecido como "HTML ID" e, neste caso, deve ser algo como "products-123" pois nossa tabela tem ID "products" e supondo que o ID no banco de dados do elemento é 123. Ele é útil para aplicarmos JS.commands.
#Confirmando ações com data-confirm
O próximo ponto e foco é o data-confirm
. Não queremos que o item seja deletado imediatamente sem qualquer tipo de confirmação, certo? O Phoenix verifica que, caso você clique um elemento com data-confirm
ele dispara um confirm
do seu navegador e apenas aplica o phx-click
se o usuário confirmar.
#O comando JS.hide/2
Dentro do nosso binding phx-click
duas coisas acontecem:
- Enviamos um evento a nossa LiveView chamado "delete" (ainda precisamos definir ele).
- Escondemos o elemento da linha atual usando o HTMLl ID.
Como você pode notar não estamos usando diretamente JS.hide/2
e sim apenas a função hide/1
. Isso acontece porque o Phoenix já trás dentro do CoreComponents
esta função simplificada que já aplica transições usando classes CSS! Dentro de seu 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
Sempre que puder prefira usar hide/1
do CoreComponents
porém se você precisar customizar a transição opte por JS.hide/2
.
#Criando o evento de deletar
Para podermos testar este código precisamos criar nosso handle_event/3
. Em sua LiveView, abaixo do mount/3
adicione o 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
Neste evento recebemos apenas o ID, imediatamente verificamos no banco se o produto existe usando a função Catalog.get_product/1
que construímos na aula anterior. Em seguida, deletamos o produto. Como já temos a variável product
nós ignoramos o segundo resultado da função de deletar.
#A função stream_delete/3
Em aulas anteriores já haviamos visto como criar streams usando stream/4
para renderizar listas de uma maneira eficiente. Agora vemos a função stream_delete/3
para deletar um item da stream.
Lembrando que streams não armazenam nenhum dado em memória sobre os items, a função stream_detele/3
recebe o nome da stream que é :products
como definimos no nosso mount/3
e o product
. Usando essas duas variáveis ela infere que o HTML ID do elemento será #products-123
e envia um dado simples indicando que a LiveView deve deletar este elemento do HTML. Lembrando que o elemento já foi escondido usando o nosso hide/1
anteriormente.
#Código da LiveView
Com todas as peças unidas sua ProductLive.Index
deve estar próximo deste código:
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
#Código final
Pronto! Agora basta você testar sua LiveView e ver como o fluxo atual está funcionando.
Se você sentiu dificuldade de acompanhar o código nesta aula você pode ver o código pronto desta aula usando git checkout deleting-data-done
ou clonando em outra pasta usando git clone https://github.com/adopt-liveview/first-crud.git --branch deleting-data-done
.
#Resumindo!
-
A função
Repo.delete/2
recebe um struct de um schema Ecto e o deleta do banco de dados. -
O slot
<:action>
é útil para adicionar botões de ação nas suas tabelas. -
Os IDs que vem do atributo especial
:let
em slots do componente<.table>
se chamam HTML ID e seguem o formatonome-da-sua-stream-ID
(onde ID é o ID no banco de dados do elemento). - O HTML ID é útil para aplicar JS commands.
-
O
CoreComponents
de projetos Phoenix vem com uma funçãohide/1
que apenas é aJS.hide/2
porém com uma transição bonita. -
Podemos usar
data-confirm
para confirmar com o usuário antes de disparar uma ação como umphx-click
. -
A função
stream_delete/3
é uma forma de deletar elementos de uma stream. Esta função otimiza enviar o mínimo de dados para a LiveView portanto segue a ideia de que streams são uma maneira eficiente de gerenciar listas em LiveView.
Feedback
Você tem algum feedback sobre esta página? Conte-nos!