I'm playing with Turbolinks 5 and Rails, trying to work out how it changes the game.
Traditionally my pages work by having a bundle of javascript which contains libraries (jQuery, Knockout, etc) and application specific code, e.g. view models - all attached to the window object in some organised structure. All controllers in an area of the application, e.g. the frontend, will share the same JavaScript (and CSS) bundle.
On each page that uses JavaScript, there will be an initializer of sorts to get things started. For example:
$.ready(function() { window.users.update.init(#{@user_edit_view_model.javascript_configuration.to_json.html_safe}) } )
The responsibility of this init function would then be to set up a knockout view model and bind it to some node. The javascript_configuration probably would contain the initialization state of the current user.
Anyway, this approach doesn't seem to play well with Turbolinks at all.
I understand that it will only fire if the user accesses users#edit
as the first page (or does a hard refresh), so $.ready
is obviously out of the picture.
If I attach to the turbolinks:load
event, the above code fires not just when the user enters users#edit
, but also on any page the user navigates to subsequently (until he/she does a full refresh at some point).
I have been able to get around this by defining a function that removes the callback after the first execution.
# For running code after the current page is ready
window.turbolinks_in = (callback) ->
event_listener = ->
callback()
window.removeEventListener("turbolinks:load", event_listener)
window.addEventListener "turbolinks:load", event_listener
In addition, I have also devised:
# For running code when leaving the current page
window.turbolinks_out = (callback) ->
event_listener = ->
callback()
window.removeEventListener("turbolinks:before-cache", event_listener)
window.addEventListener "turbolinks:before-cache", event_listener
These two functions seemingly allow me to run code when a page loads and when it "unloads".
My questions are:
- Seeing as I had to come up with my own wrapper functions, I suspect Turbolinks has been deliberately developed to discourage the initialization flow that I use. Is that true?
- If it's true, what is the idiomatic way to go about? How should I set up a knockout view model or for that matter, run any code that pertains to the particular page?
- If it's not true, how do I reliably set up an unload/leave function for a page.
turbolinks:before-cache
is not emitted when the cached page is replaced with a fresh page, so how do I cleanup after the brief display of the cached page? I do understand that it semantically makes sense thatturbolinks:before-cache
should not be fired when the (already) cached page is replaced, but what goes in it's place then? Something liketurbolinks:unload
which doesn't exist.
In terms of running code on a particular page, first of all, I'd generally avoid adding an inline
script
for that page. This normally works OK in non-Turbolinks apps, but in Turbolinks-enabled apps, where state and event handlers are maintained between page loads, things can get a bit messy. Ideally you should contain all your JS in a single file. (If you're wondering about how to inject server-generated content, I'll cover that a bit later.)You're on the right lines in terms of setting up and tearing down, but I wonder if, rather than your
turbolinks_in
andturbolinks_out
functions, you'd consider implementing a single initialize function for setup onturbolinks:load
, and subsequently a single teardown function onturbolinks:before-cache
and onturbolinks:before-render
(this is the "unload" event you're after). Your setup/teardown code might look like:So rather than directly calling an object's
init
function in an inline script,window.MyApp.init
decides which objects to initialize.How does it decide?!
It seems like you're mirroring controller names and action names for your JS objects. This is a good starting point, we just need to get those names into the client side. One common approach (used by Basecamp, I believe) is to use
meta
elements in thehead
to output some server-generated attributes:As long as your objects follow the controller/action naming pattern, your app's initialize function might be something like:
You could follow this approach to include your JavaScript config:
Then your app's init function could call the relevant initializer with the config as follows:
Lastly, we'll need to teardown the results of the initialization. To do this, we'll store the return value of an object's
init
, and if it includes adestroy
function, we'll call it:To put it all together:
Obviously this approach will only work for one init per page, so you may want to adapt it to work with more. Implementing this is beyond the scope of this answer, but you may wish to look into T3, which handles all this for you!