Home Posts Post Search Tag Search

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.