How to set up a LiveView form that can be updated by child component

I’m having an issue setting up a live view, not sure what the correct way to accomplish this is.

I have a schema (Template) that has an embeds_many. I’d like to be able to use nested live components to add/update the nested Operations.

Schema:

  schema "templates" do
    field :name, :string
    field :notes, :string
    embeds_many :operations, Operation, on_replace: :delete
  end

Currently, this lives in a live FormComponent that is a customized version of the FormComponent that mix phx.gen.live will add.

To add or update an Operation, it has its own UI, which is currently in its own live component, and it pops up in a modal. The user follows some steps here to prepare their Operation, which then has a “complete” button, which sends the event to the FormComponent. This is "add_operation".

The problems I’ve had is that when adding an Operation, the other changes to the form get lost because we create a new changeset, applying the params from the "add_operation" event to the existing Template in socket.assigns. When updating the regular text inputs, I can lose the added Operations for the same reason. And for saving the Template, I don’t know how to save the nested Operations without storing them in socket.assigns and then manually adding them to the form params on the "save" event.

This doesn’t seem to be quite the “nested forms” example, since the form for each nested Operation is ephemeral.

I’ve also tried the “checkboxes” approach from Chris McCord’s recent keynote ( Keynote: The Road To LiveView 1.0 by Chris McCord | ElixirConf EU 2023 - YouTube), giving each of my fields a hidden input and then using the normal markup to display each Operation, but I must be missing something because this doesn’t seem to actually add any items. If there are already Operations, the save event does seem to work as expected.

Changeset:

  def changeset(template, attrs) do
    template
    |> cast(attrs, [:name, :notes])
    |> cast_embed(:operations,
      with: &Operation.changeset/2,
      sort_param: :operations_order,
      drop_param: :operations_delete
    )
    |> validate_required([:name])
  end

and FormComponent

      <.simple_form
        for={@form}
        id="template-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="save"
      >
        <.input field={@form[:name]} type="text" label="Name" />
        <.input field={@form[:notes]} type="textarea" label="Notes" />

        <.inputs_for :let={op_form} field={@form[:operations]}>
          <input type="hidden" name="list[operations_order][]" value={op_form.index} />
          <!--hidden inputs for Operation's fields-->
        </.inputs_for>

        <label class="...">
          <input type="checkbox" phx-click="open_modal" name="list[operations_order][]" class="hidden" /> Add Operation
        </label>

Has anyone here run into this kind of problem? I feel like there has got to be a sane way to use another live component to manage creation/editing of the items that are being embedded.

It would be nice to see your LiveComponent lifecycle and event handling callbacks for your FormComponent and Operation modal.

Also, have you come across this article by @LostKobrakai ?

In the inputs_for, I’m pretty sure Chris used

list[..]

because he had a schema of “lists”. So, looking at yours, you probably want

template[..]

Good catch! You are correct. I made this change, and now clicking the checkbox will add an empty Operation row to the form.

1 Like