Home Posts Post Search Tag Search

LiveView 19 - Chapter: 7 Demographic Forms
Published on: 2026-01-04 Tags: elixir, Blog, State, LiveView, Html/CSS, Phoenix

Manage Component State

    First let's look at the lifecycle of the live component then we can build the events that will handle what we need.

    Consider the update_many/1 Callback
        update_many/1 is a way to load a list of things all at once. This is used many times when we need to pull a large list of similar data at the same time. Say you wanted to use a live component to render a list of 20 items. If we where to not have update_many/1 we would have to invoke 20 different live_components to render them all. 

    Handle The Save Event
        We need to build the handle_event/3 for the "save" event. We will need to save the form to the socket. Then we need to call the reducer that we built. pento/lib/pento_web/live/demographic_live/form.ex
                defp assigns_demographic(%{assigns: %{current_scope: current_scope}} = socket) do
                    assign(
                    socket,
                    :demographic,
                    %Demographic{
                        user_id: current_scope.user.id
                    }
                    )
                end
        This is just the start but it shows us that we can now handle the event and output what will be written. We will now need to add in the user_id to the params so that we can correctly add them to the database.
                defp params_with_user_id(params, socket) do
                    user_id = socket.assigns.current_scope.user.id

                    Map.put(params, "user_id", user_id)
                end

            # Now we can add that to the "save" event and see the updated output when we save.
                @impl true
                def handle_event("save", %{"demographic" => demographic_params}, socket) do
                    demographic_params = params_with_user_id(demographic_params, socket)
                    ...
                end
        Last but now least we can build the reducer that will take the demographic and add it into the database.
                defp save_demographic(socket, demographic_params) do
                    current_scope = socket.assigns.current_scope
                    case Survey.create_demographic(current_scope, demographic_params) do
                        {:ok, demographic} ->
                            send(self(), {:created_demographic, demographic})
                            socket

                        {:error, %Ecto.Changeset{} = changeset} ->
                            assign_form(socket, changeset)
                    end
                end
        This will grab the the current_scope and use that to add in a new entry to the database. Let's add that into the "save" event.
                @impl true
                def handle_event("save", %{"demographic" => demographic_params}, socket) do
                    params = params_with_user_id(demographic_params, socket)
                    socket = save_demographic(socket, params)
                    {:noreply, socket}
                end
    Send a Message to the Parent
        So we now have a way to add in a new entry to the demographic part of the survey. But remember that is not the only thing that we are trying to do here. We need to now show a different survey to the user if it succeeds. That is where the send and handle_info/2 comes into play.

        We have already sent the info to the parent with the message {:created_demographic, demographic}, we can head to the survey_live.ex and have an event that will listen for that message.
                @impl true
                def handle_info({:created_demographic, demographic}, socket) do
                    socket = handle_demographic_created(socket, demographic)
                    {:noreply, socket}
                end
        This will take care of the listening for the event and we now just have to create what we want to happen when we add a new entry to the db.
                defp handle_demographic_created(socket, demographic) do
                    socket
                    |> put_flash(:info, "Demographic created successfully")
                    |> assign(:demographic, demographic)
                end
        That is it we now have the right logic to deal with the created demographic. If you are running an older version of Phoenix you might need to add in the wrapper for the flash group. I've been wrapping everything in the Layouts.app flash={@flash current_scope={@current_scope}}> but you might need to add
            <Layouts.flash_group flash={@flash}/>

Build the Ratings Components
    Now we can start to work on the Ratings part of the page. We will do much the same thing we did before, using the survey_live.ex to orchestrate the events while we use a live component to render what we want on the page. 

    We want to show the demographic once it is completed and we want to let the user rate thing only after a demographic has been completed. We will also need to have a rating index to show the ratings after they have been completed, we will also need to have form for a single rating that can be used for all the products that are unrated. 

List Ratings
    This will be used to display a rating if it exists or a form if it doesn't, it will need to iterate over the entire product catalog for the user. This will be done by 2 stateless functions, "rating show" and "rating form"

    SurveyLive will continue to orchestrate the events and will be responsible for the overall state. 

    Build the Ratings Index Component
        Let's start off by creating a new file. This will be the component that we use for the index
                # pento/lib/pento_web/live/rating_live/index.ex
                defmodule PentoWeb.RatingLive.Index do
                    use Phoenix.Component
                    alias PentoWeb.RatingLive
                end
        The entry to the products will be product_list/1 it will have a set of attr that will be needed in order to properly render the list. 
                attr :products, :list, required: true
                attr :current_scope, :map, required: true

                def product_list(assigns) do
                    ~H"""
                    <.heading products={@products} current_scope={@current_scope} />
                    <div class="divide-y">
                        <.product_rating
                            :for={{product, index} <- Enum.with_index(@products)}
                            product={product}
                            index={index}
                            current_scope={@current_scope} />
                    </div>
                    """
                end

            # This requires an other component that we will make now heading
                attr :products, :list, required: true
                attr :current_scope, :map, required: true

                def heading(assigns) do
                    ~H"""
                    <h2 class="flex justify-between">
                        Ratings
                        <%= if ratings_complete?(@products, @current_scope) do %>
                            ✅
                        <% end %>
                    </h2>
                    """
                end
        Now we need to deal with the ratings_complete?/2 which will see if there is any completed ratings
                def ratings_complete?(products, current_scope) do
                    Enum.all?(products, fn product ->
                        Enum.any?(product.ratings, &(&1.user_id == current_scope.user.id))
                    end)
                end

            # Now the product_rating/1
                def product_rating(assigns) do
                    ~H"""
                        <div><%= @product.name %></div>
                        <%= if rating = List.first(@product.ratings) do %>
                            <RatingLive.Show.stars rating={rating} />
                        <% else %>
                            <div>
                                <h3><%= @product.name %> rating form coming soon!</h3>
                            </div>
                        <% end %>
                    """
                end
        This all will help us to take everything that we need and make sure that it is compartmentalized. Once we have all f this we can populate a list of all the products and then start to show ratings as we do them. There will be a lot that goes into the events once we are done but we should be able to make this work as we go.