CRUD
Listing products
Read time: 7 minutes
In the previous lesson we created some products! Let's create a simple page to list saved products.
This lesson is a direct continuation of the previous one.
git clone https://github.com/adopt-liveview/first-crud.git --branch saving-data-done
.
#Back to our Context
Remember that all operations related to modifying our product domain will be concentrated in the Catalog
context. At this moment we need a function to list all products. Open lib/super_store/catalog.ex
and add the list_products/0
method:
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
To list rows from our database, we use the function Repo.all/2
which takes an Ecto query and returns all rows. Our Product
module itself is considered a query and, in this case, represents select * from products
.
#Testing in iex
.
Open your iex -s mix
and 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"
},
...
]
As you can see, our function works. We can proceed to apply it in a new LiveView.
#Creating a new LiveView
To list our products, we will create a new LiveView called SuperStoreWeb.ProductLive.Index
. Phoenix projects like to follow this pattern: YourAppWeb.NameOfSomethingLive.{Index, Show, New, or Edit}
. Create the folder lib/super_store_web/live/product_live
and inside it, create a file named index.ex
with the following 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 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
#Remember streams?
In the lesson on rendering lists we discussed streams as an optimized way to render items in HEEx. In that lesson there was a bit more complexity in the code because we needed to add an id
to each element. But in this case, since we're working with a database, all elements have an id
, so we can define a stream of products without any hassle.
#Using the <.table>
component
Phoenix projects contain a very powerful component called <.table>
in their CoreComponents
. Throughout the CRUD lessons we'll learn more about it.
<.table id="products" rows={@streams.products}> ... </.table>
At the moment all you need to understand is that this component works very well with streams. We pass two assigns to the component: an unique id
and rows
receives the stream of products
.
<:col :let={{_id, product}} label="Name"><%= product.name %></:col>
Inside the component, you can see that we use the <:col>
slot twice. Each of these slots requires a label
attribute to define the column name in the table and receives the special attribute :let
for you to access {id, element}
. At the moment, we can ignore the id
and receive the product
to render the content of that column for each product. If all of this seems very weird to you, you can take a look at our lesson on rendering lists with slots in the component section.
#Updating our old LiveView
Currently, in the lib/super_store_web/live
folder, we have the file page_live.ex
. With this name, the purpose of this LiveView isn't obvious. Move this file to lib/super_store_web/live/product_live/new.ex
and rename the module to SuperStoreWeb.ProductLive.New
. Now, not only we know its purpose from the folder structure but also the module name also follows the Phoenix convention!
#Updating our router.ex
Open your router at lib/super_store_web/router.ex
and modify your routes within the main scope:
scope "/", SuperStoreWeb do
pipe_through :browser
live "/", ProductLive.Index, :index
live "/products/new", ProductLive.New, :new
end
Note that we also changed the live action from ProductLive.New
to make it clear that it's a LiveView that creates something.
Success! Open your browser and you'll see that at the homepage all your products are listed. But wait, how do we go to the new product page? Your user won't guess the route!
#Connecting the pages using links
Go to your ProductLive.Index
and modify its render/1
a bit:
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
We used the <.header>
component, which also comes from the CoreComponents
, not only to give a title to our product list page but also to use its <:action>
slot to add a link to our product page.
Similarly, modify the render/1
of your ProductLive.New
to:
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
In addition to the <.header>
at the top, we added the <.back>
component at the bottom, referencing the home page of our application. In case you're curious, this component isn't magical at all; it's just a <.link>
with a back icon (a left arrow).
#Final code
Now your application is not only more organized in terms of folders but the user will also have a good first navigation experience.
If you found it challenging to follow the code in this lesson, you can see the completed code using git checkout listing-data-done
or by cloning it into another folder using git clone https://github.com/adopt-liveview/first-crud.git --branch listing-data-done
.
#Recap!
-
Using
Repo.all/2
we can list the result of an Ecto query. -
The
Product
module can be considered an Ecto query in the formatselect * from products
. -
Phoenix projects like to follow this pattern:
YourAppWeb.NameOfSomethingLive.{Index, Show, New, or Edit}
. -
To keep your LiveView folders more organized in your project, we use the format
lib/your_app_web/live/your_model/{index.ex, new.ex, edit.ex, show.ex}
, as we'll see in future lessons. -
When using databases it's very easy to use streams in LiveView because the elements already come with an
id
. -
The
<.table>
component is very powerful in simplifying tables with items, as we'll see in the future. -
In your
router.ex
, prefer Live Actions between:new
,:index
,:edit
, and:show
, as we'll see in the next lessons. -
The
<.header>
component is very useful for titling your pages and can also contain an<:actions>
slot to simplify adding action buttons, as we used to add the create product button.
Feedback
Got any feedback about this page? Let us know!