Communicating Between Live Components

Hello all!

I’ve been working a few days on trying to solve a relatively simple issue, however it’s turning into quite the road block. I have a Live Component. which is nested inside of a Form Component which I’ve used to replace a custom AJAX search box which was dreadful to use and consisted of 400 lines of javascript.

I’m having an issue attempting to reconstruct part of the functionality, which basically involves copying the value in short to a related sibling search box if it happens to be empty.

I’ve had little luck so far in getting the two Live Components to talk to each other without causing a mess.

The first idea i had was to use Phoenix PubSub, but discovered live components can’t have handle info, so that goes to the parent, which is fine. I decided to try the more direct route, and use send/2 which gets my message to the parent however it looks like the only option is send_update/2 to get anything to a child liveview.

That won’t work because calling update wipes out all of the existing assigns for the component, and there is no way to get the originals back to it (as its a different component thats requesting the update). Back to square one!

I’m not terribly familiar with send/2, however that could be another avenue to explore. I don’t really see anyway to let the requesting component know which process it needs to send to. I tried backtracking through the Live Component code for send_update, but it does a weird thing with the Phoenix Channel and it doesn’t look obvious how it actually triggers update on the correct component by ID.

What are the other options for communicating between Live Components? The less desirable I can think of would be to fire up full blown Live Views for every search box, but that seems too heavy for one input and some basic search functionality.

1 Like

This shouldn’t be true, are you using stateless or stateful components? To do the kind of communication you want you definitely need stateful live components, and in those cases update does not clobber existing assigns.

1 Like

I usually have a simple public API for each component that needs to be talked to. Something like:

defmodule LiveComponentTwo do
  ######### Public API
  def update_text(id, text) do
    send_update(__MODULE__, id: id, update: :update_text, text: text)
  end
  #########

  def update(%{update: :update_text, text: text}, socket) do
    # I suppose here you could check if the value in the socket is empty
    {:ok, assign(socket, :text, text)}
  end

  def update(assigns, socket) do
    {:ok, assign(socket, assigns)}
  end
end

then, LiveComponentTwo.update_text("id_of_the_component", "text")

Notice that I add a key update: :update_text and then pattern match on that key in the update callback.

6 Likes

If i’m understanding the differentiation in the docs I believe they are stateful. I use handle_event all over them and they are Phoenix.LiveComponent’s and not Phoenix.Component’s. My update function looks like this:

def update(assigns, socket) do
    name = String.to_existing_atom("#{Map.get(assigns, :assoc)}_id")

    socket = socket
    |> assign(assigns)
    |> assign([items: [], vid: nil, index: 0, count: 0, name: name])

    {:ok, assign(socket, value: get_value(socket.assigns))}
  end

When I call send_update from the parent i’m only passing vid and value (as I don’t have access to the other assigns needed at this point), and inspecting update those are the only variables available in assigns.

send_update(Search, id: id, value: value, vid: vid)

> Inferred IO.inspect In the Update Function
> %{                                                                                                                                                                                                                                                                            
  id: "component id",                                                                                                                                                                                                                                                
  value: "field value",                                                                                                                                                                                                                                                   
  vid: "field id"                                                                                                                                                                                                                                                
} 
1 Like

I did try pattern matching like that however I ran into the same issue where it wiped out all of my assigns.

Can you show an example? I have lots of code that works this way and it works just fine.

Here’s the Live Component in question. Note that it’s a prototype and employs thousands of dirty, no good hacky hacks.

