Dispatching LiveComponent based on module

I have a LiveView where I want to render two different live components that each have their own representation of the same module. Think something like Elixir forum’s editor window: a panel with controls on the left, and the formatted output on the right. Both operate on the same content, but each render differently.

I thought about using protocols, something like

# Dashboard.ex
defmodule Dashboard do
  defprotocol Editor do
    def render(struct)
  end

  defimpl Editor, for: Avatar do
    use Phoenix.LiveComponent
    def render(struct) do
      ~H"""
      <div phx-target={@myself}>...</div>
      """
    end

    def handle_event("event", _, socket) do
       {:noreply, socket}
    end
  end

  defprotocol Preview do
    def render(struct)
  end

  defimpl Preview, for: Avatar do
    use Phoenix.LiveComponent
    def render(struct) do
      ~H"""
      <div phx-target={@myself}>...</div>
      """
    end

    def handle_event("event", _, socket) do
       {:noreply, socket}
    end
  end
end
# view.heex
<%= live_component Dashboard.Editor, @avatar %>
<%= live_component Dashboard.Preview, @avatar %>

Because live_component passes assigns as an enumerable, I’m not sure I can get the behaviour I want here. Does anyone have any suggestions? Should I just stick with pattern matching?

This should work just the fine the way you are describing. But when you pass the assigns to a LiveComponent, it should be a keyword list like this:

<%= live_component Dashboard.Editor,
      id: "editor-component",
      avatar: @avatar,
      content: @content %>
<%= live_component Dashboard.Preview,
      id: "preview-component",
      avatar: @avatar,
      content: @content %>

Unless I’m missing a trick, this is my blocker—live components receive only one argument, a keyword list, but Protocol only knows how to dispatch to implementations with a concrete type as the first argument

Why do you need a Protocol for this? Do you have other structs besides Avatars which will use the same Editor/Preview components?

No requirement on Protocol—it looked like a suitable solution at first glance. But yeah, I have a number of structs (and growing) that need to be rendered in 2 or 3 different ways.

I avoided over-engineering by hardcoding at the start, but as the number of structs grew it got tiresome to write all the boilerplate. This is my half-way approach that kinda works, but using the defimpl to call out to another component is a level of indirection I’d rather not have to deal with.

defmodule Dashboard
  defprotocol Editor do
    def view(component)
  end

  defmodule Avatar do
    use Ecto.Schema

    embedded_schema do
      field(:url, :string)
    end

    defimpl Dashboard.Editor do
      def view(component)
        live_component(Dashboard.Components.Editor.Avatar, [module: component, id: "example"])
      end
    end
  end

  defmodule Components do
    defmodule Editor do
      defmodule Avatar do
        use Phoenix.LiveComponent

        def render(assigns) do
          ~H"""
            <div>...</div>
          """
        end

        def handle_event("event", _, socket) do
          {:noreply, socket}
        end

      end
    end

   defmodule Preview do
      defmodule Avatar do
        ...
      end
    end
  end
end
<%= Dashboard.Editor.view(@avatar) %>
<%# Dashboard.Preview.view(@avatar) %>