hotwired / turbo

The speed of a single-page web application without having to write any JavaScript
https://turbo.hotwired.dev
MIT License
6.74k stars 430 forks source link

Turbo frame history not navigating properly w/ back button after upgrading from 7 to 8 #1241

Open charlesobrien opened 7 months ago

charlesobrien commented 7 months ago

Context:

Application has a simple live search via form:

What's happening: Live search is working fine. When the form is submitted (automatically as a user types), the turbo frame is updated with the correct results and the URL displays the correct query parameters. However, when navigating with the back button the turbo frame does not navigate backwards, it only shows the most recent set of results (despite the url and associated query params appropriately changing backwards).

What should be happening: As the user navigates backwards the URL query params should change (which is happening) and the turbo frame should reflect the history from that point in time.

When did this start happening: This happened on Turbo 8.0.4. and Turbo Rails 2. If I revert to Turbo < 8 and Turbo Rails < 2, the behavior works as expected.

deluxetom commented 7 months ago

I'm experiencing the same issue, after the upgrade, a link inside a turbo frame is updating the URL correctly but when I use the back button, it only shows the current snapshot. The weird part is that its only for the first link inside the turbo frame, if I click on another link afterwards, the history and the snapshots seem to work as expected

tleish commented 5 months ago

I tried reproducing and could not. Is there a simple code example that reproduces this?

charlesobrien commented 5 months ago

I tried reproducing and could not. Is there a simple code example that reproduces this?

Hi @tleish! Sorry for the delay. I should have time this weekend to make a simple app to demo the issue.

pjg commented 5 months ago

I've run into this as well. A simple html page with turbo_frame and a link can trigger this.

Controller action index and corresponding index.html.erb:

<%= turbo_frame_tag 'page' do %>
  <%= render 'users', users: @users %>

  <p>
    <%= link_to_prev_page @users, '< Previous Page', data: { turbo_frame: 'page', turbo_action: 'advance' } %>
    <%= link_to_next_page @users, 'Next Page >', data: { turbo_frame: 'page', turbo_action: 'advance' } %>
  </p>
<% end %>

(prev/next links are from kaminari).

Loading index, clicking "Next", then Browser's back button triggers this issue. The page content is not being refreshed.

deluxetom commented 5 months ago

I just tested again and like @pjg, when I use:

Turbo.visit(url.toString(), {frame: 'search-results', action: 'advance'});

The URL is updated but when I hit the back button, I don't see the previous page. it only happens with the initial call, if I use Turbo.visit a second time and hit the back button, it works as expected.

tleish commented 5 months ago

Ok, I was able to reproduce this by moving the links outside the turbo-frame, where it did work where links were within the turbo-frame.

tpaulshippy commented 5 months ago

I am not able reproduce this, even by moving the links outside the frame. I was looking primarily at this page after running npm start

Does this only happen in rails? Do the URLs have to have query strings? Trying to figure out how you break this.

tleish commented 5 months ago

Tried my local test again. Realized it had nothing to do with links inside or outside the turbo-frame, but rather IF the content inside the turbo-frame had any HTML, which I don't necessarily think is a bug.

For example:

<turbo-frame id="results">
 Does not cache and show on Restoration Visit
</turbo-frame>
<turbo-frame id="results">
  <div>Caches and shows on Restoration Visit</div>
</turbo-frame>

So, I'm still unable to reproduce the original issue.

tpaulshippy commented 5 months ago

Interesting. That sounds like a different bug. I wonder why the HTML tags affect the behavior.

tpaulshippy commented 5 months ago

I'm not able to reproduce that issue either @tleish

Got it. If the original frame content has just plain text, the back button will not restore it. Weird. Are you going to open another issue for that one? @tleish

tleish commented 5 months ago

I'm not sure the turbo-frame with text only not rendering on restoration visits is a bug. I believe turbo uses cloneNode to cache html. If the frame has no nodes to cache (text only), then it doesn't cache it.

tleish commented 5 months ago

FYI, one (failed) attempt to reproduce the issue:

https://github.com/tleish/turbo-frame-history

Perhaps others can use this as a starting point to reproduce the issue.

tleish commented 5 months ago

Note: I tested this on MacOS Chrome, Firefox and Safari. Perhaps it's a specific OS and/or browser?

pjg commented 5 months ago

I've created a minimal reproducible testcase. I'm using Rails. Here's the repo: https://github.com/pjg/turbo-frame-bug

Steps to reproduce:

bundle install
bin/rails server

Navigate to http://localhost:3000

Click "Next page".

Click Back browser button.

On my end (MacOS; Chrome, Safari, Firefox) when I click Back page content doesn't refresh. It stays the same, while it should display the previous page.

NOTE: It starts working again if I click Next Page/Previous Page a couple of times.

tpaulshippy commented 5 months ago

Here's a more minimal way to reproduce (outside of rails):

  1. Create a folder.
  2. Drop the turbo.es2017-umd.js file into it.
  3. Create the following two html files:

    firstpage.html

    <!DOCTYPE html>
    <html>
    <head>
    <title>Frame advance bug</title>
    <script data-turbo-track="reload">
    </script>
    <script src="turbo.es2017-umd.js"></script>
    </head>
    
    <body>
    <h1>My Page</h1>
    
    <turbo-frame id="page">
      <p>First page</p>
    </turbo-frame>
    <p>
      <a
        data-turbo-frame="page"
        data-turbo-action="advance"
        href="/nextpage.html"
        >Next page</a
      >
    </p>
    </body>
    </html>

