Jese Leos

Render Components from Turbo Broadcasts

I was surprised to learn there is no built-in way to render components from turbo broadcasts.

Components (Phlex or ViewComponent) still feel very underutilized in Rails. Perhaps that will change in the future, but we made 20 years without them, so perhaps not.

Either way, I wanted an easy way to re-use them when broadcasting changes.

The easiest option I could find was something that looked like this:

def broadcast_created
   component = Component.new(self)
   html = ApplicationController.render(component, layout: false)

    broadcast_append_later_to(
      "the_stream",
      target: "the_target",
      html: 
    )
end

This works fine for a one-off. I could always add something like .as_html to my base component class to make it less repetitive. But if I wanted to make the "component" just work, I added the following as an initializer in my Rails app.

module TurboStreamsBroadcastExtensions
  def broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering)
    render_component_to_html(rendering)
    super
  end

  def broadcast_action_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering)
    render_component_to_html(rendering)
    super
  end

  private

  def render_component_to_html(rendering)
    if rendering&.dig(:component)&.respond_to?(:render_in) && rendering&.dig(:html).blank?
      rendered_component = ApplicationController.render(rendering.delete(:component), layout: false)
      rendering[:html] = rendered_component
    end
  end
end

Turbo::Streams::Broadcasts.prepend TurboStreamsBroadcastExtensions

With this in place, I can simply add "component" to my broadcast_append_later_to

def broadcast_created
    broadcast_append_later_to(
      "the_stream",
      target: "the_target",
      component: Component.new(self)
    )
  end

Perhaps this will be built someday, but for now, I think it will do the trick.

If you want to try this, here is a gist with the code.