Non-input component updating an input in a form

I’m just learning LiveView (0.19) and this took a bit too long to figure out, so I hope this will help someone shorten their search. If there is another way/better way to do this, I’m quite interested to know.

The objective is to use a belongs_to relation as a selection list (in a formatted custom component) for an id to an input for a form. I spent most of my time looking for a phx- event to send on phx-click to update the form input. I don’t fully understand the event model, so my attempts to craft an event that updated the form failed.

Eventually I found this: Form bindings — Phoenix LiveView v0.19.4

There’s a lot to read about how LiveView works, so this section took a while to find. But this works and explicitly looks like this:

Inside my form, I have the field

        <.input field={@form[:pin_id]} type="number" id="pin_id" label="Pin" />
        <.live_component
          module={PsampleWeb.PinLive.Select}
          id={:select}
        />

And my Select component looks like:

<div class="overflow-y-auto h-32 border-solid border-red-200 border-2 p-8">
<script>
function sendValue(elementId, value) {
  const element = document.getElementById(elementId);
  element.value = value;
  element.dispatchEvent(new Event('change'))
}
</script>

<.table
  id="pin-table"
  rows={@streams.pin_collection}
  on_click={fn {_id, pin} -> "sendValue('pin_id','#{pin.id}')" end}
>
  <:col :let={{_id, pin}} label="Alt"><%= pin.alt %></:col>
</.table>
</div>

For completeness, my Select.mount function:

  def mount(socket) do
    {:ok, stream(socket, :pin_collection, Conf.list_pin())}
  end

In core_components.ex, for .table added the attribute:

attr :on_click, :string, default: nil, doc: "per row onclick"

and later (in the default td of a row generated by phoenix), I added onclick:

              phx-click={@row_click && @row_click.(row)}
              onclick={@on_click && @on_click.(row)}

And this works. Clicking on a row in the table sends an event to the form input pin_id. However, it does not result in a socket event, so the live view doesn’t see the change until submit or another direct change to another input.

It seems like there might be a way to send a socket event that would have this same effect and that an event handler would be able to enhance the interaction (generically). This does seem like a very common kind of task (using a formatted display of a relation to control an input field), so an easier way to do this (that exists, that I didn’t find, that could be devised) seems of benefit.

Here’s a generic client side hook for controlling inputs that can be triggered via JS.dispatch, one of LiveView’s built in JS Commands:

let Hooks = {}
Hooks.ControlledInput = {
  mounted() {
    this.el.addEventListener("setValue", e => {
      this.el.value = e.detail
      this.el.dispatchEvent(new Event("input", {bubbles: true}))
    })
  }
}
let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, ...})

<.input field={@form[:pin_id]} type="number" id="pin_id" label="Pin" phx-hook="ControlledInput" />

<.table
  id="pin-table"
  rows={@streams.pin_collection}
  row_click={fn {_id, pin} -> JS.dispatch("setValue", to: "#pin_id", detail: pin.id) end}
>

Also see:

2 Likes