Back button not working - duplicated history

I have a sidebar with some links that, when clicked, alter a different turbo frame, basically an SPA. To highlight the current page, I use a Stimulus controller that checks which URL is currently in window.location, and add the appropriate CSS to the right nav item:

_nav.html.erb

several links like this:
... 
      <%= spa_link_to podcasts_path, data: { navid: "podcasts" }, class: 'body_text_alternate' do %>
        <%= inline_svg 'icons/search.svg', class: 'icon icon_small', aria_hidden: 'true' %>
        Browse
      <% end %>

helper

  def spa_link_to(name = nil, options = nil, &block)
    default_data_attributes = { controller: "nav", action: "nav#navigate", turbo_frame: "main-app-frame" }
    options[:data] ||= {}
    options[:data].reverse_merge!(default_data_attributes)

    link_to(name, options, &block)
  end

nav_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  updateActiveLink() {
    // Deactivate all links
    document.querySelectorAll(`a[data-navid]`).forEach (link => {
      link.parentElement.classList.remove('active');
    });

    // Get the current URL
    const currentPath = window.location.pathname;
    const currentActiveNavid = currentPath == "/" ? "podcasts" : currentPath.split("/")[1]

    // Activate current open link
    const navEl = document.querySelector(`a[data-navid="${currentActiveNavid}"]`)
    navEl.parentElement.classList.add('active');
  }

  // Listen for click event on navigation links
  navigate(event) {
    // Get the href attribute of the clicked link
    const url = event.currentTarget.getAttribute('href');
    // Update the browser's URL
    history.pushState({}, '', url);
    Turbo.navigator.history.push(url);
    this.updateActiveLink();
  }

}

Then for example I’ll have app/views/podcasts/index.html.erb with:

<%= turbo_frame_tag "main-app-frame" do %>
  <main class="template_page">
    <header class="header_page">
      ...

So clicking on the sidebar link will navigate to the Podcasts index page, replacing the content of “main-app-frame” with podcasts/index.

This setup works fine and as you can see in the Stimulus controller, the browser URL is updated as well.

However, Browser back/forward buttons and the browser history behave oddly. Two entries for the same page are added (maybe because I’m calling history.pushState and Turbo.navigator.history.push? But I read you are supposed to call both), and navigating back only works after you pressed the Back button twice (probably same underlying reason).

I’ve tried all sorts of combinations of having both Turbo.navigator.history.push and history.pushState, either one, etc, nothing works and I’m pulling my hair out. Any ideas?

Isn’t this happening because turbo by default captures all links and updates the history on its own?
Meaning you are updating the history manually but so is turbo?

Have you tried adding:

navigate(event) {
  event.preventDefault()

  // ...

so you overwrite turbo’s default behavior on this specific link?

1 Like

Yep, that was more or less it. I also had to add an event listener for popstate to do a Turbo visit on the current path.

For anyone with the same issue, my working controller now looks like:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
     if (!this.constructor.popstateListenerAdded) {
      this.updateActiveLink();
      // Listen for popstate event
      window.addEventListener('popstate', (event) => {
        this.turboVisit(window.location.pathname);
      });
      this.constructor.popstateListenerAdded = true;
    }
  }
...

  // Listen for click event on navigation links
  navigate(event) {
    if (!event.target.dataset.allowDefault) {
      event.preventDefault();
    }

    const url = event.currentTarget.getAttribute('href') || event.target.dataset.redirectPath;

    if (!event.target.dataset.noHistory) {
      history.pushState({}, '', url);
    }

    this.turboVisit(url);
  }

  turboVisit(url) {
    this.updateActiveLink();
    this.updateWindowTitle(url.split("/")[1]);
    Turbo.visit(url, { frame: "main-app-frame", action: "advance" })
  }
....