How to wrap a table row with a turbo frame?

Given these views:

# index.html.eb
<tbody>
  <%= render @exercises %>
</tbody>

and

# _exercise.html.erb
<%= turbo_frame_tag dom_id(exercise) do %>
  <tr>
    <td><%= exercise.name %></td>
    <td><%= exercise.description %></td>
    <td><%= link_to exercise.video_url, exercise.video_url %></td>
    <td><%= link_to 'Show', exercise %></td>
    <td><%= link_to 'Edit', edit_exercise_path(exercise) %></td>
    <td><%= link_to 'Destroy', exercise, method: :delete, data: { confirm: 'Are you sure?' } %></td>
  </tr>
<% end %>

I get some funny markup. If I use ā€œView Sourceā€ in Firefox, the markup seems ok (the table row is inside the turbo frame). However, when using the web inspector, I see that the turbo frames actually appear above the table:

How can I fix this?

2 Likes

Browsers re-organize the HTML they are given into a DOM that follows their interpretation of the rules of layout. Since Turbo uses a made-up tag that does not adhere to the rules of what tags may be children of a tag, that made-up tag gets sorted out of the hierarchy. You will find that you can put a around the entire , and that will not get re-organized, but just like if you tried to put a

inside the
, anything that cannot (legally) be a child of the will be moved within the DOM. All browsers do this, not just Firefox. All browsers are free to interpret what the correct re-organization scheme is, though, so you may see some put the foreign tags after the table rather than before it.

Is the solution you coded working for your purposes, philosophical purity of markup aside?

Or is the updated content being rendered above your table when Turbo re-builds the page?

Walter

1 Like

Ugh, sorry, this got all stripped out because I replied by e-mail. Hereā€™s what I wrote, without angle brackets and capitalized to give it the same meaning:

Browsers re-organize the HTML they are given into a DOM that follows their interpretation of the rules of layout. Since Turbo uses a made-up TURBO-FRAME tag that does not adhere to the rules of what tags may be children of a TBODY tag, that made-up tag gets sorted out of the hierarchy. You will find that you can put a TURBO-FRAME around the entire TABLE, and that will not get re-organized, but just like if you tried to put a DIV inside the TBODY, anything that cannot (legally) be a child of the TBODY will be moved elsewhere within the DOM. All browsers do this, not just Firefox. All browsers are free to interpret what the correct re-organization scheme is, though, so you may see some put the foreign tags after the table rather than before it.
Is the solution you coded working for your purposes, philosophical purity of markup aside?

Or is the updated content being rendered above your table when Turbo re-builds the page?

Walter

2 Likes

Hey @walterdavis, thank you very much for your answer, it does make things clearer for me. The problem for me is that in this case Turbo does not work at all. What Iā€™m trying to do is that when clicking on edit, I get the edit form of the given recorded rendered in place of the row. However, as the edit button is in this case outside of the turbo frame, this does not work. Right now, Iā€™ve fallen back to not using a table, but divs. Once again, thank you for your answer.

You can try setting turbo-frame on content inside individual <td>s

  <tr>
    <td><turbo-frame id="name-<%= exercise.id  %>"><%= exercise.name %></turbo-frame></td>
    <td><turbo-frame id="description-<%= exercise.id  %>"><%= exercise.description %></turbo-frame></td>
  </tr>
2 Likes

Hey @dylan, how would that help me, though? Iā€™d like, that when I click the Edit button, the whole row is replaced with an edit form.

I had the same issue. Indeed table elements are very strict and can only accept a set of element.

The solution I decided to go with and that I suggest is to stop using tables and use div instead, you can then use CSS to style it just like a table with display: table, display: table-row, display: table-cell etc.

If you use a utility CSS framework like tailwind itā€™s very simple.

8 Likes

I ended up doing this and it solves the problem.

Add an id to tbody:

    # index.html.eb
    <tbody id="exercises">
         <%= render @exercises %>
    </tbody>

Make some changes to # _exercise.html.erb

<%= content_tag :tr, id: dom_id(exercise) do %>
        <td><%= exercise.name %></td>
        <td><%= exercise.description %></td>
        <td><%= link_to exercise.video_url, exercise.video_url %></td>
        <td><%= link_to 'Show', exercise %></td>
        <td><%= link_to 'Edit', edit_exercise_path(exercise) %></td>
        <td><%= link_to 'Destroy', exercise, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>

Create a new file # views/exercises/create.turbo_stream.erb

<% turbo_stream.append 'exercises', @exercise %>

It should now working.

2 Likes

There is a closed issue on github about this problem. (without solution :frowning:)

1 Like

