Hotwire Discussion

Turbo Frame doesn't seem to show

I truly am trying to get my head around this conceptually. Every time I think I know what I’m doing with Rails I hit a wall (very hard) and then feel rather stupid. I’m trying to understand Turbo Frames. I thought that these were basically sections of the webpage which could be updated independently from the rest of the page. A button click would lead to the server sending some “updated” code which turbo would see the frame and update it accordingly. This is what I thought I was doing … but apparently I’ve missed something.

I have a show.html.erb page with a turbo frame in it. It shows an image of a hamper.

<div class="w-full">
  <%= turbo_frame_tag "hamperimage" do %>
    <%= render partial: 'hamper_image', locals: {hamperphoto: @hamperphoto} %>
  <% end %>
</div>

Later in the same page I have a list of options which trigger a change of the picture (and other parts of the page but I’m trying to get this to work first!). This used to work with a link_to, a remote:true and some javascript. I have removed the “remote: true” and changed it to a button_to (following a tutorial which said to do this!). I’ve removed the javascript. Here is the code which should action a change:

 <% @hamper.hamper_options.order(option_id: :asc).each do |hamperoption| %>
  <%= button_to change_option_hamper_path(option: hamperoption.option.name), method: :post do %>
    <% if hamperoption&.image_url(:small)  %>
      <%= image_tag hamperoption.image_url(:small), class: "w-24 mr-5" %>
    <% else %>
      <%= image_tag "processingimage.png", class: "w-24" %>
    <% end %>
  <% end %>
<% end %>

When I click the image expecting a change, it triggers the action:

  def change_option
    @hamper = Hamper.friendly.find(params[:id])
    @option = Product.where(name: params[:option]).first
    @option ||= ''
    cookies.signed[:htviewing] = JSON.generate({ data: { hamper: @hamper.id, option: @option.id.to_s } })
    @saleprice = @hamper.saleprice + @option.sale_price
    @optionid = @option.id
    @hamperphoto = HamperOption.where(hamper: @hamper, option: @option).first
    render :layout => false
  end

Which renders the change_option.html.erb file:

<%= turbo_frame_tag "hamperimage" do %>
  <%= render partial: 'hamper_image', locals: {hamperphoto: @hamperphoto} %>
<% end %>

Which … when I look at the network response delivers the following:

<turbo-frame id="hamperimage">
    <img class="object-contain w-full px-6" src="https://hampertree.nyc3.digitaloceanspaces.com/78609b009426d0845d1c0efd2b23af37.jpg" />
</turbo-frame>

So … as far as I understand things … it should now replace the turbo-frame with the new content? Is that correct? I am not using turbo streams (yet) … as I thought it was the case that Turbo would look for the turbo frame and replace it automatically.

The status code is coming back as 200 OK.

However, nothing is changing on the page.

I am obviously doing something really stupid - but can’t work out where my conceptual understanding is going wrong.

Thanks in advance

PS. Please break it down into basics!

Your button_to must be inside the turbo-frame tag OR you need to explicitly tell which turbo-frame that button should affect with the attribute data-turbo-frame

Thanks for your reply! But I tried that too …

<%= button_to change_option_hamper_path(option: option.name), method: :post, class: "text-black hover:text-black text-left", "data-turbo-frame": "hamperimage" do %>

Same outcome … nothing seems to be updating.

Here is the HTML which is being rendered:

<form class="button_to" method="post" action="/hampers/christmas-epicurean-delight-with-posh-plonk/change_option?option=Vintage+Cuv%C3%A9e+Sparkling"><button class="text-black hover:text-black text-left" data-turbo-frame="hamperimage" type="submit">
      <div class="border-2 my-2 rounded border-blue-500">
        <div class="bg-white px-2 py-4 text-xs">
          <div class="flex flex-row">
            <div class="w-full flex flex-col text-xs">
              <div class="font-bold">Vintage Cuvée Sparkling by Posh Plonk</div>
              <div class="mt-2 mx-5 italic">A classic méthode traditionnelle sparkling, blended from inspiring and diverse cool climate vineyards, Posh Plonk Vintage Cuvee embodies the eleg ... </div>
            </div>
          </div>
        </div>
      </div>
</button><input type="hidden" name="authenticity_token" value="XXXXXX"></form>

The data-turbo-frame attribute should be on the form tag!

Here are the examples in the manual: Turbo Handbook: Decompose with Turbo Frames

