CRUD
Editando um produto
Tempo de leitura: 5 minutos
Para finalizarmos o CRUD iremos criar um formulário de edição de produto. Vamos ver como este pode ser extremamente parecido com o formulário de criação de produto.
Esta aula é uma continuação direta da aula anterior
git clone https://github.com/adopt-liveview/first-crud.git --branch deleting-data-done.
#De volta ao Context
Vamos voltar ao nosso lib/super_store/catalog.ex e adicionar uma nova função:
defmodule SuperStore.Catalog do
# ...
def update_product(%Product{} = product, attrs) do
product
|> Product.changeset(attrs)
|> Repo.update()
end
end
Diferente de create_product/1 que apenas recebe os atributos, para atualizarmos um produto precisamos do dado original para poder aplicar as alterações. Nossa função Catalog.update_product/2 recebe o struct original e as modificações, aplica o changeset e, usando a função Repo.update/2 retorna {:ok, %Product{}} ou {:error, %Ecto.Changeset{}}.
#Testando no iex
Usando o Elixir Interativo podemos pegar o último produto com product = SuperStore.Catalog.list_products() |> List.last e o atualizá-lo usando SuperStore.Catalog.update_product(product, %{name: "Edited"}):
$ 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.0ms idle=823.0ms
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: 7,
name: "asda",
description: "asd"
}
iex(2)> SuperStore.Catalog.update_product(product, %{name: "Edited"})
[debug] QUERY OK source="products" db=0.7ms idle=539.3ms
UPDATE "products" SET "name" = ? WHERE "id" = ? ["Edited", 7]
↳ :elixir.eval_external_handler/3, at: src/elixir.erl:405
{:ok,
%SuperStore.Catalog.Product{
__meta__: #Ecto.Schema.Metadata<:loaded, "products">,
id: 7,
name: "Edited",
description: "asd"
}}
iex(3)>
Note que nos segundo argumento passamos apenas o nome. Nosso changeset requer uma description obrigatoriamente porém, como o produto original já possui uma descrição, essa validação passa.
#Construindo nossa LiveView
Vamos escrever o código da LiveView passo-a-passo de modo que possamos ver as semelhanças com a ProductLive.Create. Na pasta lib/super_store_web/live/product_live/ crie um arquivo edit.ex.
#Começando
defmodule SuperStoreWeb.ProductLive.Edit do
use SuperStoreWeb, :live_view
alias SuperStore.Catalog
alias SuperStore.Catalog.Product
end
O primeiro passo é criar o módulo e use SuperStoreWeb, :live_view. Em seguida, adicionamos dois alias úteis para o que vem a seguir.
def mount(%{"id" => id}, _session, socket) do
product = Catalog.get_product!(id)
form =
Product.changeset(product)
|> to_form()
{:ok, assign(socket, form: form, product: product)}
end
#O mount/3
Em nossa função mount/3 nós recebemos como parâmetro o id do produto. Logo mais iremos definir isso no router como live "/products/:id/edit", ProductLive.Edit, :edit portanto podemos garantir que haverá este id.
O próximo passo é usar product = Catalog.get_product!(id) para recuperar o produto pelo id. Vale lembrar que se não houver um produto com este id um erro 404 será automaticamente gerado como vimos em aulas anteriores.
Definimos nosso form como um changeset que recebe o produto original. No formulário de criação nós usamos Product.changeset(%Product{}), ou seja, o produto vazio pois naquele momento não existe um produto. Como estamos trabalhando com edição, todos os nossos changesets irão receber o produto sendo editado.
Note também que no assign passamos o product. Iremos usar esse assign não só no nosso HEEx como também em outros eventos.
#O evento de validação
def handle_event("validate_product", %{"product" => product_params}, socket) do
form =
Product.changeset(socket.assigns.product, product_params)
|> Map.put(:action, :validate)
|> to_form()
{:noreply, assign(socket, form: form)}
end
O evento de validação é uma cópia cuspida do formulário de criação exceto que o Product.changeset/2 recebe no primeiro argumento, ao invés de %Product{} (o produto vazio), o socket.assigns.product que contém o valor do produto sendo editado.
#O evento de salvar
def handle_event("save_product", %{"product" => product_params}, socket) do
socket =
case Catalog.update_product(socket.assigns.product, product_params) do
{:ok, %Product{} = product} ->
put_flash(socket, :info, "Product ID #{product.id} updated!")
{:error, %Ecto.Changeset{} = changeset} ->
form = to_form(changeset)
socket
|> assign(form: form)
|> put_flash(:error, "Invalid data!")
end
{:noreply, socket}
end
Mais uma vez o nosso evento é uma cópia cuspida do evento de criar produto. Renomeamos o evento para "save_product" para fazer sentido com o formulário e trocamos a função principal no case para Catalog.update_product/2 passando mais uma vez o socket.assign.product. Mofificamos também o put_flash/2 para uma mensagem que faz mais sentido.
#A render/1
def render(assigns) do
~H"""
<.header>
Editing Product <%= @product.id %>
<:subtitle>Use this form to edit product records in your database.</:subtitle>
</.header>
<div class="bg-grey-100">
<.form
for={@form}
phx-change="validate_product"
phx-submit="save_product"
class="flex flex-col max-w-96 mx-auto bg-gray-100 p-24"
>
<h1>Editing a product</h1>
<.input field={@form[:name]} placeholder="Name" />
<.input field={@form[:description]} placeholder="Description" />
<.button type="submit">Send</.button>
</.form>
</div>
<.back navigate={~p"/products/#{@product}"}>Back to product</.back>
"""
end
Nesta parte modificamos apenas os textos e o nome do evento do binding phx-submit. Não houve nenhuma modificação funcional exceto que o link do componente <.back> agora retorna pra página de visualizar o produto.
#Atualizando o router
Abra seu arquivo de router e adicione a rota live "/products/:id/edit", ProductLive.Edit, :edit. No momento seu router deve estar do seguinte modo:
# ...
scope "/", SuperStoreWeb do
pipe_through :browser
live "/", ProductLive.Index, :index
live "/products/new", ProductLive.New, :new
live "/products/:id", ProductLive.Show, :show
live "/products/:id/edit", ProductLive.Edit, :edit
end
# ...
#Adicionando um link pro formulário
Temos uma página, mas nossos usuários não conhecem ela. Abra sua ProductLive.Show e atualize apenas o componente <.header> para adicionar este <:actions>:
<.header>
Product <%= @product.id %>
<:subtitle>This is a product record from your database.</:subtitle>
<:actions>
<.link navigate={~p"/products/#{@product}/edit"}>
<.button>Edit event</.button>
</.link>
</:actions>
</.header>
#Código final
Finalizado! Nossa aplicação possui um CRUD completo. Ainda existem algumas coisas que podem melhorar e iremos ver isso em outra seção mas se você seguiu o curso até então você já tem conhecimento suficiente para se virar criando seu próximo CRUD!
Se você sentiu dificuldade de acompanhar o código nesta aula você pode ver o código pronto desta aula usando git checkout editing-data-done ou clonando em outra pasta usando git clone https://github.com/adopt-liveview/first-crud.git --branch editing-data-done.
#Resumindo!
-
Usando
Repo.update/2conseguimos atualizar um produto passando um changeset. - Uma LiveView de formulário de editar pode ser extremamente parecida com uma de criar um dado.
- Você já sabe fazer um CRUD completo em LiveView 😉.
Feedback
Você tem algum feedback sobre esta página? Conte-nos!