Form Component
Live Component
Tempo de leitura: 10 minutos
Na aula anterior aprendemos como reutilizar código HEEx usando componentes. Todavia, até então nunca vimos nenhum caso em que conseguimos reutilizar código de callbacks em uma LiveView. Para isto iremos aprender uma nova parte importante do LiveView: Live Components.
Esta aula é uma continuação direta da aula anterior
git clone https://github.com/adopt-liveview/refactoring.git --branch form-component-done
.
#O que é um Live Component?
Até então conversamos apenas sobre componentes funcionais. Eles nos possibilitaram facilitar muito nosso código por prevenir que repetíssemos HTML e facilitou a manutenção de nosso código no futuro. Sua limitação porém é que estes não tem qualquer relacionamento com lógica de negócio.
Já Live Components trazem não só as vantagens de componentes funcionais como eles também podem gerenciar seu próprio estado local. Pense em Live Components como se fossem LiveViews que podem ficar dentro de outras LiveViews.
#Convertendo nosso código atual para Live Component
Vamos começar por converter o formulário de novo produto para Live Component. Abra seu ProductLive.FormComponent
e edite ele para:
defmodule SuperStoreWeb.ProductLive.FormComponent do
use SuperStoreWeb, :live_component
def render(assigns) do
~H"""
<div class="bg-grey-100">
<.form
for={@form}
phx-change="validate"
phx-submit="save"
class="flex flex-col max-w-96 mx-auto bg-gray-100 p-24"
>
<%= render_slot(@inner_block) %>
<.input field={@form[:name]} placeholder="Name" />
<.input field={@form[:description]} placeholder="Description" />
<.button type="submit">Send</.button>
</.form>
</div>
"""
end
end
Não houveram grandes mudanças aqui além de termos removidos os attr
e slot
, removido o assign @rest
e o principal: mudamos no topo de use SuperStoreWeb, :html
para use SuperStoreWeb, :live_component
. Com isso já podemos aplicar o Live Component.
#Usando um Live Component
Vá até seu ProductLive.New
e edite seu HEEx do formulário para:
~H"""
...
<.live_component
module={FormComponent}
id="new-form"
>
<h1>Creating a product</h1>
</.live_component>
"""
Para usar um Live Component devemos usar o component <.live_component>
passando no mínimo o module
e id
como parâmetros. No momento ele não faz nada. Vamos voltar para o ProductLive.FormComponent
.
#Inicializando o estado do FormComponent
No momento seu formulário de criação deve mostrar uma exceção. Isso acontece pois não inicializamos o @form
. Vamos começar por aprender o callback de inicialização novo para Live Components: update/2
:
defmodule SuperStoreWeb.ProductLive.FormComponent do
use SuperStoreWeb, :live_component
alias SuperStore.Catalog
alias SuperStore.Catalog.Product
def update(assigns, socket) do
form =
Product.changeset(%Product{})
|> to_form()
{:ok,
socket
|> assign(form: form)
|> assign(assigns)}
end
# ...
end
O callback update/2
de Live Components se parece muito com o mount/3
de uma LiveView. Ele recebe os assigns
passados no <.live_component>
e o socket
. Assim como no mount/3
nós criamos o form
e fazemos assign dele.
A principal diferença aqui está no fato de que nós pegamos os assigns
recebidos no callback e fazemos assign(assigns)
para que todos eles estejam disponíveis dentro do componente também. Ou seja, se você usar <.live_component module{FormComponent} x={10} y={20}>
, dentro do seu Live Component @x
e @y
estarão disponíveis.
#Adicionando os eventos
# ...
def handle_event("validate", %{"product" => product_params}, socket) do
form =
%Product{}
|> Product.changeset(product_params)
|> Map.put(:action, :validate)
|> to_form()
{:noreply, assign(socket, form: form)}
end
def handle_event("save", %{"product" => product_params}, socket) do
case Catalog.create_product(product_params) do
{:ok, product} ->
{:noreply,
socket
|> put_flash(:info, "Product created successfully")
|> push_navigate(to: ~p"/")}
{:error, %Ecto.Changeset{} = changeset} ->
form = to_form(changeset)
{:noreply, assign(socket, form: form)}
end
end
def render(assigns) do
~H"""
<div class="bg-grey-100">
<.form
for={@form}
phx-target={@myself}
phx-change="validate"
phx-submit="save"
class="flex flex-col max-w-96 mx-auto bg-gray-100 p-24"
>
<%= render_slot(@inner_block) %>
<.input field={@form[:name]} placeholder="Name" />
<.input field={@form[:description]} placeholder="Description" />
<.button type="submit">Send</.button>
</.form>
</div>
"""
end
Como você pode notar a lógica toda de criação é uma cópia da original. Vale mencionar que adicionamos um |> push_navigate(to: ~p"/products/")
para que quando o produto for criado o usuário seja enviado para a lista de produtos.
#phx-target
Em nossa render/1
atualizamos nosso <.form>
para adicionar os bindings de form porém um novo binding aparece: o phx-target
. Para entender esse binding preciso revelar a você uma nova informação sobre Live Components: eles vivem num processo isolado.
Sabendo que um Live Component vive num processo seu você precisa deixar explícito que os eventos de formulário são tratados por ele e não pela Live View que o contém. Usando phx-target={@myself}
o <.form>
saberá para onde enviar eventos.
#Generalizando o componente
No momento o Live Component só sabe criar novos produtos. Veremos agora como generalizar ele para aceitar edição.
#Onde mudar o código?
Anotando as áreas que precisam mudar um pouco para que edição funcione:
-
O
update/2
deve saber quando inicializar o form com produto vazio ou um produto existente. -
O
handle_event("validate", ...)
deve saber quando inicializar o form com produto vazio ou um produto existente. -
O
handle_event("save", ...)
deve saber se deve usarCatalog.create_product/1
ouCatalog.update_product/2
.
Eis a sugestão: os tópicos 1 e 2 giram em torno de "saber o produto". Para novo, usaremos um produto vazio e para editar usamos o produto existente. Isso pode ser resolvido com um assign como <.live_component module{FormComponent} product={...}>
.
Já o terceiro tópico depende de saber se é edição ou criação. Podemos resolver isso com um assign também como <.live_component module{FormComponent} action={:new / :edit}>
. Além disso, podemos usar o assign automático @live_action
que vem do router. Se a página for :edit
, o @live_action
será :edit
. Isso simplifica as coisas!
#Atualizando nossas LiveViews
Na sua ProductLive.New
atualize o HEEx para:
~H"""
...
<.live_component module={FormComponent} id="new-form" product={%Product{}} action={@live_action}>
<h1>Creating a product</h1>
</.live_component>
...
"""
Na sua ProductLive.Edit
atualize o HEEx para:
~H"""
...
<.live_component module={FormComponent} id={@product.id} product={@product} action={@live_action}>
<h1>Editing a product</h1>
</.live_component>
...
"""
#Melhorando o update/2
Voltamos ao FormComponent
. Como sabemos que o Live Component sempre receberá um product
como assign podemos fazer:
def update(%{product: product} = assigns, socket) do
form =
Product.changeset(product)
|> to_form()
{:ok,
socket
|> assign(form: form)
|> assign(assigns)}
end
Além disso, como o product
faz parte dos assigns
, no futuro poderemos usar socket.assigns.product
.
#Melhorando o handle_event("validate", ...)
def handle_event("validate", %{"product" => product_params}, socket) do
form =
socket.assigns.product
|> Product.changeset(product_params)
|> Map.put(:action, :validate)
|> to_form()
{:noreply, assign(socket, form: form)}
end
Ao invés de usar diretamente %Product{}
a única coisa que mudou aqui é que construímos o form
usando socket.assigns.form
que vem do nosso <.live_component ... product={...}>
.
#Melhorando o handle_event("save", ...)
Neste momento iremos usar o socket.assigns.action
para descobrir qual ação tomar:
def handle_event("save", %{"product" => product_params}, socket) do
case socket.assigns.action do
:new ->
case Catalog.create_product(product_params) do
{:ok, product} ->
{:noreply,
socket
|> put_flash(:info, "Product created successfully")
|> push_navigate(to: ~p"/")}
{:error, %Ecto.Changeset{} = changeset} ->
form = to_form(changeset)
{:noreply, assign(socket, form: form)}
end
:edit ->
case Catalog.update_product(socket.assigns.product, product_params) do
{:ok, product} ->
{:noreply,
socket
|> put_flash(:info, "Product updated successfully")
|> push_navigate(to: ~p"/products/#{product.id}/edit")}
{:error, %Ecto.Changeset{} = changeset} ->
form = to_form(changeset)
{:noreply, assign(socket, form: form)}
end
end
end
Como você pode notar a única coisa nova aqui é o case
mais externo que checa o valor de socket.assigns.action
. Porém nossa função ficou bem grande e com case
s aninhados. Podemos melhorar isso criando uma outra função!
def handle_event("save", %{"product" => product_params}, socket) do
save_product(socket, socket.assigns.action, product_params)
end
defp save_product(socket, :edit, product_params) do
case Catalog.update_product(socket.assigns.product, product_params) do
{:ok, product} ->
{:noreply,
socket
|> put_flash(:info, "Product updated successfully")
|> push_navigate(to: ~p"/products/#{product.id}/edit")}
{:error, %Ecto.Changeset{} = changeset} ->
form = to_form(changeset)
{:noreply, assign(socket, form: form)}
end
end
defp save_product(socket, :new, product_params) do
case Catalog.create_product(product_params) do
{:ok, product} ->
{:noreply,
socket
|> put_flash(:info, "Product created successfully")
|> push_navigate(to: ~p"/")}
{:error, %Ecto.Changeset{} = changeset} ->
form = to_form(changeset)
{:noreply, assign(socket, form: form)}
end
end
Agora nosso evento de "save"
simplesmente repassa valores para uma função privada nova chamada save_product/3
. Esta função utiliza pattern matching para verificar o segundo argumento se é :edit
ou :new
e aplica as funções necessárias.
#Revisando o código final
Vamos dar uma olhada em cada parte do código que mexemos nesta aula para ver o produto final
#ProductLive.FormComponent
defmodule SuperStoreWeb.ProductLive.FormComponent do
use SuperStoreWeb, :live_component
alias SuperStore.Catalog
alias SuperStore.Catalog.Product
def update(%{product: product} = assigns, socket) do
form =
Product.changeset(product)
|> to_form()
{:ok,
socket
|> assign(form: form)
|> assign(assigns)}
end
def handle_event("validate", %{"product" => product_params}, socket) do
form =
socket.assigns.product
|> Product.changeset(product_params)
|> Map.put(:action, :validate)
|> to_form()
{:noreply, assign(socket, form: form)}
end
def handle_event("save", %{"product" => product_params}, socket) do
save_product(socket, socket.assigns.action, product_params)
end
defp save_product(socket, :edit, product_params) do
case Catalog.update_product(socket.assigns.product, product_params) do
{:ok, product} ->
{:noreply,
socket
|> put_flash(:info, "Product updated successfully")
|> push_navigate(to: ~p"/products/#{product.id}/edit")}
{:error, %Ecto.Changeset{} = changeset} ->
form = to_form(changeset)
{:noreply, assign(socket, form: form)}
end
end
defp save_product(socket, :new, product_params) do
case Catalog.create_product(product_params) do
{:ok, product} ->
{:noreply,
socket
|> put_flash(:info, "Product created successfully")
|> push_navigate(to: ~p"/")}
{:error, %Ecto.Changeset{} = changeset} ->
form = to_form(changeset)
{:noreply, assign(socket, form: form)}
end
end
def render(assigns) do
~H"""
<div class="bg-grey-100">
<.form
for={@form}
phx-target={@myself}
phx-change="validate"
phx-submit="save"
class="flex flex-col max-w-96 mx-auto bg-gray-100 p-24"
>
<%= render_slot(@inner_block) %>
<.input field={@form[:name]} placeholder="Name" />
<.input field={@form[:description]} placeholder="Description" />
<.button type="submit">Send</.button>
</.form>
</div>
"""
end
end
#ProductLive.New
defmodule SuperStoreWeb.ProductLive.New do
use SuperStoreWeb, :live_view
import SuperStoreWeb.CoreComponents
alias SuperStore.Catalog.Product
alias SuperStoreWeb.ProductLive.FormComponent
def render(assigns) do
~H"""
<.header>
New Product
<:subtitle>Use this form to create product records in your database.</:subtitle>
</.header>
<.live_component module={FormComponent} id="new-form" product={%Product{}} action={@live_action}>
<h1>Creating a product</h1>
</.live_component>
<.back navigate={~p"/"}>Back to products</.back>
"""
end
end
#ProductLive.Edit
defmodule SuperStoreWeb.ProductLive.Edit do
use SuperStoreWeb, :live_view
alias SuperStore.Catalog
alias SuperStoreWeb.ProductLive.FormComponent
def mount(%{"id" => id}, _session, socket) do
product = Catalog.get_product!(id)
{:ok, assign(socket, product: product)}
end
def render(assigns) do
~H"""
<.header>
Editing Product <%= @product.id %>
<:subtitle>Use this form to edit product records in your database.</:subtitle>
</.header>
<.live_component module={FormComponent} id={@product.id} product={@product} action={@live_action}>
<h1>Editing a product</h1>
</.live_component>
<.back navigate={~p"/products/#{@product}"}>Back to product</.back>
"""
end
end
#Conclusão
Como podemos notar, as LiveView ficaram bem enxutas. Não há código de formulário repetido. Com esta aula aprendemos como reutilizar lógica de negócio em mais de uma LiveView usando Live Components!
Se você sentiu dificuldade de acompanhar o código nesta aula você pode ver o código pronto desta aula usando git checkout live-component-done
ou clonando em outra pasta usando git clone https://github.com/adopt-liveview/refactoring-crud.git --branch live-component-done
.
#Resumindo!
- Live Components são componentes capazes de gerenciar seu próprio estado. Eles também são uma excelente arma para evitar duplicação de código.
-
Para criar um Live Component você usa no topo do módulo
use SeuProjetoWeb, :live_component
. -
Para usar um Live Component você usa o componente
<.live_component module={} id="algum-id">
. -
Você pode usar o callback
update/2
de um Live Component para definir o estado inicial. -
Você pode usar
assign(socket, assigns)
dentro doupdate/2
para salvar no componente todos os assigns passados na chamada<.live_component x={10} y={20} z={30}>
. - Live Components vivem em processos separados da LiveView que o chamou.
-
Quando criar eventos em Live Componentes você pode usar o
phx-target={@myself}
para deixar claro que o evento será tratado por este componente e não a LiveView que o contém.
Feedback
Você tem algum feedback sobre esta página? Conte-nos!