Liveview form help to understand

i have the following liveview form

<div>
  <.form let={f} for={@changeset} phx-change="validate" phx-submit="save" phx-target={@myself}}>
    <%= label f, :username %>
    <%= text_input f, :username %>
    <%= error_tag f, :username %>
    <%= submit "Save" %>
  </.form>
</div>

the corresponding .ex file for this form is

defmodule PentoWeb.Components.SubmitForm do
  use PentoWeb, :live_component
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field(:username, :string)
  end

  def changeset(survey, attrs \\ %{}) do
    survey
    |> cast(attrs, [:username])
    |> validate_required([
      :username
    ])
  end

  def mount(socket) do
    user_details = %__MODULE__{
      username: "abcd"
    }
    change_set = changeset(user_details)

    {:ok,
     socket
     |> assign(:user_details, user_details)
     |> assign(:changeset, change_set)}
  end

  def update(assigns, socket) do
    IO.puts("update being called")
    changeset = changeset(socket.assigns.user_details)

    {:ok,
     socket
     |> assign(assigns)
     |> assign(:changeset, changeset)}
  end

  def handle_event("validate", params, socket) do
    changeset = changeset(socket.assigns.user_details, Map.get(params, "submit_form"))
    IO.inspect(changeset)

    {:noreply,
     socket
     |> assign(:changeset, changeset)}
  end
end

i am having trouble understanding how this form works, basically if someone can answer the following questions

  1. in the form markup what exactly does the following for={@changeset} do ? i know it looks at a changeset, but what is it used for ?
  2. what does the following achieve <%= text_input f, :username %>. i know it displays the username in the textbox but how does it get the value, if you look closely at the code i’m using user_details in the mount but i’m not specifying that this is the object the form should refer to, so how does it know which object to refer to ?

Basically with the for you are supplying the form with its data, that is then assigned to f using the let. It is that f you use in the input later.

yup, i got that, but in the for i’m just simply supplying the changeset and in the initial load this is blank, there is nothing in the changeset, so how does it know to display “abcd” in the field ? even if i remove the line |> assign(:user_details, user_details) it still displays abcd in the text box, how dies it get this value ?

You’re not using a blank changeset, you’re creating it here:

    user_details = %__MODULE__{
      username: "abcd"
    }
    change_set = changeset(user_details)

and it has a value for the username, put a IO.inspect(changeset.data) below this code and you’ll see the values.

actually it does not have a value, if i inspect it i see the following

#Ecto.Changeset<action: nil, changes: %{}, errors: [],
 data: #PentoWeb.Components.SubmitForm<>, valid?: true>

it does not have the text abcd but yet the form is showing it

See my edit, inspect changeset.data.

Also:
https://hexdocs.pm/ecto/Ecto.Changeset.html#module-the-ecto-changeset-struct

aha, got it, thanks for that, that clarifies quite a bit. also in the validate method if i submit a blank username i see the following changeset which is expected

#Ecto.Changeset<
  action: nil,
  changes: %{},
  errors: [username: {"can't be blank", [validation: :required]}],
  data: #PentoWeb.Components.SubmitForm<>,
  valid?: false
>

i want to show the error and in the form i have the following

    <%= text_input f, :username %>
    <%= error_tag f, :username %>

how do i display the error, its not showing up on the form (please excuse me if this a newbie question, i’m new to this whole eco system)

Read the section “A note on :errors” here (just scroll down a bit).

Combining it with the link that I sent before you’ll see that there’s a field :action in the changeset struct, that field needs to be set manually to show errors, here’s where you’ll have to put the action:


  def handle_event("validate", params, socket) do
    changeset = changeset(socket.assigns.user_details, Map.get(params, "submit_form")) |> Map.put(:action, :validate)
    IO.inspect(changeset)

    {:noreply,
     socket
     |> assign(:changeset, changeset)}
  end

You’ll usually see :validate in examples around the web, but that’s a convention and you can put any atom in there, you just need to say to the changeset “Hey, when we’re validating, set an action to yourself so the form knows that he has to show errors”.

thanks for that clarification (appreciate it), one last question considering the line :point_up: why do we need socket.assigns.user_details when checking the changeset and do we need to store this value in the assigns ?

A changement is meant to track changes before applying them to update the initial data, it is supposed to be ephemeral. So you create a new changeset each time by casting the values in your params to the initial structure. There is a nice blogpost about this, by @LostKobrakai, https://kobrakai.de/kolumne/one-to-many-liveview-form, scroll down to the section “Echo.Changeset in LiveView”.

still having a bit of trouble understanding that, so if i try

changeset(%__MODULE__{}, Map.get(params, "submit_form")) it still works, i have replaced socket.assigns.user_details with %__MODULE__{}

i thought it uses that first parameter to just infer the schema or something like that?

It does not. There’s a big chance you won’t be seeing any difference in the rendered form, because the form itself only uses a subset of a changeset’s features, but consider the following:

%MyApp.Schema{}
|> cast(%{}, [:required_field])
|> validate_required([:required_field])

The resulting changeset will have an error that :required_field is not set. Now add an existing record as the data:

%MyApp.Schema{required_field: "some text"}
|> cast(%{}, [:required_field])
|> validate_required([:required_field])

The resulting changeset here won’t have an error for :required_field, because it is already set, even though there are no changes to the field.

I’d strongly suggest learning about changesets outside the contexts of forms, because they’re a really powerful tool in a wide array of places with powering forms being only one of them.

1 Like

Hmm, it likely appears to be still working because all of the parameters in user_details – in this case just username– are used in the form which means they come back across in Map.get(params, "submit_form"). I suspect if you had a field such as login_count that never gets displayed, but is a required field through validate_required, then things might be different as @LostKobrakai mentions above.

thanks everyone for the explanation. this has helped me a lot in terms of understanding how this whole cycle works.

1 Like