Turbolinks: Events to simulate jQuery $(document).ready()

Created on 5 Feb 2016  Â·  14Comments  Â·  Source: turbolinks/turbolinks

In previous versions of Turbolinks, you could replace calls to

$(document).ready ->

with

$(document).on 'ready page:load', () ->

which would allow you to bind events to page elements on the both initial page load and subsequent Turbolinks-driven page loads. Worked as you'd expect ready() to on non-Turbolinks sites; you could bind click events to dom elements and whatnot.

Trying to figure out how to do this with Turbolinks 5 -- I tried both:

$(document).on 'ready turbolinks:load', () ->

and, alternately, after adding the compatibility shim:

$(document).on 'ready page:load', () ->

However, neither worked as expected. Specifically, when I'd trigger history back or forward via the browser buttons, the ready callback would fire, which is not what happened in previous Turbolinks version.

What events should I be binding to here? Or should I look at replacing this pattern with something else?

Most helpful comment

Gotcha, thanks. So a significant difference here that would be worth documenting for folks upgrading existing applications is that the old page:load event does not fire after history back/forward, whereas turbolinks:load does.

So, using a contrived example, with previous Turbolinks this non-indempotent transformation would only prepend one "X " to h2:

$(document).on 'ready page:load', () ->
  $('h2').prepend('X ')

Whereas when upgrading to TL5, if you simply modify the above code to use turbolinks:load:

$(document).on 'turbolinks:load', () ->
  $('h2').prepend('X ')

... you'll append an X on first visit, and then keep prepending "X "s to h2 elements every time you navigate away and then back via history back/forward.

This can of course be worked around, e.g.:

$(document).on 'turbolinks:load', () ->
  $('h2')
    .not('[data-modified]').attr('data-modified', true)
    .prepend('X ')