Iā€™ll second @Intrepidd - stop using HTML Table and start using CSS Table. Just had my own tour of the trenches over this issue - I did a small post on the issue - feel free to comment if Iā€™m totally in the woods! Tailwind, Hotwire, more

Yep itā€™s a good point. But for now I tied with bootstrap responsive tables.

@vincentlee has the right solution for this when using tables.

1 Like

@vincentlee certainly has a point - if ā€˜turbo-chargingā€™ HTML Tables was/is the only issue to deal with but IMHO we also have to finally start making ā€œtablesā€ responsive - perhaps peruse my second installment on the topic

I did some research and played with this a bit last week. Itā€™s not impossible for Turbo to make this work, but I think it would take support from the core team.

The <turbo-frame> custom html tag is an ā€œAutonomous custom elementā€. It stands alone.

But, there are also ā€œCustomized built-in elements.ā€ For example, defining:

customElements.define('turbo-frame-row', TableRowFrameElement, { extends: 'tr' });

and implementing:

HTMLTableRowElement

class TableRowFrameElement extends HTMLTableRowElement

which adds the same logic in the existing FrameElement class which backs our turbo frame tag.

Then in your tables you would use:

<tbody>
  <tr is="turbo-frame-row">...</tr>
</tbody>

and Turbo would need to look for these in addition.

Iā€™m just guessing that they donā€™t want to add this complexity if you can use CSS display: table stylingā€¦

But Iā€™m also not a fan of html documents becoming a steaming pile of divs.
Tables are tables, screen readers know that too.

4 Likes

I had the same problem and played around for hours until even REALIZING the problem why my <td> tags were stripped. Although it makes sense from a rendering perspective, this behaviour is totally unexpected when trying turbo for the first time.

I hope you will consider @leehericks suggestion about using Customized built-in elements. Tables are really important, not just for screen readers. There are many nice javascript frameworks for tables, such as Data Tables that make tables work really well, but do not work with divs.

How would use a form as table-row though? (Use case: replace row with data with Edit form)

= turbo_frame_tag landing_page, id: dom_id(landing_page_product), class: "table-row" do
     = form_with... do |f|
    .table-cell
      f.number_field...
    .table-cell
      f.submit 'save'

This does not work: the form breaks the table format and only fills the first table-cell of the other table-rows which are not forms.

I just ran into this. But my solution was to just move the <turbo-frame> to wrap <table> instead.

To answer the OPā€™s question directly: just move the <turbo-frame> to wrap the whole table instead of a row, this is necessary due to HTML rules of layout.

If we think about it, the <turbo-frame> does not have to be right beside the loop, inside <tbody>. We could wrap the whole page in a <turbo-frame> and things would still work, but that is inefficient.

In my view, <turbo-stream> is not absolutely necessary here. Just sharing.

1 Like

I work in an area where HUGE tables are just part of daily life. They necessitate progressive loading. My existing solution is to immediately display a modest number of rows, then load some more via a partial that calls itself until I run out of data. Hereā€™s an example:

Loading the whole table via a single Turbo Frame wonā€™t help. I suppose I could load the initial 100 rows and do a single replacement of the entire 14,000 row table when itā€™s done loading but that could be a 5 second delay where currently there is functionally none.

Every example of Turbo Stream Iā€™ve seen seems to be used for synchronizing state. Could I instead use it to blast row after row in order from within the initial index action? Iā€™d love to avoid making 15 distinct server requests to load a table.

I strongly believe that you could :+1:

This should do it - more or less

# layouts/application.html.erb
...
<%= yield  %>
# controllers/messages_controller.rb
  def index
    @messages = Message.get_them_by_the_thousands do |row|
      render turbo_stream: turbo_stream.prepend "messages", target: "messages_tbody", partial: "messages/row", locals: {message: row}
    end
  end
<!-- messages/index.html.erb -->
    <%= turbo_stream_from "messages"  %>
    <%= render partial: "index" %>
<!-- messages/_index.html.erb -->
  <%= turbo_frame_tag "messages" do %>
    <table>
      <tbody 
        id="messages_tbody">
        <% messages.each do |message| %>
          <%= render partial: "messages/row", locals: { message: row } %>
        <% end %>
      </tbody>
    </table>
  <% end %>
<!-- messages/_row.html.erb -->
<tr id="<%= dom_id(message) %>">
  <td>message attributes</td>
</tr>
<!-- models/message.rb -->
class Message < AbstractResource
  after_update_commit :broadcast_update

  def broadcast_update
    broadcast_replace_later_to "messages",
      target: "messages_list", 
      partial: "messages/row", 
      locals: { message: self }
  end

end
2 Likes