Skip to content

Instantly share code, notes, and snippets.

@Intrepidd
Last active April 7, 2024 07:42
Show Gist options
  • Save Intrepidd/ac68cb7dfd17d422374807efb6bf2f42 to your computer and use it in GitHub Desktop.
Save Intrepidd/ac68cb7dfd17d422374807efb6bf2f42 to your computer and use it in GitHub Desktop.
// DISCLAIMER : You can now probably use `data-turbo-action="advance"` on your frame to perform what this controller is aiming to do
// https://turbo.hotwired.dev/handbook/frames#promoting-a-frame-navigation-to-a-page-visit
// Note that you probably want to disable turbo cache as well for those page to make popstate work properly
import { navigator } from '@hotwired/turbo'
import { Controller } from '@hotwired/stimulus'
import { useMutation } from 'stimulus-use'
export default class extends Controller {
connect (): void {
useMutation(this, { attributes: true })
}
mutate (entries: MutationRecord[]): void {
entries.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
const src = this.element.getAttribute('src')
if (src != null) {
const url = new URL(src)
navigator.view.lastRenderedLocation = url
navigator.history.push(url)
}
}
})
}
}
@fffx
Copy link

fffx commented Jan 27, 2021

// js
import { Turbo } from 'turbo'
import { Controller } from "stimulus"

export default class extends Controller {
  connect() {
    this.observer = new MutationObserver((mutationsList) => {
      mutationsList.forEach((mutation) => {
        if (mutation.type === "attributes" && mutation.attributeName === "src") {
          history.pushState({ turbo_frame_history: true }, "", this.element.getAttribute("src"))
        }
      })
    })

    this.observer.observe(this.element, { attributes: true })

    this.popStateListener = (event) => {
      if (event.state.turbo_frame_history) {
        Turbo.visit(window.location.href, { action: 'replace' })
      }
    }

    window.addEventListener("popstate", this.popStateListener)
  }

  disconnect() {
    this.observer.disconnect()
    window.removeEventListener("popstate", this.popStateListener)
  }
}

@julianrubisch
Copy link

julianrubisch commented May 6, 2021

Here's a version using the ingenious stimulus-use package, reads a bit nicer:

import { Turbo } from "@hotwired/turbo-rails";
import { Controller } from "stimulus";
import { useMutation } from "stimulus-use";

export default class extends Controller {
  connect() {
    useMutation(this, { attributes: true, childList: true, subtree: true });
  }

  disconnect() {
    window.removeEventListener("popstate", this.popStateListener);
  }

  mutate(entries) {
    for (const mutation of entries) {
      if (mutation.type === "childList") {
        // re-initialize third party, e.g. jquery, plugins
      }

      if (mutation.type === "attributes" && mutation.attributeName === "src") {
        history.pushState(
          { turbo_frame_history: true },
          "",
          this.element.getAttribute("src")
        );
      }
    }

    this.popStateListener = event => {
      if (event.state.turbo_frame_history) {
        Turbo.visit(window.location.href, { action: "replace" });
      }
    };

    window.addEventListener("popstate", this.popStateListener);
  }
}

@Intrepidd
Copy link
Author

Actually here is my latest version using the newly available navigator API + stimulus use ;)

import { navigator } from '@hotwired/turbo'
import { Controller } from 'stimulus'
import { useMutation } from 'stimulus-use'

export default class extends Controller {
  connect (): void {
    useMutation(this, { attributes: true })
  }

  mutate (entries: MutationRecord[]): void {
    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)) }
      }
    })
  }
}

@julianrubisch
Copy link

cool! does navigator.history handle popstate for you, too?

@Intrepidd
Copy link
Author

Intrepidd commented May 6, 2021 via email

@julianrubisch
Copy link

julianrubisch commented May 6, 2021

awesome, thanks!

It's just a shame that stuff is documented so poorly :-(

@MikeDevresse
Copy link

Is there a way to get title in order to push it too ?

@Intrepidd
Copy link
Author