(For a real-world example, I'm using this pattern to avoid an error thrown by re-initializing a third-party library.)

Wondering if there's a need for another event -- say, turbolinks:ready -- which would fire on DOMContentLoaded and subsequent Turbolinks-driven page visits, but _not_ on page restores via history back/forward (as the old page:load behaves)?

All 14 comments

Hi Geoff,

The only event you need to listen for is turbolinks:load. It fires once on the initial page load in response to DOMContentLoaded, and again on every Turbolinks visit, whether it’s triggered by history, a link click, or a call to visit.

Gotcha, thanks. So a significant difference here that would be worth documenting for folks upgrading existing applications is that the old page:load event does not fire after history back/forward, whereas turbolinks:load does.

So, using a contrived example, with previous Turbolinks this non-indempotent transformation would only prepend one "X " to h2:

$(document).on 'ready page:load', () ->
  $('h2').prepend('X ')

Whereas when upgrading to TL5, if you simply modify the above code to use turbolinks:load:

$(document).on 'turbolinks:load', () ->
  $('h2').prepend('X ')

... you'll append an X on first visit, and then keep prepending "X "s to h2 elements every time you navigate away and then back via history back/forward.

This can of course be worked around, e.g.:

$(document).on 'turbolinks:load', () ->
  $('h2')
    .not('[data-modified]').attr('data-modified', true)
    .prepend('X ')

(For a real-world example, I'm using this pattern to avoid an error thrown by re-initializing a third-party library.)

Wondering if there's a need for another event -- say, turbolinks:ready -- which would fire on DOMContentLoaded and subsequent Turbolinks-driven page visits, but _not_ on page restores via history back/forward (as the old page:load behaves)?

Ok digging into the code I see that the cached body is cloned via cloneNode(true), which will cache the nodes (with any modifications) but not bound events.

So this is really only a problem with non-idempotent DOM transformations. I guess you can handle this case by undoing them before-cache, or somehow track that they've been performed and avoid doing them twice on the same body.

Ok I've successfully upgraded a codebase from classic to TL5, using the following pattern:

$(document).on 'turbolinks:load', ->
  # bind event listeners, initialize third-party libraries

$(document).on 'turbolinks:before-cache', ->
  # teardown third-party libraries, save any relevant state in dom attributes

No extra API was necessary, so I'll go ahead and close this issue.

You’re right that there are some significant differences here from Turbolinks Classic.

Before rendering a new page, Turbolinks 5 clones the current page’s body and saves it to its snapshot cache. Then whenever Turbolinks displays a cached page—either by a restore visit using the Back or Forward buttons, or by showing a preview during an advance visit to an already-visited location—all non-permanent elements are freshly cloned, which means they have no attached event listeners or associated data.

The benefits of this approach are that it’s simpler to reason about when to register event listeners (no need to distinguish between page “change” and page “load”), and that Turbolinks is less likely to leak memory (because existing event listeners are discarded).

The constraint with this approach, as you’ve correctly identified, is that all DOM manipulation needs to be idempotent. If you transform the document with JavaScript, you must make sure it’s safe to perform that transformation again, _particularly on a cloned copy of the document_. In practice, this usually means using a data attribute or some other heuristic to detect when an element has already been processed.

Once you accept this constraint, in most cases there’s really no need to couple your application to Turbolinks’ events at all. Using MutationObserver, you can receive immediate notification when significant elements are added to or removed from the document, and add or remove event listeners and apply transformations as needed.

With MutationObserver, it doesn’t matter when or how you insert HTML. Newly added elements will trigger the same behavior, whether they come from a Turbolinks visit, an SJR response, or some other script on the page. (Turbolinks Classic approximates this behavior with the page:update event, which fires after every page change and SJR response, but without precise knowledge of which—if any—elements have changed.)

Basecamp 3 makes extensive use of both MutationObserver and custom elements, which also have attach and detach lifecycle callbacks.

Hi @sstephenson, following the discussion I have a question here:

I was using Turbolinks classic (2.5.x) until this afternoon, I upgraded to 5.0.0.beta2. Now my pages are broken. I have javascripts at the bottom of each of my pages, which do the necessary initialization to the page they belong to.

I am not able to make those events firing correctly anymore. Before I just put <script> tags at the bottom of the page, without $(document).ready(function () { }). This is because that each time the page is loaded via Turbolinks (classic), those scripts get evaluated, therefore the page is working correctly with the needed initialization.

Now I found that, the <script> tags are not evaluated after a Turbolinks page load. I read your last comment. I'm fine with 'transformations should be idempotent'. However it appears that Rails (Sprockets, and now Turbolinks 5) encourages the developer to put all javascript into one bundle and load that at somewhere of each page. For the page specific scripts, the developer has to determine if the initialization is appropriate for the current page. Now with Turbolinks 5, the on-page script is broken, or not?

The problem I think here, is that turbolinks:load event tries to mimic ready on document. But with Turbolinks, the document is _not_ exactly the document without that: specifically, the ready event used to fire just for _this_ page, but now for _every_ page. If we replace ready with turbolinks:load in the transition from no Turbolinks to Turbolinks, we have make sure that all the turbolinks:load callback on document for _each_ page, are safe for _all_ the pages.

Am I correct here?

Hello again, @sstephenson, I gathered enough information to answer the question I asked myself:

Turbolinks 2.5.x never evaluate <script> tags when the page is loaded via a history back/forward. In Turbolinks 3.x, adding data-turbolinks-eval="always" will make it be evaluated always, even on a history back/forward. And Turbolinks 5.0.0.beta2 doesn't have this yet.

I'm using gem 'turbolinks', github: 'turbolinks/turbolinks-classic' for the moment and it appears to satisfy my needs for now. Hope script evaluation can be added to Turbolinks 5.0 soon.

Sorry for getting back to this closed issue, I'm just having troubles porting my app from Turbolinks classic. Specifically, I've used page:update and page:change events. I noticed the compatibility.coffee and tried turbolinks:render, but it didn't work. I am having a js.erb template that does some page manipulation and currently I rely on page:update to trigger. Any tips, please?

@assimovt Please see Responding to Page Updates in the documentation.

Thanks to @sstephenson i've solved it!! Working on this for two hours!! Thanks!!!

I'm sorry I get this closed issue up, but I got the same situation as @gbuesing had.
What's the right way to prevent JS firing each time on history back/forward events?

$(document).on('turbolinks:load', function() {
  console.log('fired');
});

The code above works well on clicks/visits, but if I click to my browser's Back button and then click to Forward, I'll get that JS fired twice.

I'm actually wondering the same thing as @blackst0ne as well. I want to run some graph initialization code only on one page - but I need to wait for the page to load to grab the width of the space available for the graph.

If I use the turbolinks:load, my app will continue to try to run that code on subsequent pages that do not use it.

To be clear - the recommended way here is to just continue to allow that code to run on every subsequent page - but make sure it more or less exits out gracefully once it sees that it is not supposed to run on that page?

@gregblass @blackst0ne
I'm getting the same problem here
In index.js loaded from index.html

$(document).on('turbolinks:load', function() {

 alert("js for index ready");

});

on about.js loaded from about.html

$(document).on('turbolinks:load', function() {

 alert("js for about ready");

});

When loading index.html i get the alert " alert("js for index ready");" ok
When i click a link to move to about.html i still get " alert("js for index ready");"
when i move again to index.html i'm getting
alert("js for index ready");"
alert("js for about ready");"

i just want to trigger some events on about.html page at ready state like i was doing with jQuery, not firing that in index.html when coming back from about.html
Any solution?
.

@mindprojects

I don't have any. I don't use neither turbolinks, nor jquery anymore.

Was this page helpful?
0 / 5 - 0 ratings