We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
LiveView 20 - Chapter 7: Show a Rating
Published on: 2026-01-07
Tags:
elixir, Blog, LiveView, Ecto, Html/CSS, Phoenix, Framework
Show a Rating
We are closer than every let's keep going. We not want to build a component that will show the rating for a product if a user has made a rating.
Build the Rating Show Component
First let's build the file pento/lib/pento_web/live/rating_live/show.ex and key in this
defmodule PentoWeb.RatingLive.Show do
use Phoenix.Component
attr(:rating, :map, required: true)
def stars(assigns) do
filled = filled_stars(assigns.rating.stars)
unfilled = unfilled_stars(assigns.rating.stars)
all_stars = Enum.concat(filled, unfilled)
star_display = Enum.join(all_stars, " ")
assigns = assign(assigns, :star_display, star_display)
~H"""
<div>
{raw(@star_display)}
</div>
"""
end
def filled_stars(stars) do
List.duplicate(
"★",
stars
)
end
def unfilled_stars(stars) do
List.duplicate(
"☆",
5 - stars
)
end
end
This will help us to use the ratings that a user has given and show filled stars for the rating. You can see that there is a filled and unfilled rating that will be used and will populate a filled or unfilled star based off the rating a user gives.
Now we can take this and render it in our index with the .Show.stars we just need to make sure that we populate the attrs. We put that into our code without having it so now we can just alias the new component and add the line.
alias RatingLive.Show
def product_rating(assigns) do
~H"""
<div>{@product.name}</div>
<%= if rating = List.first(@product.ratings) do %>
<Show.stars rating={rating} />
<% else %>
<div>
<h3>{@product.name} rating form coming soon!</h3>
</div>
<% end %>
"""
end
Show the Rating Form
Now we get to create the rating form that will be shown to the user so long as we don't have a rating for a product.
Build the Rating Form
First let's build the file and then the component. pento/lib/pento_web/live/rating_live/form.ex you will key in this. Making sure that you add in the right alias and the use.
defmodule PentoWeb.RatingLive.Form do
use Phoenix.Component
use PentoWeb, :live_component
alias Pento.Survey
alias Pento.Survey.Rating
alias PentoWeb.RatingLive
def update(assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign_rating()
|> assign_form()}
end
def assign_rating(socket) do
rating = %Rating{user_id: socket.assigns.current_scope.user.id}
assign(socket, :rating, rating)
end
def assign_form(socket, changeset \\ nil) do
form =
case changeset do
nil ->
current_scope = socket.assigns.current_scope
rating = socket.assigns.rating
to_form(Survey.change_rating(current_scope, rating))
changeset ->
to_form(changeset)
end
assign(socket, :form, form)
end
end
Prepare to Render
With all this done we can now render the form. We will need to use the form.ex to render it as we don't use the html.heex in the latest version. So head to the form.ex and key in this.
@impl true
def render(assigns) do
~H"""
<div>
<.form
for={@form}
phx-submit="save"
phx-target={@myself}
id={"rating-form-#{@id}"}
>
<div class="flex items-center gap-2">
<.input
field={@form[:stars]}
type="select"
prompt={RatingLive.Show.unfilled_stars(0) |> Enum.join()}
options={[
"
★★★★★": 5,
"
★★★★": 4,
"
★★★": 3,
"
★★": 2,
"
★": 1
]}
/>
<.button type="submit">Save</.button>
</div>
<input
type="hidden"
name={@form[:user_id].name}
value={@form[:user_id].value}
/>
<input
type="hidden"
name={@form[:product_id].name}
value={@form[:product_id].value}
/>
</.form>
</div>
"""
end
With that built we will need to add it to the index so that a user can see it.
defmodule PentoWeb.RatingLive.Index do
use Phoenix.Component
alias PentoWeb.RatingLive
alias RatingLive.Show
attr(:product, :map, required: true)
attr(:index, :integer, required: true)
attr(:current_scope, :map, required: true)
def product_rating(assigns) do
~H"""
<div class="py-0">
<div>{@product.name}</div>
<%= if rating = List.first(@product.ratings) do %>
<RatingLive.Show.stars rating={rating} />
<% else %>
<.live_component
module={RatingLive.Form}
id={"rating-form-#{@product.id}"}
product={@product}
index={@index}
current_scope={@current_scope}
/>
<% end %>
</div>
"""
end
...
end
I also needed to make some changes to older code as it didn't pull some information that we needed.
# pento/lib/pento_web/live/survey_live.ex
defp assign_products(socket) do
user = socket.assigns.current_scope.user
products = Catalog.list_products_with_user_rating(user)
assign(socket, :products, products)
end
With that we are all set to show the rating if we have it and then show the form if we don't now we need to be sure that we have the events handled.
Handle Component Events
Let's head back to the form.ex and add in the events.
def handle_event("save", %{"rating" => rating_params}, socket) do
save_rating(socket, rating_params)
end
def save_rating(socket, rating_params) do
case Survey.create_rating(
socket.assigns.current_scope,
rating_params
|> Map.put("product_id", socket.assigns.product.id)
) do
{:ok, rating} ->
product = %{socket.assigns.product | ratings: [rating]}
send(self(), {:created_rating, product, socket.assigns.index})
{:noreply, socket}
{:error, changeset} ->
{:noreply, assign_form(socket, changeset)}
end
end
Now that we have that taken care of we will be able to add a rating to the db, but the survey_live will not have the updated information so we need to deal with the event that we will create when we add in the rating, :created_rating. head to survey_live.ex
@imp true
def handle_info({:created_rating, product, product_index}, socket) do
socket = handle_rating_created(socket, product, product_index)
{:noreply, socket}
end
defp handle_rating_created(socket, product, product_index) do
current_products = socket.assigns.products
products = List.replace_at(current_products, product_index, product)
socket
|> put_flash(:info, "Rating created successfully")
|> assign(:products, products)
end
You did it mate that should be everything.