But that’s the interesting problem and the thing I don’t get … This is the code I have in my show.html.erb:

<div class="flex flex-col text-xs py-2">
  <% hamper.options.order(id: :asc).each do |option| %>
    <%= button_to change_option_hamper_path(option: option.name), method: :post, class: "text-black hover:text-black text-left", "data-turbo-frame": "hamperimage" do %>
      <% border = (option.id == optionid) ? "border-blue-500" : "" %>
      <div class="border-2 my-2 rounded <%= border %>">
        <div class="bg-white px-2 py-4 text-xs">
          <div class="flex flex-row">
            <div class="w-full flex flex-col text-xs">
              <div class="font-bold"><%= "#{option.name}" %> by <%= "#{option.manufacturer.titlecase}" %></div>
              <div class="mt-2 mx-5 italic"><%= "#{truncate(option.description, length: 150, omission: ' ... ') }" %></div>
            </div>
          </div>
        </div>
      </div>
    <% end %>
  <% end %>
</div>

There is no form tag. Whatever magic is taking place, is turning the button_to into a form. So … how do I get the form to have the data-turbo-frame when I’m not creating the form?

Sorry if this is a stupid question …

Unless you mean I need to turn all the button_to into form tags. They used to be link_to’s but a tutorial said to turn into button_to’s

Ah, but there’s an option for button_to to achieve that! See this example:

<%= button_to 'Link to your account',
       admin_dropbox_connect_path,
       class:'btn btn-primary',
       form: { data: { turbo: false } } %>

Thanks for helping. Sadly, this still isn’t updating the image.

I’ve changed the button_to which becomes:

<%= button_to change_option_hamper_path(option: option.name), method: :post, class: "text-black hover:text-black text-left", form: {data: {turbo: {frame: "hamperimage"}}} do %>

I’m hoping I’ve interpreted your example correctly!

This made the form tag the following:

<form data-turbo="{&quot;frame&quot;:&quot;hamperimage&quot;}" class="button_to" method="post" action="/hampers/christmas-epicurean-delight-with-posh-plonk/change_option?option=Vintage+Cuv%C3%A9e+Sparkling"><button class="text-black hover:text-black text-left" type="submit">

I’m sorry, I posted that but not quite awake yet. The principle is the same: button_to has a form parameter to be able to insert attributes on the form tag.

However, I guess it doesn’t support nesting the parameters that far. You should be able to use this:

form: { data: {'turbo-frame': 'hamperimage'} }

You are a legend! That’s worked a treat and the image has updated.

So … now the next problem (which relates to this one) if you have time to help again …

I have three seperate turbo-frames on the page which, when I click the button_to it should update all frames [Title of the page changes, the image changes and the option which was clicked should have a blue border around it].

When I used change_option.js.erb, this was simply a case of updating the divs using javascript.

How can I achieve the same effect and get the change_option method to change the contents of three seperate turbo frames? Or am I straying into the land of turbo streams now?

I’m afraid that’s going to need one of these three options:

  • turbo streams
  • updating a larger part of the page at once
  • using JS for some parts (such as the blue border effect)

If you search “multiple update” in this forum there will be at least two posts that discuss the various options :slight_smile:

Thanks so much once again. I have a feeling I’m going to be best to refactor the part of the page I want to update to make it all one turbo frame and then cope with it that way. I’m not brave enough to do streams at the moment! I couldn’t even get the button_to to work lol.

Just for interest, how do you know to add the form tag to the button? The tutorials I have watched didn’t make this at all clear … but then I think they were updating from inside the form tag.

UPDATE: Also … I thought it wasn’t supposed to send the layout when it’s a turbo call. The response from change_option seems to be sending the whole layout too. Is there something I need to do to change this?

If they were putting a form in a frame, you’re right. That’s going to be automatically catched and is a bit simpler than what you wanted to achieve. The Turbo handbook has a single example about a form that targets a frame on a different part of the page. I believe there was an idea of collecting a lot of small use cases somewhere. That might be useful to help coming up with the “right” ideas.

You’re right that Rails shouldn’t be sending a layout when handling a frame request. However, if you’re explicitly specifying a layout in your controller or action, it will be used regardless. See Defined controller layout always gets used · Issue #60 · hotwired/turbo-rails · GitHub

Thanks once again. A collection of small use cases would be very helpful.

I’m not adding a layout. It’s using the standard application.html.erb from the layouts folder.