Communication between live components?

Hi,

I evalute liveview and cosider moving our current react solution to liveview at some point.

For that I try to rewrite one of our pages. It is a list page, listing the main entity of our app. It also allows to select entities and invoke actions. Each action is shown as a dialog.

That woks well so far, but now I am running into a problem:

For one specific action, filtering for customers, I show a dialog. The dialog displays two select widget, selecting customers to include and to exclude. The select widget is rather complex and allows to select a set of customers.

The dialog and the contained select-widgets are both live components. In the select widget I want to notify the parent (dialog) about changes of the selection. But I see no valid way of doing that.

  • By Sending a message, I will only reach the liveview.
  • Using the example code in the phoenix docs, sending a function as parameter to the select widget is also difficult, because I do not want to send the results right away to live view, I rather want to update the state of the dialog

My prefered solution would be to trigger an event (like phx-change) in the select widget that would be received by the dialog. Is that possible?

I hope the question isn’t too confusiong.

I don’t know if you’ve run into this part of the documentation. But maybe what you need to do is this:
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html#module-livecomponent-as-the-source-of-truth

If you’ve tried it and could share a code sample

Yes I did. But I understand they allow just to transform the message with a function that I pass to the component. But I want to receive the updates from the select widget, put it into the state of the dialog component, and when the user closed the dialog, I want to pass the data to the liveview. That is something that they did not explain in the documentation.

Oh, in that case all you’d need to do is add a phx-click like you mentioned but don’t define a target in the component, that way the LiveView will be responsible for handling that event.
The reason they don’t suggest that in the documentation is because using a component to update a LiveView causes the whole page to reload, so be mindful about your states.

You can do something like this:

# parent live competent
<.live_component module={...} parent={@myself} />

# child component
<div phx-click="foo" phx-target={@parent}>...</div>

The word "parent" isn’t magic. You can call it whatever you want.

Maybe I misunderstood your question though.

1 Like

From what I also understood thats what he want to do.
But he doesnt need to pass the parent, as by default the LiveView responsible for that component will handle the event

I don’t want to loop through the event. I want to update the parent about changing in the widget. The parent will later take this data and send it to the liveview (user closing the dialog).

I managed to hack-implement what I wanted (found How to send an event from within `handle_event` in a Phoenix LiveView · GitHub):

    send(
      socket.root_pid,
      %Phoenix.Socket.Message{
        event: "event",
        join_ref: nil,
        payload: %{
          "cid" => socket.assigns.parent.cid,
          "event" => "update",
          "value" => selected |> Enum.map(fn %{id: id} -> id end)
        },
        ref: nil,
        topic: "lv:" <> socket.id
      }
    )

Why is liveview not offering a way to do this kind of updates?

1 Like

It does, look at these

https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html#module-unifying-liveview-and-livecomponent-communication

https://hexdocs.pm/elixir/Kernel.html#send/2

1 Like

The “blessed” way to do what you’re asking is to use send_update/3 to send an update to the parent component and then handle it in the update/2 callback. The docs also provide guidance for “unifying” the communication by passing a function which sends a message (via send/2 or send_update/2) back to the caller.

Personally I strongly dislike these APIs and I think something else (unified) is needed. It has been discussed before, and it will probably happen some day.

3 Likes

The issue about that has been closed in favor of the explicit option of passing an anonymous function. This would‘ve been experimented with pre 1.0 if there would‘ve been the intend to add it.

2 Likes

Sorry to hear that - I don’t think it’s a very good solution.

I will hold out hope, but I assume you are probably correct. Guess it’s time to cook up my own solution, then.

Ok I try again with a more complete example because I think you can already do what you want, easily:

defmodule FooView do
  def render(assigns) do
    ~H"""
      <div>my parent liveview</div>
      
      <.live_component module={MyFirstChildComponent} id="first-child-component" />    
    """
  end
end

defmodule MyFirstChildComponent do
  def mount(socket), do: assign(socket, :count, 0)

  def handle_event("clicked-from-child", _params, socket) do
    # do whatever you want and this will update the child!
    {:noreply, assign(socket, :count, socket.assigns.count + 1)}
  end
  
  def render(assigns) do
    ~H"""
    <div>now we are in the first child component</div>
    
    <%!-- the "parent" argument could be named anything! --%>
    <.live_component
      module={MySecondChildComponent}
      id="my-second-child-component"
      parent={@myself}
      count={@count}
    />
    """
  end
