How to open a modal, update the URL and then close the modal and update the URL

I have an index action list view and a show action that opens a modal over the top of the list in a turbo frame. When the modal opens, the URL changes to /path/:id and when it is closed it goes back to /path. I also need be able to go straight to /path/:id and the modal open, and then be able to close it and go to /path.

Does anyone have any suggestions for how to implement this?

I currently have:

  • each list item with a link to the show path annotated with data: {turbo_frame: :modal, turbo_action: :advance} so the modal loads into the modal turbo frame and the URL updates accordingly.
  • the close modal link that goes to the index path annotated with data: {turbo_frame: "_self", turbo_action: :advance}. This feels counter intuitive because it replaces the modal with the whole response from the index path - which contains an empty turbo stream as well as all the list data which we don’t use from the response.

Is there a simpler way of doing this?

You can use js to hook into turbo and do the url changes manually.

Let’s say you have a modal_controller.js, and you have show() and hide() methods - like the one from tailwind stimulus components.

Then

hide = () => {
  // ...

  this.restorePreviousUrl()
  // ...
}

show = (event) => {
  // ...
   const link = event.currentTarget // or similar
   this.changeUrl(link.href)
  // ...
}
import { navigator, session } from '@hotwired/turbo'

...

  changeUrl(url) {
    this.currentUrl = new URL(url)
    navigator.history.push(new URL(url))
    session.view.lastRenderedLocation = new URL(url)
  }

  restorePreviousUrl() {
    if (!this.currentUrl) {
      return
    }

    if (this.currentUrl.href == new URL(window.location).href) {
      // Pause history navigation listening, so we can
      // manually pop a url from the stack without triggering
      // a turbo reload
      navigator.history.stop()
      history.back()

      // as back() is an async method, we need to wait a bit before
      // starting Turbo history again
      window.setTimeout(() => {
        navigator.history.start()
      }, 80)
    }
  }

Let me know how it goes

We have something similar setup. Take the parts you like

  • index page with turbo modal links to display the show page of each resource in the modal (we don’t advance the URL)
  • the “close” of the modal redirects to the index page while breaking out of the turbo frame, via a query param (eg ?full_page_load=true) picked up by the index page, and when present, will use the turbo_page_requires_reload helper to cause the turbo-visit-control tag to be set to reload.
  • if the resource show URL is actually entered, we display the turbo modal contents on the page (not in a turbo modal). This is helpful for automated tests, since can go directly to the page (the modal aspect isn’t important for many of the operations on that modal)
  • if the index page URL is entered with a query param (eg, ?show=123):
    • the controller sets the @modal instance variable with the modal view component instance, containing the show page for resource 123
    • the layout, wherein is the top-level body element for the modal, renders the @modal (when present)