Handling events in parents live components

Hi,
Coming from React I’ve been a bit struggling to understand how to structure and handle live component events.
To give a specific example I have two live components.

  1. FileUpload live component which presents a dialog for the user to upload files
  2. Files live component which wraps FileUpload and converts file to appropriate structure to attach to the parent entity.
    What I’m trying to do seems to be relatively simple - I want the FileUpload component to call a handler passed to it with a list of uploaded files. That handler is in the Files component.
    I’m aware of the fact that I can use send and handle_info in the main parent LV. But in this scenario, I don’t want LV to handle this communication and let those components handle it.
    I am looking at it in the wrong way?

Could you share what you have for those two components currently?

I’ve found myself in the same position a few times coming from React and my general inclination at the moment is to use functions for markup and LiveView components for reusing behaviours. I tend to have fewer components in LiveView than I would naturally have for the same UI in React.

We are using the surface UI and it’s pretty much the same deal. Just slightly different syntax for templates.

Files component:

 def render(assigns) do
    ~H"""
    <div>
      <FileUpload
        id="file-upload"
        accept={{ :any }}
        max_entries=3
        upload_handler={{ %{target: __MODULE__, id: @files_id}  }}
      />
      <span :for={{ attachment <- @attachments }}>{{ attachment.file.name }}</span>
    </div>
    """
  end
 def handle_uploaded_files(files_id, files) do
    send_update(__MODULE__, id: files_id, uploaded_files: files)
  end

def update(assigns, socket) do
    if assigns[:uploaded_files] do
      uploaded_attachments =
        Enum.map(
          assigns[:uploaded_files],
          fn f -> attach_to_owner(f, owner) end
        )

      socket =
        update(socket, :attachments, fn attachments -> attachments ++ uploaded_attachments end)

      {:ok, socket}
    else
      attachments = get_files_for(owner)
      socket = socket |> assign(assigns) |> assign(attachments: attachments)
      {:ok, socket}
    end
  end

FileUpload component:

def render(assigns) do
    ~H"""
    <Form submit="save" change="validate" for={{ :upload }}>
      <LiveFileInput upload={{ @uploads.file }} />
      <Form.Submit>Upload</Form.Submit>
      <p class="alert-error" :for={{ {_ref, error} <- @uploads.file.errors }}>
        {{ Phoenix.Naming.humanize(error) }}
      </p>
      <For each={{ entry <- @uploads.file.entries }}>
        <div class="flex flex-col w/2">
          {{ live_img_preview(entry, height: 80, class: "flex") }}
          <progress max="100" value={{ entry.progress }} />
          <button :on-click="cancel-upload" phx-value-ref={{ entry.ref }}>
            Cancel
          </button>
        </div>
      </For>
      <span class="alert-info" if={{ length(@uploaded_files) }}>{{ length(@uploaded_files) }} {{ inflect("files", length(@uploaded_files)) }} uploaded.</span>
    </Form>
    """
  end

def handle_event("save", _params, socket) do
    # Entries are already consumed by S3 client uploader need to call this to trigger cleanup.
    files =
      consume_uploaded_entries(socket, :file, fn meta, entry ->

        Path.join(
          s3_host(socket.assigns.bucket, region),
          s3_key(entry, socket.assigns.path_prefix)
        )
      end)

    %{target: target, id: id} = socket.assigns.upload_handler

    target.handle_uploaded_files(id, files)

    {:noreply, assign(socket, :uploaded_files, files)}
  end

What I did for now is pushed the target module as a property to children component and drilled the id of it. It feels very hacky to me and I’m not even sure how to unit test this at the moment.

I’d love to hear what patterns people are using for components like this. I would probably just have one component personally or use send(self(), message) to handle it.

When I last implemented live uploads (for user avatar images), I put the allow_upload/3 in the main LiveView (mount), then pass the @uploads assign to a stateful LiveComponent, where the user does the upload with live_file_input/2. The input is inside a form with phx_target: @myself so the Component will handle the form submission event. When it’s finished, it emits a PubSub message so the image link will be available globally.

So you use PubSub to communicate between components ?
I do like this way, though I have been told that it can have severe performance impact as message is being broadcasted to all connected LV clients, have you noticed the impact ?

I haven’t noticed any performance issues in testing. And I use PubSub for many things, not just file uploads; also live updates for user profiles and comments and such.

If I didn’t need to keep the other users up-to-date, I would just use send(self(), message) (the parent LV is always the source of truth). But PubSub covers both bases.

Have you tried handling the save event in the patent and passing the event down to FilesUpload? I might be missing something but it seems odd to pass the parents module down so you can tell it to update itself with a callback.

The handle_uploaded_files could then be moved to the child so the parent can call that to update the child.

Sure, you could handle it in the parent LV. I like to use Components as much as possible for separation of concerns and to reduce the amount of code in the parent LiveView module.

it seems odd to pass the parents module down so you can tell it to update itself with a callback.

Remember that I am using PubSub to not just update the parent LV, but ALL liveviews globally. So I am sending out a message either way; it doesn’t really matter whether it’s the LV or the LC which sends the message.

The problem specific to our scenario is that the Files component is actually used in several different live views. Which means if parent LV is responsible for handling the comms we would need to copy/paste the same logic.

Have you seen Phoenix.LiveComponent - Targeting Component Events? If your components are the source of truth in your app, and you don’t want to involve the parent LV with handling the event, it looks like you can target another component and even multiple components by passing their DOM id(s) to phx-target.

I’m not too familiar with this because in my apps I prefer the parent LV to be the source of truth for almost everything. But maybe it could work for your use case?

You could put the reused part from your save event into a function in the component, pass down a save event from the LiveView to the component. In the LV’s event, call the common function of the component and then implement the custom logic for that LV. At the moment the event is on the client with some common logic followed by the custom handler of the LV - invert the relationship.

Not sure if it’s the most elegant solution but I did something like that yesterday when migrating to Surface.

can you share an example code? I struggle to follow the description a bit.

According to docs:

If you want to target another component, you can also pass an ID or a class selector to any element inside the targeted component.

I’m trying to handle an event outside of this component. Additionally, I cannot really use phx-target because the event is raised from an event handler. See handle_event("save") in the example above.