end

defmodule MySecondChildComponent do
  def render(assigns) do
    ~H"""
    <div>Finally we are in the child component!</div>
    
    <span>{@counter}</span>
    
    <%!-- Here the phx-target needs to be the argument that was given into this component.
    So if you named it "foobar" it would be @foobar --%>
    <button phx-click="clicked-from-child" phx-target={@parent}>Click me!</button>
    """
  end
end

Thank you for creating this example.

MySecondChildComponent is in my case a bit more complex. It is a typeahead field, which allows to select one ore more customers.

So I would prefer to handle the event in handle_event and then maybe push an update to the parent (selection changed). But I guess it would somehow be possible to send the event directly to the parent from the html-element. But what, if I also need to update the state of MySecondaryChildComponent?

What speaks against the following?

# parent
def render(assigns) do
  ~H"""
    <.autocomplete 
      users={@users} 
      on_selection={fn selection -> send({:selection, selection}) end} />
  """
end

def handle_info({:selection, selection}, socket) do 
  # change parent state based on selection made
end

# autocomplete
def handle_event("selection", %{selected => ids}, socket) do
  selected = validate_selection(socket.users, ids)
  socket.assigns.on_selection.(selected)
  # update internal state
  {:noreply, socket}
end

If the parent happens to be a live component switch from send/1/handle_info/2 in the parent to send_update/3/update/2

1 Like

I think that would be possible.

I guess on line 6 you would do send(self(), {:selection, selection}), right?

I have not tried the send_update. Coming from react it feels confusing to received parameters from the parent, in my case the dialog live component, and from the child, the Typeahead field, in the same function.

Right now I use a utility module to send events to parent:

defmodule WebUI.Utils.LiveUtils do
  use Phoenix.LiveComponent

  def send_event(socket, event, value, parent \\ nil) do
    send(
      socket.root_pid,
      %Phoenix.Socket.Message{
        event: "event",
        join_ref: nil,
        payload: %{
          "cid" => parent || socket.assigns.parent.cid,
          "event" => "update",
          "type" => "other",
          "value" => value
        },
        ref: nil,
        topic: "lv:" <> socket.id
      }
    )
  end
end

EDIT: this is my opinion and not an overall position of the Phoenix team about LiveComponents. Give it a try and evaluate it for yourself.

IMO LiveComponents are one of the most overused features of LiveView. It makes sense from a historical sense, they were one of the first abstractions to be added, but today we have function components, LV hooks, JS commands, and others which can be better suited for creating abstractions.

For example, imagine you are creating a “live table” with pagination and search. Most people would immediately design it as a LiveComponent for reuse, but I would rather introduce a LiveTable data structure and build on top of that. Something like this:

assign_live_table(socket, :posts, arg1, some_opt: ...)

And now, all of the state of the table is stored directly in the socket, which you can access and manipulate through additional functions. When it comes to rendering, you pass the table plus the name of any event you want dispatched, as if it was any other function component/html element:

<LiveTable.render table={@posts} row-click="some-event" />

Inside LiveTable.render, you can use JS commands to dispatch events and, if there are internal events, you can use attach_hook during “assign_live_table” to consume them.

This approach should not be foreign either. This is precisely how streams and assign async are designed and implemented in LiveView itself.

We have tried this direction with couple of our Dashbit clients and, every time so far, it has led to better designs: components that are easier to use and tests. Of course, live components still have their use case, especially as they can optimize what goes over the wire, but they are likely to be overused, so keep alternative approaches in mind.

13 Likes

How would you handle multiple LiveTable components in one LiveView?

6 Likes

To me that’s the main benefit of using liveview, designing pure state modules that encapsulate generic state “islands” and transition functions. I don’t define assign_x or update_x functions, in the sense that my state modules never see the sockets (they could), but helpers like an update_state function :

# creation
socket |> assign(:my_table, LiveTableState.make(some_collection))

# update after an event
socket |> update_state(:my_table, fn state_value -> LiveTableState.some_transition(state_value, some_argument) end)

And of course an appropriate hierarchy of components capable of handling a LiveTable’s state.

Just like streams do: Namespacing under a key.

My question was related to the internal events. If you look at the example in the documentation of attach_hook, it is not obvious (to me at least) how to separate the events if multiple components are used in a LiveView.

1 Like