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 =
      |> Ecto.Changeset.put_change(:time, time})

    {:noreply, assign_form(socket, changeset)}
  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
    |> Enum.map(fn hour ->
      label = "#{hour}:00"
      value = Time.new!(hour, 0, 0)

      [key: label, value: value]

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.


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 =
      |> Map.put("time", time)
      |> Routine.changeset()
      |> Map.put(:action, :validate)
      |> to_form()

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

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


  form =
      |> 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!