Hotwire Discussion

TurboSteam and variants

Exploring Turbo and Hotwire, I came across a possible limitation of the broadcasting from the model approach. Maybe there is a solution, but here it goes:

Here’s the scenario. I have an boards/show.html.erb

<h1>Board <%= @board.name %> Desktop</h1>

And I have boards/show.html+phone.erb

<h1>Board <%= @board.name %> Mobile</h1>

When the board is updated, there is a broadcasts_to call.

broadcasts_to self

Simple.

If the user is looking on this page on the desktop and someone updates the board, TurboStream sent back via modal’s broadcast replaces content using the right template (show.html.erb).

If the user is looking on this page on the mobile and someone updates the board, TurboStream broadcast command has no way of knowing which template to send to the user so it sends the (show.html.erb) instead of show.html+phone.erb

If the update is triggered by an outside process, ie. background job, turbostream can send updates to connected clients, but I don’t see any option to force the correct variant.

Maybe Strada is the solution, but it’s not here yet.

One option is instead of sending back partials from models, TurboStream should send a request to self-update.

For example, if you’re looking at a page and receive a turbo_stream_refresh command, Turbo sends a fetch request to the current page in the background, gets the new html and replaces it.

Maybe TurboStream should work on this principle. Instead of pushing updates to clients it should just say “Hey, there’s an update to this page available, refresh it” or “Hey, there’s an update to a section of this page available, go and fetch it and refresh it”.

Hi there @tdak

here are my ideas, perhaps it may be helpful?:

Option 1:
One option would be to combine the two partials - so instead of having a show.html+phone.erb and a separate show.html.erb template - have only one: showing the mobile view or the desktop view accordingly. Now you can choose whether to actually display the updates to the user, depending on whether they are using a mobile - using CSS.

Option 2
The other option is to detect whether a mobile is being used on the rails backend and to update the relevant partial accordingly: mobile or desktop. From memory, there is a PR in the making to specify different partials to update, but not sure if this has been merged yet - so the logic as to which partial to update can be contained there and the below concern will more or less be solved:

Maybe TurboStream should work on this principle. Instead of pushing updates to clients it should just say “Hey, there’s an update to this page available, refresh it” or “Hey, there’s an update to a section of this page available, go and fetch it and refresh it”.

Thanks for your suggestions Ben,

I think right now option 1 is the workable solution.

For option 2, I don’t know if there is a way to tell from the backend if the client is on mobile or desktop. If there is that’s awesome.

For option 2, I don’t know if there is a way to tell from the backend if the client is on mobile or desktop. If there is that’s awesome.

Try this:

That works in the controller space. That’s what I already use to set the correct variant.

def detect_device_variant
    request.variant = :phone if browser.device.mobile?
  end

The question how do you detect when the model is saved by a background job for example. Or, if two people are looking on the same page, one via browser and one via mobile, broadcasts_to needs to know that somehow.

The values will have to be passed into as parameters somehow:

class BoardWorker
    def perform(mobile_flag)
       if mobile_flag
          # then respond accordingly
       end
    end
end

class BoardsController
    def action
       BoardWorker.perform( yes_we_are_using_mobile)
    end
end

A similar situation on the model – except I haven’t thought it too closely – perhaps consider your options carefully.

Yeah, that’s good work around for now. Not ideal though.

I would go with @BKSpurgeon Option 1 and keep one view and style using CSS.

1 Like

I do agree that keeping it to one view is best in this case, but wanted to show you how to go about passing data from the “controller space” into background jobs:

This is the example, which implements it:

  1. Create a device_variant in a Current model, i.e.
class Current < ActiveSupport::CurrentAttributes
  attribute :device_variant
end
  1. Set device_variant on the controller level in a before action
Current.device_variant = request.variant
  1. Add the current parameter to all jobs - see the #serialize from the example
    def serialize
      super.merge('device_variant' => Current.device_variant)
    end
  1. Finally, access device_variant as in the example code, or I think it should also be available directly in the job with self.arguments, but I haven’t tried it.

That’s brilliant, using Current.device_variant. Awesome suggestion.

It would work for controller → model process. Background jobs would have no way of knowing what devices are currently connected to the rails server. So solution one is probably best bet.

A less elegant solution would be to define the variant in the stream name:

<%= turbo_stream_from @board, request.variant %>
<%= turbo_frame_tag dom_id(@board) %>

then in your model you would broadcast different contents for each stream+variant:

after_commit :broadcast_update, on: :update

def broadcast_update
  broadcast_replace_later_to self, :mobile, variant: :mobile
  broadcast_replace_later_to self, :desktop
end

That’s brilliant. Actually that is the most elegant solution.

1 Like