Send_update to merge changes into assigns

Working in a LiveComponent, is there a way to merge updates to assigns with the existing assigns once the data arrives?

For example, if I have two inputs, a URL input url and a text input title. I’m handling a blur event on the URL input which will asynchronous populate the title text input, but I’d like to allow the user to continue to modify the form as they desire.

def handle_event("url_blur", %{"value" => url}, socket) do
  pid = self()
  
  Task.async(fn ->
    title = fetch_title(url)

    send_update(
      pid,
      __MODULE__,
      socket.assigns
      |> assign(id: socket.assigns.id, loading: false)
      |> update(:changeset, fn c ->
        c
        |> Ecto.Changeset.put_change(:title, title)
      end)
    )
  end)
  
  {:noreply, assign(socket, loading: true)}
end

I set loading to true so I can indicate that to the user and I send an asynchronous update to assigns with a change to the changeset. However, if there is user input on other inputs in the same form/changeset before the asynchronous changeset update arrives, that user input is unfortunately wiped once the change to title finally occurs.

I have attempted a few variations of this, such as adding the values to assigns and then performing the changeset update in the live_component’s update/2. However, I’m not able to find a way to merge the asynchronous changeset. Is there another approach I’m not thinking of?

I haven’t yet tried the independent pubsub approach because I’m not certain it will work any differently, and it will require significant refactoring because my form as a live_component is used in multiple contexts. Will I be required to use a live_view with a pubsub?

Yes, sending a message to the LiveView is the only method to asynchronously patch assigns. send_update will not work for this.

def handle_event("url_blur", %{"value" => url}, socket) do
  pid = self()
  
  Task.async(fn ->
    title = fetch_title(url)

    send(
      pid,
      {:update_changeset, title}
    )
  end)
  
  {:noreply, assign(socket, loading: true)}
end

def handle_info({:update_changeset, title}, socket) do
  changeset = socket.assigns.changeset
    |> Ecto.Changeset.put_change(:title, title)

  {:noreply, socket |> assign(changeset: changeset, loading: false)}
end

def handle_info(_, socket) do
  {:noreply, socket}
end

This is unfortunate because I had hoped to manage state in a LiveComponent.