Hotwire Discussion

Confusion how to implement url change with back/forward button functionality using turbo frames

I’m trying to use turbo frames for the full navigation of my Rails app.

#index.html.erb
<%= turbo_frame_tag "page_content" do %>
        <div class="flex flex-col items-center"> 
            <h1 class="text-3xl font-bold">Home</h1>
        </div>
        <%= link_to "Page 1", page_1_path %>
<% end %>

#page_1.html.erb
<%= turbo_frame_tag "page_content" do %>
        <div class="flex flex-col items-center"> 
            <h1 class="text-3xl font-bold">Page 1</h1>
        </div>
        <%= link_to "Page 2", page_2_path %>
<% end %>

#page_2.html.erb
<%= turbo_frame_tag "page_content" do %>
        <div class="flex flex-col items-center"> 
            <h1 class="text-3xl font-bold">Page 2</h1>
        </div>
<% end %>

The links are working as expected, replacing the contents of the turbo_frame with the new content from the corresponding pages, but I would like the URL to be updated and the back/forward buttons in the browser to function.

I’ve searched around, and come across this stimulus controller from turbo_frame_history_controller.ts · GitHub.

I’ve implemented it as so

#history_controller.js
import { navigator } from '@hotwired/turbo'
import { Controller } from 'stimulus'
import { useMutation } from 'stimulus-use'

export default class extends Controller {
  connect () {
    console.log("heyoo");
    useMutation(this, { attributes: true })
  }

  mutate (entries) {
    entries.forEach((mutation) => {
      if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
        const src = this.element.getAttribute('src')
        if (src != null) { navigator.history.push(new URL(src)) }
      }
    })
  }
}

I’m confused how this controller works though and if it is what I’m actually looking for. I have tried putting the controller in my app/javascript/controllers/history_controller.js and tried wrapping my turbo_frame_tag in index.html.erb in a

<div data-controller="history">

but the URL still doesn’t change and the back/forward buttons still don’t work. The console.log does log as expected though.

What am I missing?

Also, side note, I’m getting this when webpacker compiles the above stimulus controller

[Webpacker] Though the "loose" option was set to "false" in your @babel/preset-env config, it will not be used for @babel/plugin-proposal-private-property-in-object since the "loose" mode option was set to "true" for @babel/plugin-proposal-class-properties.
The "loose" option must be the same for @babel/plugin-proposal-class-properties, @babel/plugin-proposal-private-methods and @babel/plugin-proposal-private-property-in-object (when they are enabled): you can silence this warning by explicitly adding
	["@babel/plugin-proposal-private-property-in-object", { "loose": true }]
to the "plugins" section of your Babel config.

Not sure what to make of it.

Thank you for any suggestions!

Can you try to connect the historyController on the turbo_frame_tag like so:

<%= turbo_frame_tag "page_content", data: { controller: 'history' } do %>
[...]
<% end %>

OR

change these lines in your history_controller.js:

[...]
useMutation(this, { attributes: true, subtree: true });
[...]
const src = mutation.target.getAttribute("src");
[...]

Thank You! Connecting my historyController to the turbo_frame on index.html.erb has gotten me half way to what Im wanting. There is some strange behavior I’m noticing now though with the back button.

If click the link to go to page_1.html.erb, the URL in the browser is updated correctly, but the when I press the back button nothing happens. When I press it again I go to back beyond my app pages if that makes sense (such as Google if that was the site I was previously visiting).

What I do notice happening is the turbo_frame src attribute gets added with the URL to page_1.
Then if I click the link to page_2 the src get updated to that URL.

Pressing the back button from page_2, after navigating there from page_1, does go back to page_1 but it loads the entire page instead of replacing the turbo frame.
Is this the intended behavior?
I feel like its suppose to just be doing the reverse of clicking a link.

Any suggestions about the above?
Thanks again!

What backend are you using?

I’m using Rails with the Hotwire-rails gem

When clicking an element inside of a turbo-frame, turbo knows to add the Turbo-Frame: [id] header to the request. However, executing “back” outside the turbo-frame, the application has no state of the turbo-frame and therefore does not send a Turbo-Frame: [id]. Turbo library would likely need to store additional information in the history object (which has been discussed).

Alternatively, I believe you could use turbo-streams instead to accomplish what you want. The only downside being that turbo-streams requires javascript (where with turbo-frames, if browser does not have javascript then some of the content can still render).