CRUD
Listando produtos
Tempo de leitura: 7 minutos
Na aula anterior nós criamos vários produtos! Vamos criar uma página simples de listar produtos salvos.
Esta aula é uma continuação direta da aula anterior
git clone https://github.com/adopt-liveview/first-crud.git --branch saving-data-done
.
#De volta ao nosso Context
Lembre-se que todas as operações que dizem respeito a modificar o nosso domínio de produtos serão concentradas no context Catalog
. Neste momentos precisamos de uma função para listar todos os produtos. Abra o lib/super_store/catalog.ex
e adicione o método list_products/0
:
defmodule SuperStore.Catalog do
alias SuperStore.Repo
alias SuperStore.Catalog.Product
def list_products() do
Product
|> Repo.all()
end
def create_product(attrs) do
Product.changeset(%Product{}, attrs)
|> Repo.insert()
end
end
Para listar linhas do nosso banco de dados usamos a função Repo.all/2
que recebe uma query em formato Ecto e retorna todas as linhas. O nosso módulo Product
em si é considerado uma query e, neste caso, representa select * from products
.
#Testando no iex
.
Abra seu iex -s mix
e execute SuperStore.Catalog.list_products()
:
$ 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)> SuperStore.Catalog.list_products()
[debug] QUERY OK source="products" db=0.2ms queue=0.2ms idle=283.7ms
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: 1,
name: "Elixir in Action",
description: "A great book"
},
...
]
Como você pode ver nossa função funciona. Podemos ir aplicar ela em uma nova LiveView.
#Criando uma nova LiveView
Para listar nossos produtos iremos criar uma nova LiveView chamada SuperStoreWeb.ProductLive.Index
. Projetos Phoenix gostam de seguir esse padrão: SuaAppWeb.NomeDeAlgoLive.{Index ou Show ou New ou Edit}
. Crie a pasta lib/super_store_web/live/product_live
e dentro dela crie um arquivo index.ex
com o seguinte 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 render(assigns) do
~H"""
<.table id="products" rows={@streams.products}>
<:col :let={{_id, product}} label="Name"><%= product.name %></:col>
<:col :let={{_id, product}} label="Description"><%= product.description %></:col>
</.table>
"""
end
end
#Lembra de streams?
Na aula sobre renderização de listas nós falamos sobre streams como maneira otimizada de renderizar itens em HEEx. Naquela aula houve um pouco mais de complexidade no código pois precisávamos adicionar a cada elemento um id
mas neste caso como estamos trabalhando com um banco de dados todos os elementos possuem um id
portanto conseguimos definir uma stream de products com zero dor de cabeça.
#Usando o componente <.table>
Projetos Phoenix contém em seu CoreComponents
um componente muito poderoso chamado <.table>
. Ao decorrer das aulas de CRUD iremos aprender um pouquinha mais sobre ele.
<.table id="products" rows={@streams.products}> ... </.table>
No momento tudo que você precisa entender é que este componente sabe trabalhar muito bem com streams. Passamos ao componente dois assigns: id
único e rows
recebe a stream de products
.
<:col :let={{_id, product}} label="Name"><%= product.name %></:col>
Dentro do componente você pode ver que usamos o slot <:col>
duas vezes. Cada um desses slots necessita de um atributo label
para definir o nome da coluna na tabela e recebe o atributo especial :let
para você ter acesso ao {id, elemento}
. No momento podemos ignorar o id
e recebemos o product
para renderizar o conteúdo daquela coluna para cada produto. Se isso tudo parece muito estranho você pode dar uma olhada na nossa aula de renderização de listas com slots na seção de componentes.
#Atualizando nossa LiveView antiga
No momento, na pasta lib/super_store_web/live
nós temos o arquivo page_live.ex
. Com este nome não fica óbvio o propósito desta LiveView. Mova esta arquivo para lib/super_store_web/live/product_live/new.ex
e renomeie o módulo para SuperStoreWeb.ProductLive.New
. Agora não só sabemos o propósito dela por ver a organização de pastas como também o nome do módulo segue o padrão Phoenix!
#Atualizando nosso router.ex
Abra seu router em lib/super_store_web/router.ex
e o modifique suas rotas dentro do escopo principal:
scope "/", SuperStoreWeb do
pipe_through :browser
live "/", ProductLive.Index, :index
live "/products/new", ProductLive.New, :new
end
Note que também mudamos a live action do ProductLive.New
para deixar óbvio que é uma LiveView que cria algo.
###
Sucesso! Abra seu navegador e verá que na página inicial todos os seus produtos são listados. Mas peraí, como vamos para a página de criar produto? Seu usuário não vai adivinhar a rota!
#Conectando as páginas usando links
Vá até a sua ProductLive.Index
e modifique a sua render/1
um pouco:
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}>
<:col :let={{_id, product}} label="Name"><%= product.name %></:col>
<:col :let={{_id, product}} label="Description"><%= product.description %></:col>
</.table>
"""
end
Usamos o componente <.header>
que também vem dos CoreComponents
para não só dar um título à nossa página de listar produtos como também usamos seu slot <:action>
para adicionar um link para nossa página de produto.
Do mesmo modo modifique a render/1
da sua ProductLive.New
para:
def render(assigns) do
~H"""
<.header>
New Product
<:subtitle>Use this form to create product records in your database.</:subtitle>
</.header>
<div class="bg-grey-100">
<.form
for={@form}
phx-change="validate_product"
phx-submit="create_product"
class="flex flex-col max-w-96 mx-auto bg-gray-100 p-24"
>
<h1>Creating 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"/"}>Back to products</.back>
"""
end
Além do <.header>
no topo, adicionamos no final o componente <.back>
referenciando a página inicial da nossa aplicação. Caso você esteja curioso, este componente não tem nada mágico ele é apenas um <.link>
com um ícone de voltar (uma seta para a esquerda).
#Código final
Agora sua aplicação não apenas está mais organizada em termos de pastas como também o usuário vai ter uma boa primeira experiência de navegação.
Se você sentiu dificuldade de acompanhar o código nesta aula você pode ver o código pronto desta aula usando git checkout listing-data-done
ou clonando em outra pasta usando git clone https://github.com/adopt-liveview/first-crud.git --branch listing-data-done
.
#Resumindo!
-
Usando
Repo.all/2
listamos o resultado de uma query em Ecto. -
O módulo
Product
pode ser considerado uma query Ecto no formatoselect * from products
. -
Projetos Phoenix gostam de seguir esse padrão:
SuaAppWeb.NomeDeAlgoLive.{Index ou Show ou New ou Edit}
-
Para as pastas de seu projeto LiveView ficarem mais organizadas usamos o formato
lib/sua_app_web/live/seu_model/{index.ex ou new.ex ou edit.ex ou show.ex}
como veremos em aulas futuras. -
Quando usamos bancos de dados fica muito fácil usar streams em LiveView pois os elementos já vem com
id
. -
O componente
<.table>
é muito poderoso em simplificar tabelas com items como veremos no futuro. -
Em seu
router.ex
dê preferência a Live Actions entre:new
,:index
,:edit
e:show
como veremos nas próximas aulas. -
O componente
<.header>
é bem útil para dar título a suas páginas e também pode conter um slot<:actions>
para simplificar adicionar botões de ação como usamos para adicionar o botão de criar produto.
Feedback
Você tem algum feedback sobre esta página? Conte-nos!