nextpage.html

<turbo-frame id="page">
    <p>Next page</p>
</turbo-frame>

The issue seems to be the presence of the data-turbo-track="reload" attribute on any valid tag in the HEAD. If you remove that attribute, the issue goes away. But if you put it on any tag (I tried it on <title>, <meta>, and <script>), you see the issue.

pjg commented 5 months ago

The issue seems to be the presence of the data-turbo-track="reload" attribute on any valid tag in the HEAD. If you remove that attribute, the issue goes away.

I do not find it to be true. In my regular app I'm running with data-turbo-track: '' (empty string). If I remove data-turbo-track from my minimal Rails example app, I can still reproduce the issue.

tpaulshippy commented 5 months ago

Have you viewed source on the page and confirmed that there is no data-turbo-track="reload" anywhere in your HEAD? I ask because I'm not sure it's easily removed from a boilerplate rails app.

pjg commented 5 months ago

You're right. Rails adds data-turbo-track: "reload" for importmaps.

The source is here: https://github.com/rails/importmap-rails/blob/0a6330133b75a72d7c5d07fe87c2fce50d76b997/app/helpers/importmap/importmap_tags_helper.rb#L15 -- I don't think it can be "turned off".

tpaulshippy commented 5 months ago

Yep. That's the issue. Well, the issue is in Turbo but yeah that's why you can't workaround the issue easily.

tleish commented 5 months ago

@tpaulshippy - I think you found the cause. Testing out @pjg github project, I saw it reproduced. The original page requesting includes several elements with data-turbo-track, but the HTML from turbo-frame response has an empty <head>. This is because the turbo-rails gem includes the following logic:

app/controllers/turbo/frames/frame_request.rb

# turbo-rails-2.0.5/app/controllers/turbo/frames/frame_request.rb
# The layout used is <tt>turbo_rails/frame.html.erb</tt>. If there's a need to customize this layout, an application can
# supply its own (such as <tt>app/views/layouts/turbo_rails/frame.html.erb</tt>) which will be used instead.

layout -> { "turbo_rails/frame" if turbo_frame_request? }

From the above comment you can replace the minimal app/views/layouts/turbo_rails/frame.html.erb with your own. Testing this out I addedapp/views/layouts/turbo_rails/frame.html.erb to the project with tags that include data-turbo-track (javascript_importmap_tags adds it). Note that I ignore title, meta and other tags that do not include data-turbo-track.

<html>
<head>
  <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
  <%= javascript_importmap_tags %>
</head>
<body>
<%= yield %>
</body>
</html>

After doing this workaround, turbo-frame Restoration Visits (back) work as expected.

I don't see this as the "solution", just a temporary workaround until the actual problem is fixed.

jonh-favo commented 4 months ago

Will this also make sure to trigger a reload of the page if there has been a new deploy and the asset files has changed? If so, it maybe a smart thing to do anyway?

tpaulshippy commented 4 months ago

Yes @jonh-favo

Looks like this issue started to break here: https://github.com/hotwired/turbo/commit/ed72ba16bc23dcccd88e0ab76ba72cfef82cd599

It doesn't seem like the frame should be required to have all the same tags (with turbo-track=reload) in head as the hosting page for the back button to work. But that seems to be what's happening.

jonh-favo commented 4 months ago

I discovered that I also need to add all meta tags that configures turbo in that turbo_rails/frame.html.erb layout, and I needed to add csrf_meta_tag. Otherwise all requests made by turbo afterwards were missing the the x-csrf-token header and reverted to default turbo behavior.

For instance I've disabled the prefetch functionality in the application.html.erb layout, but after a single turbo request that updated a frame it was suddenly enabled again.

kenkantzer-truss commented 4 months ago

I also ran into this just today, can confirm @tleish's workaround fixes it.

kenkantzer-truss commented 4 months ago

Just noting this for @tleish, primarily in case it's relevant to the fix in the merged PR:

I discovered that the above workaround breaks pages that try to force reload a new page via <% turbo_page_requires_reload %> or <meta "turbo-visit-control", content: "reload"</meta>.

It looks like adding <%= yield :head %> to the workaround fixes it, but I don't know turbo well enough to be confindent:

<html>
  <head>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag "application", "custom", "actiontext", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
    <%= yield :head %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>
cavenk commented 2 months ago

Hi, I have tried doing the solution above by replacing the frame.html.erd but it's still not working.

When clicking the back button with or without this solution, the back button seems to work 1 time out of 10 but I am not sure what I did different to make it work. Also I noticed that when it works, sometimes its getting the page from the cache but sometimes its doing a full page reload instead.

Did someone still has the same problem?

kenkantzer-truss commented 2 months ago

hey @cavenk, the above workarounds should work - for turbo 8, as a recap, the full workaround I'm currently using in production for the past 6 weeks or so is:

in app/views/layouts/turbo_rails/frame.html.erb:

<html>
  <head>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag "application", "custom", "actiontext", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
    <%= yield :head %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

ymmv - but the problem you're mentioned (1/10 times it works) does sound similar to what I was experiencing - the bacl bittpm would sometimes be successful depending on if a turbo frame action had previously triggered or not.

fonji commented 1 week ago

Hi, I also run into this problem and tried the layout patch without success. It always fails if I try to go back to the very first page.

If I do a single navigation, the back button does nothing (except breaking things mostly because of outdated CSRF). If I navigate twice, then the back button works once.