Search code examples
ruby-on-railsbackbone.jsturbolinks

Rails : Backbone + Turbolinks Hybrid Routing


I'm building a Rails 4 application and I'm trying to combine location.hash Backbone History with turbolinks push state. The application is broken up into multiple smaller, SPA like pages.

Example of Problem

The problem I'm having is that when I do Backbone.Router.navigate(), within a page, it doesn't register anything with turbolinks. Here's a hypothetical example to demonstrate the problem:

  1. Visit /one
  2. @router.navigate 'page-one'
  3. Location becomes /one#page-one
  4. @router.navigate 'page-two'
  5. Location becomes /one#page-two
  6. Click on link to /two
  7. Turbolinks intercepts and navigates to /two
  8. Click on the Back Button, you are still stuck on /two
  9. Click on the Back Button again, you are still stuck on /two
  10. Click on the Back Button a third time, you are brought back to /one

What I Tried

I guessed that what's probably happening is that when navigating via backbone, the browser is logging the change but turbolinks isn't. I tried to expose Turbolinks.reflectNewUrl:

turbolinks.js.coffee

@Turbolinks = { visit, pagesCached, reflectNewUrl }

And then modifying Backbone.Router.navigate so that it registers with turbolinks every time we navigate:

navigate = Backbone.Router.prototype.navigate
Backbone.Router.prototype.navigate = (page, args...) ->
  navigate page, args...
  window?.Turbolinks?.reflectNewUrl "##{page}"

That sort of worked, Step 8 above is no longer stuck on /two, but click on the back button again and we get this error:

Uncaught TypeError: Cannot call method 'getElementsByTagName' of null turbolinks.js:142
removeNoscriptTags turbolinks.js:142
changePage turbolinks.js:111
fetchHistory turbolinks.js:69
(anonymous function) turbolinks.js:390

So something is still wrong.

Now I'm hoping there's a kind soul out there to help me out and point out my perhaps obvious blunder :)


Solution

  • I disagree about not mixing Turbolinks and Backbone. They fulfil different roles and there are occasions where you might like to use both.

    In fact, I understand Basecamp uses both.

    You can use this (now infamous) post as a starting point: http://www.goddamnyouryan.com/blog/rails-4-turbolinks-and-backbone

    However I still had the problem you're having. Here's how I solved it (provisionally, I haven't tested this in the wild):

    window.MyApp =
      Models: {}
      Collections: {}
      Views: {}
      Routers: {}
    
      initialize: ->
        # Build it up
        @router = new MyApp.Routers.Pages(data)
        Backbone.history.start()
    
      close: ->
        # Tear it down
        @router.close(false)
        @router = undefined
    
    # Turbolinks exposes the before-change event when a page change is initiated
    $(document).on 'page:before-change', ->
      # If the app has been initialized
      if MyApp.router?
    
        # Stop the history and clean up the app
        Backbone.history.stop()
        MyApp.close()
    
        # We're leaving the domain of the backbone app, check for Turbolinks
        if Turbolinks?.supported
          # Push the last known state of the app to the Turbolinks history
          window.history.replaceState {turbolinks: true, url: window.location.href}, window.title, window.location.href
    
    $(document).on 'page:change', ->
      # If it's the page with our backbone app
      if $('#pages').size() > 0
        MyApp.initialize()