Is there a way to get title in order to push it too ?

I don't think so, the frame update would not include the title.

However you can probably have a stimulus controller that updates the page title on connect given a value, and push a HTML tag in the frame response that would instantiate such controller with a desired title value

@manunamz
Copy link

manunamz commented Jun 1, 2021

For anyone else trying to use stimulus-use without a build system, add StimulusUse before useMutation() and import navigator directly from cdn (but if there's a better way, please comment!). In the end the result will look like this (in javascript):

// js
import { navigator } from 'https://cdn.skypack.dev/@hotwired/turbo';

export default class extends Stimulus.Controller {
  // from: https://gist.github.com/Intrepidd/ac68cb7dfd17d422374807efb6bf2f42
  connect () {
    StimulusUse.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)) }
      }
    })
  }
}

(Stimulus.Controller is also used as stimulus is being imported via <script src="https://unpkg.com/stimulus/dist/stimulus.umd.js"></script>)

@julianrubisch
Copy link

@michelson
Copy link

@Intrepidd, thanks for this solution is precious; one thing I've found is that the forms with target _top do not honor the redirects.
How could this be accomplished?

@Intrepidd
Copy link
Author

@michelson I am not sure to follow, if you have a form with target _top and use my controller the URL won't change after the redirect ?

@michelson
Copy link

@Intrepidd, sorry, I've double-checked this, and it is working as expected.

@cc-ray-boutotte
Copy link

How does one use this stimulus controller? I added a data-controller attribute to frame, to div encompassing links that do not update the url, and still nothing. Cannot get history to change using this.

@Intrepidd
Copy link
Author

How does one use this stimulus controller? I added a data-controller attribute to frame, to div encompassing links that do not update the url, and still nothing. Cannot get history to change using this.

@cc-ray-boutotte the controller is to be attached to the frame like so :

<turbo-frame data-controller="turbo-frame-history" id="xxx">

@Selion05
Copy link

Does anyone else have duplicate entries? for now I "fixed" it with an extra check

import { navigator } from '@hotwired/turbo';
import { Controller } from '@hotwired/stimulus';
import { useMutation } from 'stimulus-use';

export default class extends Controller {
    connect() {
        useMutation(this, { attributes: true });
    }
    source = null;

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

@marcoroth
Copy link

@Selion05 do you have the controller multiple times on the same page? Or nested in each other?

@Selion05
Copy link

No I don't. Just double checked and created a random number which i log inside the controller... It is just one controller instance...

@Intrepidd
Copy link
Author

Intrepidd commented Sep 25, 2023

New iteration to prevent issues when fiddling with the history.

Issue was :

Frame visit A
Frame visit B
Frame visit C
Hit prev
Frame visit D
Hit prev

Nothing would happen

@michelson
Copy link

Hi @Intrepidd, is this solution still needed with the new additions to hotwire?

Thanks for the hard work!

@Intrepidd
Copy link
Author

Thanks for challenging the status quo, actually I just discovered data-turbo-action introduce in v 7.2.0 appears to do the trick : https://turbo.hotwired.dev/handbook/frames#promoting-a-frame-navigation-to-a-page-visit

@michelson
Copy link

Hi @Intrepidd , It seems that the turbo-action advance updates the URL but it refreshes the whole page.
Also, have you seen issues on the history back button on your solution?

@Intrepidd
Copy link
Author

@michelson I don't experience the refreshing of the whole page with data-turbo-action="advance", maybe the refresh is caused by something specific to your app ?

And yes, I've seen issues with my solution when playing back and forth with the history, I'm glad I was able to drop it for a more native solution :)

@michelson
Copy link

Hi @Intrepidd, not sure why I get that behavior, what I have is a link outside the frame that is visiting the frame, like:

<a data-turbo-frame="conversation" href="...">link</a>
<turbo_frame_tag "conversation" data-turbo-action="advance" src="...">...</>

Could happen that the browser refresh is due because the link is outside the frame?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment