Unable to send message from LiveComponent to it's parent

Hi,

I’m building a wizard that lets users assemble a package of compatible hardware parts.

The user can select the hardware he wants from a select menu. When that happens, I update the internal state of the LiveComponent named WizardLive.Form. This should be the source of truth.

This is defined in WizardLive.Edit

        <.live_component
          module={EasySolutionsWeb.WizardLive.Form}
          summary={@summary}
          id="form"
          step={@step}
          sub_step={@sub_step}
        />

And within WizardLive.Form:

    <%= if @step == "camera" do %>
      <.live_component
        id="camera"
        module={ComponentSelector}
        context="camera"
        form={@form}
        selectable_options={Cameras.list_approved_cameras()}
      />
    <% end %>

This is the function that is triggered within ComponentSelector when Add is clicked.

  def handle_event("add_component", %{"value" => context}, socket) do
    IO.puts("add_component: #{context}")
    component_id = socket.assigns.component_id_selected

    socket =
      if component_id == nil do
        {:noreply, socket}
      else
        component =
          case context do
            "camera" ->
              Cameras.get_camera!(component_id)
          end

        components = socket.assigns.selected_components

        # We want to tell the parent component that the selected components have changed.
        # Tell WizardLive.Form to update its assigns with the newly selected components.
        send self(), {:selected_components, [component | components]}
       # ^ Iterated to use send_update. Please see later posts in this thread. :-)
      end

    {:noreply, socket}
  end

I then defined a handle_event in ComponentSelector’s parent (WizardLive.Form)

  def handle_info( {:selected_components, list}, socket) do
     IO.inspect(list)
    {:noreply, socket}
  end

But that triggers this error:
** (MatchError) no match of right hand side value: {:selected_components, [%MyApp.Cameras.Camera{meta: ecto.Schema.Metadata…

I’ve tried to put the handle_event function in both ComponentSelector’s parent, the WizardLive.Form and the main LiveView (Wizard.Summary.Edit).

The structure is as follows:

Wizard.Summary.Edit
This is the parent LiveView that renders .livecomponent module=Form

WizardLive.Form
This is a LiveComponent.
Within Form’s render I do .livecomponent module=ComponentSelector.

ComponentSelector
This is also a LiveComponent and is where I try to send the message that I want to be received in WizardLive.Form.

How can I craft the message sent from ComponentSelector so that WizardLive.Form receives it?

You can use send_update to send data to a live component.

Additionally instead of needing the component to know how to send a message to the parent you can provide a callback from the parent <.live_component … on_selection_change={fn list -> send_update(__MODULE__, id: @id, list: list) end}. That way the child can be blissfully unaware of if the parent is a live component or a live view.

3 Likes

Thanks for the suggestion, @LostKobrakai .
I tried to replace send self() with this:
send_update(WizardLive.Form, id: "components", components: components)

But I’m not sure how to setup WizardLive.Form to receive that message.
The docs says:

When the component receives the update, update_many/1 will be invoked if it is defined, otherwise update/2 is invoked with the new assigns. If update/2 is not defined all assigns are simply merged into the socket.

This is how Form’s update() currently looks:

  def update(assigns, socket) do
    {
      :ok,
      socket
      |> assign(assigns)
      |> assign_projectors()
      |> assign_lcd_panels()

This is the error I get:

** (MatchError) no match of right hand side value: {:phoenix, :send_update, 
{{MyAppWeb.WizardLive.Form, "components"}, %{components: [%MyAppWeb.Cameras.Camera

Sorry for being slow. I don’t understand how I should modify the update() function to keep the existing functionality and, at the same time, be able to receive the message.
Please advise me! :blush:

Is "components" the id of your parent live component? You should get the data send with send_update as assigns on update. Most often you want to add something to that data to differenciate those update calls from ones called by updating attrs on <.live_component /> on the parent.

No, it’s not. I’m not sure what ID it’s referring to.
Is it the dom ID?

I want the message to end up in the WizardLive.Form (LiveView).
How do I see what ID it has?

When you render a live component with <.live_component /> you need to provide a mandatory id for it. That one you need.

I guess the ID is “camera”?
Not the best name perhaps…

You need the id of the parent though, you’re trying to send an update to that one.

  def render(assigns) do
    ~H"""
    <div class="page-container">
      <div class="form-container" phx-update="stream" id="wizardform">
        <.live_component
          module={EasySolutionsWeb.WizardLive.Form}
          summary={@summary}
          id="form"
          summary_id={@summary.id}
          step={@step}
          sub_step={@sub_step}
        />
      </div>

Looks like it is “form”?
I changed send_update to use the id “form”, but that yields this error:

** (MatchError) no match of right hand side value: {:phoenix, :send_update, {{EasySolutionsWeb.WizardLive.Form, "form"}, %{components: [%

It looks like it hits the correct module. That’s something. :slight_smile:
This is how my update function looks in that module:


  def update(%{components: _components} = assigns, socket) do
    {
      :ok,
      socket
      |> assign(assigns)

Can you please help me to get the signature of the update() function correctly? :pray:

That error doesn’t look like it would be affected by how your update callback looks like. You’d never receive that tuple.

Can you show the code where you are doing the send_update ?

Sure! :blush:
This is where I run the send_update():

  def handle_event("add_component", %{"value" => context}, socket) do
    component_id = socket.assigns.component_id_selected

    socket =
      if component_id == nil do
        {:noreply, socket}
      else
        component =
          case context do
            "camera" ->
              Cameras.get_camera!(component_id)
          end

        components = socket.assigns.selected_components

        # Now we want to tell the parent component that the selected components have changed.
        # E.g. WizardLive.Form should update it's assigns with the new selected components.
        send_update(MyAppWeb.WizardLive.Form, id: "form", components: [component | components])
      end

    {:noreply, socket}
  end

Hello.
I think you need to use send_update/2 instead of send/2 to send message from ComponentSelector to WizardLive.Form because of send/2 used for process-to process communication, while send_update/2 allows a child component to update its parent component’s state.

send_update(WizardLive.Form, id: "form", selected_components: [component | components])

Please replace the send self() call in ComponentSelector with send_update/2.

You’re right @PhantomDev314. :slight_smile:
It wasn’t visible in my original message, but @LostKobrakai has suggested that already. I’m now trying to get WizardLive.Form to receive the message.

Good morning,

I’ll summarize what I’ve learned in this thread and what the code looks like now:
The handle_event(:add_component triggers
send_update(MyAppWeb.WizardLive.Form, id: "form", components: [component | components]).

That message should be received by WizardLive.Form, but I’m not sure how to design the update() function that will receive this message.

Here is where I try to send the message from my LiveComponent:

defmodule MyAppWeb.Components.ComponentSelector do
  use MyAppWeb, :live_component
  import Phoenix.HTML.Form
  alias MyApp.Cameras

  def handle_event("add_component", %{"value" => context}, socket) do
     component_id = socket.assigns.component_id_selected

    socket =
      if component_id == nil do
        {:noreply, socket}
      else
        # TODO: Send the function to fetch the data as an argument to CoponentSelector instead of having the logic here.
        component =
          case context do
            "display" ->
              display = Components.get_display!(component_id)

            "camera" ->
              Cameras.get_camera!(component_id)
          end

        components = socket.assigns.selected_components
        assign(socket, :selected_components, [component | components])

        # And now we want to tell the parent component that the selected 
        # components have changed.
        # WizardLive.Form should update its assigns with the newly selected components.
        send_update(MyAppWeb.WizardLive.Form, id: "form", components: [component | components])

      end
    {:noreply, socket}
  end


end

The WizardLive.Form LC is mounted like this:

defmodule MyAppWeb.WizardLive.Summary.Edit do
  def render(assigns) do
    ~H"""
    <div class="page-container">
      <div class="form-container" phx-update="stream" id="wizardform">
        <.live_component
          module={MyAppWeb.WizardLive.Form}
          summary={@summary}
          id="form"
          summary_id={@summary.id}
          step={@step}
          sub_step={@sub_step}
        />
      </div>

Here, I want to receive the message from send_update.
If I understand it correctly, I should be able to do so if I get the update() signature correctly.
This is the error message I get now when
send_update(MyAppWeb.WizardLive.Form, id: "form", components: [component | components]) is triggered:

[error] GenServer #PID<0.12627.0> terminating
** (MatchError) no match of right hand side
 value: {:phoenix, :send_update, {{MyAppWeb.WizardLive.Form, "form"}, 
%{components: [%MyApp.Cameras.Camera{
__meta__: #Ecto.Schema.Metadata<:loaded, "cameras">, id: 1,
 approved: false, name: "cam1"
Last message: %Phoenix.Socket.Message{topic: "lv:phx-22", 
event: "event", payload: %{"cid" => 3, "event" => "add_component", 
"type" => "click", "value" => %{"value" => "camera"}}, ref: "13", join_ref: "4"}
defmodule MyAppWeb.WizardLive.Form do
  use MyAppWeb, :live_component
  import Phoenix.HTML.Form

  def update(%{components: _components} = assigns, socket) do 
      IO.puts "in update =============="
      # This is never executed..
    {

Do you have any suggestions on how I can craft an update() in WizardLive.Form to receive what send_update sends?

I figured it out.

The MatchError I got referred to what send_update returned within the event_handler.
I returned {:noreply, {:phoenix, :send_update, {{MyAppWeb.WizardLive.Form, "form"},...when I should have returned {:noreply, socket}

** (MatchError) no match of right hand side
 value:** (MatchError) no match of right hand side
 value: {:phoenix, :send_update, {{MyAppWeb.WizardLive.Form, "form"}, 
%{components: [%MyApp.Cameras.Camera{.WizardLive.Form, "form"}, 
%{components: [%MyApp.Cameras.Camera{

I fixed it simply by returning socket at the end of the function.
Thanks for helping me out, guys!

  def handle_event("add_component", %{"value" => context}, socket) do
    component_id = socket.assigns.component_id_selected

    socket =
      if component_id == nil do
        {:noreply, socket}
      else
        # TODO: Test passing the function to fetch the data, 
        # as an argument to CoponentSelector.
        component =
          case context do
            "camera" ->
              Cameras.get_camera!(component_id)
          end

        components = socket.assigns.selected_components
        assign(socket, :selected_components, [component | components])

        send_update(MyAppWeb.WizardLive.Form, 
          id: "form", components: [component | components])
        socket # Return socket, and things work!
      end
    {:noreply, socket}
  end

Hey @martins this still has a bug.

    socket =
      if component_id == nil do
        {:noreply, socket}

If component_id is nil this whole function if will return {:noreply, socket} which means your function will return {:noreply, {:noreply, socket}}.

And actually your else clause has a bug too. This line assign(socket, :selected_components, [component | components]) doesn’t do anything. You call assign with the socket, but do not bind its result.

1 Like

Yikes, embarrassing to see how crap my code is!
Thanks for helping me, @benwilson512.

1 Like