Liveview with a form that includes several changesets

Hi :wave:

I am trying to create a live view that shows a form. This form will allow you to create an snapshot per each product you own. The snapshots saves a quantity and a reference to the product.

There is another field rate that I will remove from the snippets for simplicity:

# snapshot.ex
defmodule xxx.Snapshots.Snapshot do
  ...

  schema "snapshots" do
    embeds_many :items, Item do
      field(:quantity, :integer)

      belongs_to(:product, Product)

      timestamps()
    end
  end

  ...

  def new_item_changeset(snapshot, params \\ %{}) do
    snapshot
    |> cast(params, [:quantity, :product_id])
    |> validate_number(:quantity, greater_than_or_equal_to: 0)
  end
end

I have this view:

# dashboard_live.ex
defmodule xxxWeb.DashboardLive do
  ...

  @impl true
  def mount(_params, session, socket) do
    current_user = Accounts.get_user_by_session_token(session["user_token"])

    products = Products.get_by_user!(current_user)

    items =
      products
      |> Enum.map(fn product ->
        params = %{product_id: product.id}

        Snapshot.new_item_changeset(
          %Snapshot.Item{
            product: product # preload
          },
          params
        )
      end)

    form =
      Ecto.Changeset.put_embed(
        Ecto.Changeset.change(%Snapshot{}),
        :items,
        items
      )
      |> to_form

    {:ok,
     socket
     |> assign(:page_title, "Dashboard")
     |> assign(form: form |> IO.inspect())
     |> stream(:products, products)}
  end

  def render(%{live_action: :show} = assigns) do
    ~H"""
    <.form for={@form} phx-change="validate" phx-submit="save">
      <%= for snapshot <- @form.source.changes.items do %>
        <div><%= snapshot.data.product.description %></div>
        <%!-- <div><%= product.type %></div> --%>
        <.input type="number" field={snapshot[:quantity]} />
      <% end %>

      <%!-- <%= for item <- @form[:items] do %>
        <.input type="number" field={item[:quantity]} />
      <% end %>
      --%>
      <button>Add snapshot</button>
    </.form>
    """
  end
end

I have several questions here:

  1. I have added the embeds_many in the schema because it was the only way I found to create a form with several changesets. Is it right?
  2. Is belongs_to the best way to scheme this? A user has N products where N can be dynamic through time. The snapshot is a snapshot (sorry) of that moment in time adding the quantity of products they own.
  3. I am able to list the product description with <%= snapshot.data.product.description %> however I don’t know how to make the <.input /> work as it is a changeset inside a changeset and not a form.

Any other suggestion/help would be more than welcome. I am trying to do this to understand LiveViews so I want to be the closest to the “credo” as possible.

Thanks!

Use inputs_for on embeds.

https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#inputs_for/1

1 Like

Thanks @cmo!

I was able to make it work with the following change as your proposed:

    <.form for={@form} phx-change="validate" phx-submit="save">
      <.inputs_for :let={snapshot} field={@form[:items]}>
        <div><%= snapshot.data.product.description %></div>
        <.input type="number" field={snapshot[:quantity]} placeholder="0" />
      </.inputs_for>

      <button>Add snapshot</button>
    </.form>

Do you have any other suggestion for the other questions?

I will move this changeset creation to the schema file:

    form =
      Ecto.Changeset.put_embed(
        Ecto.Changeset.change(%Snapshot{}),
        :items,
        items
      )
      |> to_form

But I am still not sure if it is exactly how it should be done or if it could be somehow simplified.

Thanks again!

  1. the cheat sheets are your friend here

  2. You can create an embedded schema or go schemaless for the form. This is handy when the db and form are not 1:1. On load, go from db represenstations to form, and the reverse on save.

Thanks for the cheat sheet @cmo, really helpful!

I will keep in mind what you said about the schemas as well.

In case it helps this is what the form has:

%Phoenix.HTML.Form{
  source: #Ecto.Changeset<
    action: nil,
    changes: %{
      items: [
        #Ecto.Changeset<
          action: :insert,
          changes: %{product_id: 1},
          errors: [],
          data: #Cleanhen.Snapshots.Snapshot.Item<>,
          valid?: true
        >,
        #Ecto.Changeset<
          action: :insert,
          changes: %{product_id: 2, rate_id: 4},
          errors: [],
          data: #Cleanhen.Snapshots.Snapshot.Item<>,
          valid?: true
        >,
        ...

And what I need to preload is the product inside that #Cleanhen.Snapshots.Snapshot.Item<>.