We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
LiveView 21 - Chapter 7: Your Turn
Published on: 2026-01-08
Tags:
elixir, Blog, LiveView, Ecto, Html/CSS, Phoenix
Your Turn
We were able to take some bits of data and html and compartmentalize them into bits that we could add together. It might have felt like we were creating a lot of code and files, but in the end we now have places to go if we want to add anything to a page.
Give It a Try
• Save a rating on phx-change rather than phx-submit. What are the pros and cons to this approach?
Ill not work on this specifically but will show the code here as I want to keep the state that I have. Doing this will make it so that a user will not be able to fix an error before it goes to the database.
phx-change="change"
...
@impl true
def handle_event("change", %{"rating" => rating_params}, socket) do
save_rating(socket, rating_params)
end
This will allow a user to submit the change any time that you change the value, I would also get rid of the button for submit and then remove the phx-submit="save"
• Show validation errors when the user selects no rating.
This is already within the code for the validations. If no rating is selected for the product and you try and submit it will show an error.
• Live components are often tied to backend database services, our DemographicLive.Form is backed by the Survey context, which wraps interactions with the Demographic schema. Add a field to the Demographic schema and corresponding database table to track the education level of a user, allowing them to choose from “high school”, “bachelor’s degree”, “graduate
degree”, “other”, or “prefer not to say”. Then, update your LiveView code to support this field in the demographic form.
For this we will need to do things in a few stages I'll try to post it all here. First its the linux command line
mix ecto.gen.migration add_education_to_demo
Once that is done we can head to the file that was created and then type in these lines.
defmodule Pento.Repo.Migrations.AddEducationToDemo do
use Ecto.Migration
def change do
alter table(:demographics) do
add(:education, :string)
end
end
end
Once that is done you will want to migrate to make it available to the db.
mix ecto.migrate
Now you can head to the schema for that table and then add in the new column.
defmodule Pento.Survey.Demographic do
use Ecto.Schema
import Ecto.Changeset
schema "demographics" do
field(:gender, :string)
field(:year_of_birth, :integer)
field(:education, :string)
belongs_to(:user, Pento.Accounts.User)
timestamps(type: :utc_datetime)
end
@doc false
def changeset(demographic, attrs, user_scope) do
demographic
|> cast(attrs, [:gender, :year_of_birth, :education])
|> normalize_string()
|> validate_required([:gender, :year_of_birth, :education])
|> validate_inclusion(:gender, ["male", "female", "other", "prefer not to say"])
|> validate_inclusion(:year_of_birth, 1900..2025)
|> validate_inclusion(:education, [
"high school",
"bachelor's degree",
"graduate degree",
"prefer not to say",
"other"
])
|> unique_constraint(:user_id)
|> put_change(:user_id, user_scope.user.id)
end
defp normalize_string(changeset) do
changeset
|> update_change(:gender, &String.downcase(&1))
|> update_change(:education, &String.downcase(&1))
end
end
Now that we have that we need to add in the right fields for the form.
<.input
field={@form[:education_level]}
type="select"
label="Education Level"
options={[
"high shool",
"bachelor's degree",
"graduate degree",
"prefer not to say",
"other"
]}
/>
Last but not least we need to add in the show so that a user can see their education level.
<:col :let={demographic} label="Education Level">
{demographic.education}
</:col>
The only thing left is to add a way to update the education level of the users that don't have it already. This will involve adding in a form for just the education level, that will show if you don't have and education level set in the db. First let's create the file for the education_form.ex (pento/lib/pento_web/live/demographic_live/education_form.ex)
defmodule PentoWeb.DemographicLive.EducationForm do
use PentoWeb, :live_component
alias Pento.Survey
alias Pento.Survey.Demographic
@impl true
def render(assigns) do
~H"""
<div>
<.form
for={@form}
phx-submit="save"
id={@id}
phx-target={@myself}
>
<.input
field={@form[:education_level]}
type="select"
label="Education Level"
options={[
"high shool",
"bachelor's degree",
"graduate degree",
"prefer not to say",
"other"
]}
/>
<div>
<.button phx-disable-with="Saving...">Save</.button>
</div>
</.form>
</div>
"""
end
@impl true
def handle_event("save", %{"demographic" => demographic_params}, socket) do
params = params_with_user_id(demographic_params, socket)
socket = update_demographic(socket, params)
{:noreply, socket}
end
@impl true
def update(assigns, socket) do
socket =
socket
|> assign(assigns)
|> assigns_demographic()
|> clear_form()
{:ok, socket}
end
def clear_form(socket) do
scope = socket.assigns.current_scope
demographic = socket.assigns.demographic || %Demographic{}
changeset = Survey.change_demographic(scope, demographic)
assign(socket, :form, to_form(changeset))
end
defp update_demographic(socket, params) do
scope = socket.assigns.current_scope
case Survey.update_demographic(scope, params) do
{:ok, demographic} ->
send(self(), {:created_demographic, demographic})
socket
{:error, %Ecto.Changeset{} = changeset} ->
assign(socket, :form, changeset)
end
end
defp params_with_user_id(params, socket) do
user_id = socket.assigns.current_scope.user.id
Map.put(params, "user_id", user_id)
end
defp assigns_demographic(%{assigns: %{current_scope: current_scope}} = socket) do
assign(
socket,
:demographic,
Survey.get_demographic_by_user(current_scope)
)
end
end
We now need to add in the function to update a demographic
@doc """
Creates or updates a demographic for the given user scope.
If a demographic does not exist for the user, it is created.
If a demographic already exists for the user, it is updated.
Examples
iex> update_demographic(scope, %{field: new_value})
{:ok, %Demographic{}}
iex> update_demographic(scope, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_demographic(%Scope{} = scope, attrs) do
case get_demographic_by_user(scope) do
nil ->
# no demographic exists, create one
create_demographic(scope, attrs)
demographic ->
# demographic exists, update it
demographic
|> Demographic.changeset(attrs, scope)
|> Repo.update()
|> case do
{:ok, demographic} ->
broadcast_demographic(scope, {:updated, demographic})
{:ok, demographic}
error ->
error
end
end
end
Now we need to add in some logic to the show so that we can show the form if we don't have a education. Otherwise it will be the normal form for a user that is brand new or the show for a user that has a completed demo.
defmodule PentoWeb.DemographicLive.Show do
use Phoenix.Component
alias PentoWeb.CoreComponents
alias PentoWeb.DemographicLive.EducationForm
attr(:demographic, :map, required: true)
attr(:current_scope, :map, required: true)
def details(assigns) do
~H"""
<h2>Demographics ✅</h2>
<CoreComponents.table id="demographics" rows={[@demographic]}>
<:col :let={demographic} label="Gender">
{demographic.gender}
</:col>
<:col :let={demographic} label="Year of Birth">
{demographic.year_of_birth}
</:col>
<:col :let={demographic} label="Education Level">
{demographic.education}
</:col>
</CoreComponents.table>
<%= if @demographic.education == nil do %>
<.live_component
module={EducationForm}
id="education_form"
current_scope={@current_scope}
/>
<% end %>
"""
end
end
That was everything that needed to be done for this.