What is the best way to update a form from outside the form in LiveView?

Sometimes I need to update a form from outside the form in LiveView.

  • For UI, some inputs are need to be created outside the form.
  • Updating form when file is uploaded.

I made an simple example.

The hour input is outside the form.
When hour input is changed, select_hour event is triggered.

page: Home · Json Media
code: json_corp/form_live.ex at main · nallwhy/json_corp · GitHub

To answer your question, I’ve handled this in two different ways in my application:

  1. do a Changeset.put_change(changeset, :time, time) in my event handler, so the data gets persisted into my form state. I have a hidden input like you do to ensure the value persists when other changes happen to the form.
  @impl true
  def handle_event("select_hour", %{"hour" => hour_str}, socket) do
    time = Time.new!(hour_str |> String.to_integer(), 0, 0)

    changeset =
      socket.assigns.form.source
      |> Ecto.Changeset.put_change(:time, time})

    {:noreply, assign_form(socket, changeset)}
  end
  1. if I’m in a live component that I want to be more reusable (not requiring the parent to implement a handle_event), I call a assign(socket, :value, hour), and use a small hook I wrote that will trigger the form change event so Phoenix’s standard form handling will detect it and populate it.

But as your current example is, I think both of these are overkill. I would instead improve the select’s data so it already has the times as values. Then there’s no necessary conversion step and you can put the time select in the same form as the name.

  defp time_options() do
    0..23
    |> Enum.map(fn hour ->
      label = "#{hour}:00"
      value = Time.new!(hour, 0, 0)

      [key: label, value: value]
    end)
  end

This returns this data which you can use in your select:

[
  [key: "0:00", value: ~T[00:00:00]],
  [key: "1:00", value: ~T[01:00:00]],
  ...
  [key: "23:00", value: ~T[23:00:00]]
]

Note: I haven’t tested this- I’ve render %Time{} into input and it works fine. Assuming it also works as a value for select.

6 Likes

I had a little trouble because I was expecting the form to behave like a live view component instead of a liveview . The default form that phoenix creates is helpful bc it shows how to call out to the relevant context to make changes without having a nested module essentially doing the same thing.
disclaimer: I’m pretty new so I’m more book smart than experienced. :slight_smile:

You can give that form some id let’s say filters-form.
And then you can submit the form from some other element outside the form by dispatching submit event.

on_confirm={JS.dispatch("submit",    to: "#filters-form" ) }

Thank you!

I solved it with params of Changeset following your first suggestion, because put_change/3 doesn’t validate the changes.

  @impl true
  def handle_event("select_hour", %{"hour" => hour_str}, socket) do
    time = Time.new!(hour_str |> String.to_integer(), 0, 0)

    form =
      socket.assigns.form.source.params
      |> Map.put("time", time)
      |> Routine.changeset()
      |> Map.put(:action, :validate)
      |> to_form()

    {:noreply, assign(socket, :form, form)}
  end
2 Likes

in a similar approach, I have a helper function that takes params, and returns updated params.
Because, I have 3 external fields. (uploads, timers etc) outside the form

becomes

  form =
      socket.assigns.form.source.params
      |> add_timer(timer)
      |> add_uploads()
      |> Routine.changeset()
      |> Map.put(:action, :validate)
      |> to_form()

defp timer(params, timer), do: Map.put(params,"timer", timer)
defp add_uploads(), do: .... 

I like your proposed solution.

1 Like

I’ve made a library that helps this kind of works!

Hi everyone, I chanced upon a similar issue and I wish to check if my implementation makes sense.

For some context, I have a single modal that shows up whenever I want to add a customer or edit the details of a customer. In order to update the customer details, I need to input the customer object into my Customers query. This means that the customer’s id information is stored in a form struct. The PROBLEM is that everytime I edit my form, it changes into a new form struct (without the id information).

My approach is to just simply have 2 handle events to deal with the 2 separate situations.

def handle_event("validate-new-customer", %{"customer" => params}, socket) do
    form =
      %Customer{}
      |> Customers.change_customer(params)
      |> Map.put(:action, :validate)
      |> to_form()

    {:noreply, assign(socket, :customer_form, form)}
  end

  def handle_event("validate-edit-customer", %{"customer" => params}, socket) do
    params = Map.put_new(params, "id", socket.assigns.customer_form.data.id)

    form =
      %Customer{}
      |> Customers.change_customer(params)
      |> Map.put(:action, :validate)
      |> to_form()

    {:noreply, assign(socket, :customer_form, form)}
  end

On top of this, I need to add :id to my cast() for my Customer struct. I also dont know if this affects anything (sorry a newbie here).

Please advice!

Ok this is a bad approach (though I made used of the knowledge above).

Found a much better approach for my case:

def handle_event("validate-customer", %{"customer" => params}, socket) do
    form =
      socket.assigns.customer_form.data
      |> Customers.change_customer(params)
      |> Map.put(:action, :validate)
      |> to_form()

    {:noreply, assign(socket, :customer_form, form)}
  end

I simply need to put the customer data into the form. :man_facepalming: :man_facepalming: :man_facepalming: Sorry for disturbing

What about set the id to the form with hidden input?

hmmm is hidden input added using <.inputs_for> ? Do you have a good guide for this? When are the recommended times to use it?

For my current case, just passing the %Customer{} is easier :slight_smile:

By default .inputs_for will add hidden inputs (id, not sure what else?). I’ve disabled this in one place of my app, because I wanted precise control over the location of these inputs so they wouldn’t interfere with drag-and-drop functionality. You can disable them with skip_hidden. Eg:

<.inputs_for :let={nested_form} field={@form[:nested]} skip_hidden>

Every place I use this helper, it’s for managing associations with a single form (eg you save User and it also saves Address and Job).

For the life of me I can’t find the docs of inputs_for right now. Googling directs me to Phoenix.HTML.Form, but I can’t find any mention of it there.

@dfalling can you please share your second solution of reusable hook

and use a small hook I wrote that will trigger the form change event so Phoenix’s standard form handling will detect it and populate it.

Any help will be much appreciated, thanks.

Sure, here it is:

// Hook that will dispatch change event when it's updated. Useful for inputs that are
// updated by a component so they won't fire a change event.
Hooks.PublishInput = {
  updated() {
    this.el.dispatchEvent(new Event("input", { bubbles: true }));
  },
};

Note: this triggers the input event, which by itself won’t get back to LiveView. It has to be inside a managed form which will then trigger whatever change handling you have.

1 Like