We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
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.