hotwired / turbo

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

How to stop Turbo (Drive) #198

Open johnmaxwell opened 3 years ago

johnmaxwell commented 3 years ago

We have a Turbolinks Rails application that we are looking to upgrade to Turbo. There are a couple of places in this application where we stop Turbolinks to subsequent Turbolinks navigation because other script/integration that we have on that page is incompatible with continued Turbolinks operation.

One example is Stripe Elements, that seems like it can only invoked once per Turbolinks page load. Once we hit a page that loads Stripe, we turn off Turbolinks using Turbolinks.controller.stop(). I previously documented our workaround in a comment of this Turbolinks Github issue that: https://github.com/turbolinks/turbolinks/issues/321#issuecomment-536102851

Currently Turbo does not export stop() or the session that has the stop(). Is there another way to get at this that I'm overlooking? If not, could stop() or session be exported to give us more control over operating Turbo in the world of complex javascript integrations?

seanpdoyle commented 3 years ago

@johnmaxwell would setting html[data-turbo="false"] one those pages, then removing the attribute afterwards suffice?

johnmaxwell commented 3 years ago

Thank you for your prompt response, @seanpdoyle ! I've had some time to look into your suggestion, and I apologize for my delay getting back to you.

Your suggestion is a good one for stopping forward navigation (link-clicks) from that page from taking place with Turbo, but it doesn't stop the history navigation (back/forward) from taking place via Turbo.

To review: The issue seems to be that once Stripe Elements is loaded on a page, it starts some script on the page and adds elements to the DOM that the script expects to be there. When the body is replaced by normal Turbo(links) operation, the elements disappear and the script emits an JS console error. So the "solution" to avoid the error is: allow Stripe to load via Turbo(links) but ensure that any subsequent navigation cold boots the page.

For example, if I'm on the cart page of a shopping site, I can click "checkout" to move to the checkout page, which loads Stripe to collect payment detail. If I then click the back button to go back to the cart, that back navigation move that will happen via Turbo instead of a cold load like we want. Shortly thereafter we see the error that we are trying to avoid appear in the JavaScript console:

Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('https://js.stripe.com') does not match the recipient window's origin ('http://localhost:3000').

I tried to cover this detail here in my comment https://github.com/turbolinks/turbolinks/issues/321#issuecomment-536102851 Under Turbolinks my solution was to stop() Turbolinks.controller and set a popstate listener that ensures that that any history navigation event will be a cold load. Turbo seems to have removed access to a stop method. It's there but not exported: https://github.com/hotwired/turbo/blob/8bce5f17cd697716600d3b34836365ebcdc04b3f/src/core/session.ts#L57-L68

I know that this doesn't land completely on Turbo(links). The actual problem is the built-in assumptions of vendors like Stripe that assume a certain mode of operation for their integration. That said, considering those kind of assumptions are prevalent, developers might benefit from tooling inside of Turbo to respond to those assumptions. It seems like the ability to stop Turbo might go some way to helping with this issue.

kmmbvnr commented 3 years ago

It seems the session method could be called as

Turbo.navigator.delegate.stop()
mike1o1 commented 3 years ago

A work around that I've used is to add a timestamped meta tag in the head, with a data-turbo-track="reload". On certain pages that I want to force a refresh on (such as something with stripe), I simply set that value to true. Turbo sees that something in the head changed, and triggers a refresh. This ends up being a double page reload on the server (one to get the page, see the head changed, and then another when Turbo does a manual reload).

Here's an example:

<!-- in whatever page loads stripe -->
<% @force_refresh = true %>
<!-- in head.html.erb -->
<% if @force_refresh %>
  <meta name="force-refresh" content="<%= Time.zone.now.to_i %>" data-turbo-track="reload">
<% end %>