New `to_form/2` in LiveView 1.18.2

I was wondering if there are any discussions around the new to_form/2 in LiveView 1.18.2. I’ve RTFM’d but there is just some dissonance in the change as it’s not mentioned in the changelog and it looks like we just need :let in nested inputs now? I am in no way complaining here, it’s def a step forward, it just caught me off guard and I’d love to know where I can follow along. Is the mailing list is best?


EDIT: I searched for to_form on the mailing list and found nothing.

1 Like

There have been a handful of pretty big new things popping up in LiveView without much fanfare, but if I were to guess, we’ll be hearing a lot more about these with the full Phoenix 1.7 release. Looking at the git history, to_form/2 landed just a few weeks ago. I’m just speculating, but given the sheer number of improvements present between Phoenix 1.7 and LiveView 0.18.*, I’d bet that the Phoenix team decided that it made the most sense to get things locked down and then release a mega-post/coordinated set of posts covering it all instead of drip-feeding for months.


Ya, that makes sense. It only hit me as I just started a new project and got hit with warnings from the generated CoreComponents. Obviously this is totally fine since I’m using a release candidate, but was just wondering if there was any public discussion around it I might be missing. If the answer is “no” that’s totally cool by me.

I don’t think you’ve missed anything.

Honestly, given these most recent developments, I wonder if it might make sense to follow Phoenix 1.7 very closely with a Phoenix 2.0 that primarily serves as an “excuse” to merge LiveView/Phoenix.Component into Phoenix core. I’ve seen a number of topics/questions pop up that boil down to “I don’t know where to look for documentation,” and that would solve a lot of those problems.

Ya, I was thinking a lot about the documentation and what you’re describing. The quality is extremely good but searching hexdocs can be quite frustrating. I often can’t remember if something is in the guides or in the module docs and then, when it comes to Phoenix, what package it is in. I always use google or ddg to search hexdocs because there is no way to search across packages—at least no evident way, it’s very possible I’m missing something obvious. But overall the search is not great. For example, if you search “liveview” on the homepage it gives you five results, none of which are LiveView itself (I’m aware there are instructions below on how to jump to packages via the URL but that is not my point). Basically, when something changes or is added or removed, it’s not always evident where to look.


Thanks for pointing me to to_form/2. Were you able to figure out its rationale? It seems to me that its purpose is to provide an API to more easily extend an existing form with additional options, or overriding errors, name, id etc.

If I want to convert existing data to a form, we already have the Phoenix.HTML.FormData protocol for that, which is of course also called internally by to_form/2.

The reason for the changes are meant to aid in LVs change tracking. Before the mentioned changes any update to the form would essentially rerender the whole form, that’s afaik meant to be gone/working better now.


you may want to give a try. it does have cross-package search for titles (functions, guides, types, etc), but not contents (so i still google for schema options for example).

with this i don’t personally feel the problem of having packages separated. in fact i prefer it since then it’s super clear who’s responsible for what.

There is more information on the docs for form/1: Phoenix.Component — Phoenix LiveView v0.18.13

TL;DR - better change tracking, less confusion by not keeping changesets in memory, as mentioned by @LostKobrakai

1 Like

Re: nested inputs, the addition of <Phoenix.Component.inputs_for> is one of the most recent changes. It replaces inputs_for() from Phoenix.HTML and it includes hidden inputs automatically (thanks @LostKobrakai :smiley:).

Here is a naive but complete example of rendering nested inputs:

# my_app_web/live/nested_live.ex
defmodule MyAppWeb.NestedLive do
  use MyAppWeb, :live_view

  defmodule Guestlist do
    use Ecto.Schema
    import Ecto.Changeset

    embedded_schema do
      field :event_name, :string

      embeds_many :guests, Guest do
        field :name, :string

    def changeset(form, params \\ %{}) do
      |> cast(params, [:event_name])
      |> validate_required([:event_name])
      |> cast_embed(:guests, with: &guest_changeset/2)

    def guest_changeset(guestlist, params) do
      |> cast(params, [:name])
      |> validate_required([:name])

  @impl Phoenix.LiveView
  def mount(_params, _session, socket) do
    guestlist = %Guestlist{
      event_name: "My New Event",
      guests: [
        %Guestlist.Guest{name: ""},
        %Guestlist.Guest{name: ""},
        %Guestlist.Guest{name: ""}

    changeset = guestlist |> Guestlist.changeset()

     |> assign(:guestlist, guestlist)
     |> assign_form(changeset)}

  @impl Phoenix.LiveView
  def handle_event("change", %{"guestlist" => params}, socket) do
    changeset =
      |> Guestlist.changeset(params)
      |> Map.put(:action, :validate)

    {:noreply, assign_form(socket, changeset)}

  def handle_event("submit", %{"guestlist" => params}, socket) do
    case fake_save_guestlist(socket.assigns.guestlist, params) do
      {:ok, _guestlist} ->
         |> put_flash(:info, "Guestlist saved!")
         |> push_navigate(to: "/")}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign_form(socket, changeset)}

  defp assign_form(socket, %Ecto.Changeset{} = changeset) do
    assign(socket, :form, to_form(changeset))

  defp fake_save_guestlist(%Guestlist{} = guestlist, params) do
    |> Guestlist.changeset(params)
    |> Ecto.Changeset.apply_action(:save)

  @impl Phoenix.LiveView
  def render(assigns) do
    <.form for={@form} id="nested-form" phx-change="change" phx-submit="submit">
      <.input type="text" field={@form[:event_name]} label="Title" />
      <fieldset class="my-4">
        <legend class="py-2">Guests</legend>
        <.inputs_for :let={f_nested} field={@form[:guests]}>
          <.input type="text" field={f_nested[:name]} label={"Name #{f_nested.index + 1}"} />
      <.button type="submit">Submit</.button>

Note on LiveView v0.18.13 the <.form> component warns that for is a required attribute but it is not. Anyone should feel free to PR that change :slight_smile:


Thanks for the clarifications! Much appreciated.

1 Like

Is “not keeping the changeset in memory” an explicit goal here? If so I wonder if it’s expected to modify the form struct over the changeset e.g. when adding/removing rows for nested associations.

What’s the reason for requiring atom keys in @form[:field]? Can we change it to accept strings? Right now it will raise, though the first thing that the fetch/2 implementation does is convert the atom to a string:

Is this due to the Keyword access for getting error values?

  # lib/phoenix_html/form.ex
  def fetch(%Form{errors: errors} = form, field) when is_atom(field) do
    field_as_string = Atom.to_string(field)

       errors: Keyword.get_values(errors, field),
       field: field,
       form: form,
       id: input_id(form, field_as_string),
       name: input_name(form, field_as_string),
       value: input_value(form, field)

  def get(%Form{}, field) do
    raise ArgumentError,
          "accessing a form with form[field] requires the field to be atom, got: #{inspect(field)}"

There’s an open issue to support string keys as well.

@josevalim I read the docs which is what I meant by “I RTFM’d”, lol, it just didn’t stand out to me that change tracking was a big motivation. Thanks to you and @LostKobrakai for the clarification!

@mcrumm I actually have a quite a bit of free time right now. I’ve only ever contributed minor doc fixes and clarifications before, but would be happy to take “good first issue” type stuff. Do you assign issues or does one just grab something and hope they are not duplicating efforts?

If there is an open issue that you are interesting in taking, just leave a comment on the issue that you would like to handle it! :slight_smile:

If you want to fix the <.form> for attr thing, I haven’t made an issue for it, but we need to drop required here:

I did find those and wasn’t sure if that was it but ya, I think I can manage that, lol.

Dodgy helper functions are now useless in favour of f[:fieldname].value :smiley:

def get_field(%Phoenix.HTML.Form{source: %Ecto.Changeset{} = changeset}, field) do
  Ecto.Changeset.get_field(changeset, field)
1 Like

I’ve discovered get_assoc/3 with the struct option is very useful for this also:

get_assoc(form.source, :lines, :struct) will get the struct that the current changeset (in the form.source) would result in. This way, there’s no need to worry about what data was already there and what is new changes.


Just be careful and not use the returned data for modifications / assign it back using put_assoc. For that one should always use changesets to not have changes silently ignored.

1 Like