defmodule Proj.Search do
  use Phoenix.LiveComponent
  use Phoenix.HTML

  import Ecto.Query

  alias Proj.Repo

  @impl true
  def update(assigns, socket) do
    name = String.to_existing_atom("#{Map.get(assigns, :assoc)}_id")

    socket = socket
    |> assign(assigns)
    |> assign([items: [], vid: nil, index: 0, count: 0, name: name])

    {:ok, assign(socket, value: get_value(socket.assigns))}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <span>
      <input
        id={@id}
        class={iclass(assigns)}
        autocomplete="off"
        phx-target={@myself}
        phx-keyup="input"
        phx-hook="SearchInput"
        autofocus={Map.get(assigns, :autofocus)}
        tabindex={Map.get(assigns, :tabindex)}
        value={@value}/>
      <div class="sc-container nueue"><%= for {item, i} <- Enum.with_index(@items) do %>
        <div class={class(i, @index)}>
          <div class="sc-tit" phx-click="fin" phx-value-id={item.id} phx-target={@myself}><%= Map.get(item, @search) %></div>
        </div>
      <% end %></div>
      <%= if not is_nil(@value) do %>
        <%= hidden_input @f, @name, value: @vid %>
      <% end %>
    </span>
    """
  end

  @impl true
  def handle_event("input", %{"key" => "ArrowUp"}, socket) do
    ind = socket.assigns.index
    index = if ind == 0, do: socket.assigns.count - 1, else: ind - 1
    s = Enum.at(socket.assigns.items, index)
    {:noreply, assign(socket, index: index, value: Map.get(s || %{}, socket.assigns.name), vid: Map.get(s || %{}, :id))}
  end

  def handle_event("input", %{"key" => "ArrowDown"}, socket) do
    ind = socket.assigns.index
    index = if ind == socket.assigns.count - 1, do: 0, else: ind + 1
    s = Enum.at(socket.assigns.items, index)
    {:noreply, assign(socket, index: index, value: Map.get(s || %{}, socket.assigns.name), vid: Map.get(s || %{}, :id))}
  end

  def handle_event("input", %{"Key" => k, "value" => val}, socket) when k in ["Tab", "Enter"] do
    if val == socket.assigns.value do
      {:noreply, socket}
    else
      handle_event("fin", %{"id" => socket.assigns.vid}, socket)
    end
  end

  def handle_event("input", %{"value" => val}, socket) do
    assigns = socket.assigns
    if assigns.value == val do
      {:noreply, socket}
    else
      q = val <> "%"
      i = if val == "" do
        []
      else
        source = Ecto.build_assoc(assigns.f.data, assigns.assoc).__struct__
        from(s in source)
        |> where([s], field(s, :active) == ^true and ilike(field(s, ^assigns.search), ^q))
        |> order_by([s], [asc: field(s, ^assigns.search)])
        |> limit(20)
        |> Repo.all()
      end

      vid = if Enum.empty?(i) do nil else Map.get(List.first(i), :id) end

      {:noreply, assign(socket, items: i, count: Enum.count(i), vid: vid, index: 0)}
    end
  end

  def handle_event("fin", %{"id" => id}, socket) do
    s = Enum.find(socket.assigns.items, fn x -> x.id == id end)
    value = Map.get(s || %{}, socket.assigns.search)
    if Map.get(socket.assigns, :affect) do
      send(self(), {:affect, id: socket.assigns.affect, value: value, vid: id})
    end
    {:noreply, assign(socket, items: [], count: 0, value: value, vid: id, index: 0)}
  end

  defp class(i, ind) do
    if i == ind do
      ["sc-t", "active"]
    else
      "sc-t"
    end
  end

  defp iclass(assigns) do
    [Map.get(assigns, :class, [])] ++ ["search"]
  end

  defp get_value(assigns) do
    (Proj.get_assoc(assigns.f, assigns.name, assigns.assoc) || %{}) |> Map.get(assigns.search)
  end
end

And the function call in the parent:

def handle_info({:affect, id: id, value: value, vid: vid}, socket) do
    send_update(Proj.Search, id: id, value: value, vid: vid)
    {:noreply, socket}
end

Right I guess what I was trying to look for was a version of the pattern matching on update that wasn’t working as you expected. The version you’ve posted here has a single def update clause.

Well now that i’ve gone back and combed over the multiple update defs method it’s working for me! It sure would be nice if this behaviour was documented…

Thank you for pointing me in the right direction

Thank you SO much for posting this example! I just ran into the same issue when trying to communicate between components and this is incredibly helpful. I had not thought of writing two update functions and then pattern matching on a specific update. Quick question …

If I’m understanding your approach correctly, you provide a public API that then calls send_update on itself. Why don’t you have LiveComponent1 just call send_update directly onto LiveComponent2 using that update: key?

It’s cleaner this way, easier to refactor, it’s obvious that this component is called from outside just by opening the file.

2 Likes