Home Posts Post Search Tag Search

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.