What's the correct way to interact with a form's underlying data? (value)

I want to pass my Schedule field to a display component when it’s present in my form. I already have to have handle for the two states: 1) it’s a Schedule 2) it’s a Changeset for a schedule. But I’m now hitting a third scenario. If I modify another field in my form, Instead of receiving a Schedule or Changeset, I receive a plain map with string fields.

Here’s the part of my heex that accesses the form value:

        <%= if @form[:schedule].value do %>
              <.label>Schedule</.label>
              <IdoWeb.ScheduleComponents.display schedule={@form[:schedule].value} />
            ...
        <% end %>

In my assign_form function, I’m logging the Ecto.Changeset.get_field(:schedule) and to_form(changeset)[:schedule].value.

Before any changes:

Ecto.Changeset.get_field(changeset, :schedule) #=> %Ido.Calendar.Schedule{
  __meta__: #Ecto.Schema.Metadata<:loaded, "schedules">,
  id: "27fcf9b2-a484-4aec-8195-e4c327e03bda",
  all_day: false,
  end_date: ~D[2023-05-20],
  end_time: ~T[14:00:00],
  end_tz: "Etc/GMT",
  start_date: ~D[2023-05-11],
  start_time: ~T[10:00:00],
  start_tz: "Etc/GMT",
  inserted_at: ~N[2023-05-13 06:12:34],
  updated_at: ~N[2023-05-13 06:13:31]
}

IdoWeb.ElementLive.FormComponent.assign_form/2]
form[:schedule].value #=> %Ido.Calendar.Schedule{
  __meta__: #Ecto.Schema.Metadata<:loaded, "schedules">,
  id: "27fcf9b2-a484-4aec-8195-e4c327e03bda",
  all_day: false,
  end_date: ~D[2023-05-20],
  end_time: ~T[14:00:00],
  end_tz: "Etc/GMT",
  start_date: ~D[2023-05-11],
  start_time: ~T[10:00:00],
  start_tz: "Etc/GMT",
  inserted_at: ~N[2023-05-13 06:12:34],
  updated_at: ~N[2023-05-13 06:13:31]
}

After an unrelated change:

Ecto.Changeset.get_field(changeset, :schedule) #=> %Ido.Calendar.Schedule{
  __meta__: #Ecto.Schema.Metadata<:loaded, "schedules">,
  id: "27fcf9b2-a484-4aec-8195-e4c327e03bda",
  all_day: false,
  end_date: ~D[2023-05-20],
  end_time: ~T[14:00:00],
  end_tz: "Etc/GMT",
  start_date: ~D[2023-05-11],
  start_time: ~T[10:00:00],
  start_tz: "Etc/GMT",
  inserted_at: ~N[2023-05-13 06:12:34],
  updated_at: ~N[2023-05-13 06:13:31]
}

IdoWeb.ElementLive.FormComponent.assign_form/2]
form[:schedule].value #=> %{
  "all_day" => "false",
  "end_date" => "2023-05-20",
  "end_time" => "14:00:00",
  "end_tz" => "Etc/GMT",
  "id" => "27fcf9b2-a484-4aec-8195-e4c327e03bda",
  "start_date" => "2023-05-11",
  "start_time" => "10:00:00",
  "start_tz" => "Etc/GMT"
}

Note how above, after an unrelated change, the data from to_form is primitive string values rather than the proper date fields. But the data was exactly the same prior to the to_form, so I’m not sure why it’s different.

What am I missing? How should I be accessing this field so I don’t have a third structure to deal with? Better yet, is there a way to just get the raw current data without having to deal with Schedule || Ecto.Changeset || …?

Thanks!

Ecto.Changeset.get_field should be that, though you can be more explicit with ecto 3.10 and Ecto.Changeset.get_assoc(cs, :schedule, :struct), the implementation is the same.

The reason form[:schedule].value returns string values/keys at some point is because the form needs to maintain input state for the user even in the case where the changeset cannot deal with the input for some reason or another. Say the input pushes text, when the changeset expects an integer. The form input shouldn’t be cleared just because the input doesn’t resolve to a valid new value.

1 Like

Oh ok, that makes a lot of sense- something has to store those potentially invalid interim states.

So inside a heex form where I want to access underlying data, should I be using the Changeset? I thought the point was to work with the new form structure, eg with form[:field].value. To use the Changeset I guess I’d do Ecto.Changeset.get_field(form.source, :schedule). In this specific scenario I will always have valid data- it’s being handled in a separate modal, so I either have nothing, or completely valid data proper Dates and Times. That’s part of what’s thrown me off- I don’t see why this valid data is being degraded to the primitives.

I went with the get_field solution. Still feels cludgey to me, but definitely a nice solution to limit the possibilities to either the schema or a Changeset.

Solution:

<IdoWeb.ScheduleComponents.display schedule={
  Ecto.Changeset.get_field(@form.source, :schedule)
} />

Thanks!