backbone-js-on-rails
backbone-js-on-rails copied to clipboard
Any chapter/paragraph on pushState and how to handle direct traffic?
I've read the backbone-js-on-rails
book several months ago, so I'm not sure if things have changed since then regarding this topic, but I can't wrap my head around a good way to handle the rails side of the equation when having a backbone app that uses pushState and the possibility of visitors using direct url's to navigate to a specific part of the application.
WIth that I mean, if I visit /projects/123
by clicking on the project 123
link in my Backbone app, Backbone will handle the link and generate the requested content. However, if I directly navigate to that url from the browser navigation bar, the request will be handled by Rails.
This is what the official Backbone documentation states:
Note that using real URLs requires your web server to be able to correctly render those pages, so back-end changes are required as well. For example, if you have a route of /documents/100, your web server must be able to serve that page, if the browser visits that URL directly. For full search-engine crawlability, it's best to have the server generate the complete HTML for the page ... but if it's a web application, just rendering the same content you would have for the root URL, and filling in the rest with Backbone Views and JavaScript works fine.
So I'm guessing I need to make sure all html requests on all controllers simply render the dashboard
view and fire up the Backbone application the same way it would do when a user visited the root /
url. After that, Backbone should take over. But will I need to do anything in Backbone to handle the new url in the browser, other than what's already described in the book?
Also, if this where to work properly, would Backbone rewrite the url to use #/projects/123
in these cases if the browser doesn' support pushState?
Thank you for any help.
#resolve
We currently only have the section on pushState which tells about why its different and why its not the default, but we don't currently cover how to use it.
What you guess about how to handle this is exactly right. Route all html requests on those controllers to render the dashboard view, instantiate the backbone application the same way you normally do. Backbone does take over and there is nothing you need to do after that. Backbone looks at the current url when loaded and routes to it automatically.
If you do pushState like this the next issue you're going to run into is how to handle links. Your links are all going to hit the server first, which defeats the purpose. So one way to handle this is to bind all links with a click handler that passes the click to Backbone navigation. Here is a sample from our own app.
$('a').live('click', function(event) {
if(!event.shiftKey && !event.ctrlKey && !event.metaKey) {
var uri = $(this).attr('href');
Trajectory.navigate(uri, event);
}
});
The Trajectory.navigate method is as follows:
navigate: function(uri, event) {
if(uri.indexOf(Trajectory.urlRoot) == 0) {
uri = uri.substr(Trajectory.urlRoot.length);
var matched = _.any(Backbone.history.handlers, function(handler) {
if (handler.route.test(uri)) {
return true;
}
});
if(matched) {
if(event) event.preventDefault();
Backbone.history.navigate(uri, true);
return false;
}
}
}
On Feb 22, 2012, at 4:07 AM, Jean Mertz wrote:
I've read the
backbone-js-on-rails
several months ago, so I'm not sure if things have changed since then regarding this topic, but I can't wrap my head around a good way to handle the rails side of the equation when having a backbone app that uses pushState and the possibility of visitors using direct url's to navigate to a specific part of the application.WIth that I mean, if I visit
/projects/123
by clicking on theproject 123
link in my Backbone app, Backbone will handle the link and generate the requested content. However, if I directly navigate to that url from the browser navigation bar, the request will be handled by Rails.This is what the official Backbone documentation states:
Note that using real URLs requires your web server to be able to correctly render those pages, so back-end changes are required as well. For example, if you have a route of /documents/100, your web server must be able to serve that page, if the browser visits that URL directly. For full search-engine crawlability, it's best to have the server generate the complete HTML for the page ... but if it's a web application, just rendering the same content you would have for the root URL, and filling in the rest with Backbone Views and JavaScript works fine.
So I'm guessing I need to make sure all html requests on all controllers simply render the
dashboard
view and fire up the Backbone application the same way it would do when a user visited the root/
url. After that, Backbone should take over. But will I need to do anything in Backbone to handle the new url in the browser, other than what's already described in the book?Thank you for any help.
Reply to this email directly or view it on GitHub: https://github.com/thoughtbot/backbone-js-on-rails/issues/55
Thanks, that is perfectly clear.
Right now my controller is like this:
respond_to :html, :json
def index
respond_with(@projects = current_account.projects)
end
But I guess in order to be able to use pushState, I'd have to use something like this, correct?
respond_to :html, :json
def index
respond_with(@projects = current_account.projects) do |format|
format.html { render text: '', layout: true }
end
end
And then do this for every controller action. Or is there a more efficient way to do this (before_action?)
edit or... I could use match '*path', to: 'main#index'
as shown by Ryan Bates in his latest railscast. Any downsides to this method? (man, it's really hard to find anything about this through google!)
Hi,
I'm also trying to get pushState working with direct traffic.
To "learn by doing", I'm trying to simply convert a scaffolded Rails app to use backbone, but while keeping the original views functional (i.e. progressive enhancement, etc.).
I got everything mostly working here: https://github.com/davidsulc/protocolonel/commit/5001ca269254d2d84e2cbd2eed40c7096ac99b0e
However, since I was initializing backbone in the index
view, navigating directly to localhost:3000/protocols/1
wouldn't initialize the backbone app, so I moved the init
call to the rails application layout: https://github.com/davidsulc/protocolonel/commit/daefc4bab33a88863940507238d8c867baaefc79
If I navigate to localhost:3000/protocols/1
, the backbone app is initialized properly and the proper route gets fired, but the events don't seem to be bound properly. I wrote a function that navigates to the links (Protocolonel.Support.navigateLink
, in https://github.com/davidsulc/protocolonel/blob/ubiquitous_init_attempt/app/assets/javascripts/protocolonel.js), but the events in backbone's show
view (https://github.com/davidsulc/protocolonel/blob/ubiquitous_init_attempt/app/assets/javascripts/views/protocol_show.js) aren't getting bound properly, and clicking on a link triggers a page request from the server.
What am I doing wrong ?
As @JeanMertz said, it's nearly impossible to find anything on the subject on the web. In addition, I'm still in the early stages of wrapping my head around "the backbone way", so I apologize in advance if there are any glaring errors in the code.
Ok, so I got everything working properly with pushState : https://github.com/davidsulc/protocolonel/commit/5c15173e2b5e0a1c6da0ce7eb15f92ef9861f9ba
I needed to attach the router to an existing DOM element (#app
in my code). Although this works, I don't really understanding how or why. Could you explain this in the book ? I was unable to find any example of attaching the router to a DOM element (I essentially copied it from the example app). In addition, there is no mention on the web of why this would be necessary, or what happens in backbone when you do it.
During my various attempts at solving my problem, I also noticed that direct links would work (prior to my fixing the code by attaching the router to a proper DOM element) if I specified the view's el
to be an existing DOM element (although clicking on any link would break the app, since that DOM element would be removed). Any idea why this would be the case ?
@davidsulc I'm not sure why you would need to attach the router to an existing DOM element. What was the problem you were having (what exactly wasn't working) when you did not do this?
While I'm not sure why you would need to do this, I can see that in all of our apps we're using SwappingRouter which does attached the router to an existing DOM element. So if there is a gotcha or bug here, its possible we just didn't hit it.
FWIW, I agree that there is a dearth of information on pushState and I would like to expand the coverage of this in the book once we have it worked out. Trajectory is now completely pushState, but thats just one app and so I think we still need more knowledge about it before fully writing it up and/or recommending it.
Since I bought the book, you can guess I'm a backbone n00b, and everything below boils down to conjecture based on investigation...
First of all, here's what I set out to do: generate a simple scaffold with rails, then implement the same functionality in backbone. The way I'm going about it currently is simply initializing the backbone app in the application layout if appropriate. (I.e. model data is fetched from the server by backbone when it could be harvested from the HTML, which I realize is suboptimal, but this is a work in progress :D)
Since I have a functional app that I'm enhancing with backbone (and using pushState), the links all have real href
attributes leading to actual HTML pages. I hijack those links ('show', 'edit', 'back', etc.) with this function (where Protocolonel
is the app's name):
navigateLink: function (e) {
var target = $(e.currentTarget);
if( ! target.attr('data-method')){ // don't change delete links
e.preventDefault();
Protocolonel.router.navigate(target.attr('href'), { trigger: true });
}
}
Then, in my views, I simply wire up a click
events on links to fire that function. Here's the interesting bit that happens if I don't attach the router to an existing DOM element:
- if I go to the app's root path (by entering the path in the address bar), everything works great
- if I navigate to a deeper page (e.g. the
show
action for a model instance), and I click on a link (that should trigger thenavigateLink
function), the proper function isn't triggered and the full page is fetched from the server (although backbone initializes properly). I seems like somehow the events defined in the backbone view aren't getting bound properly - if I do as above and click (e.g.) the 'edit' link, I land on the edit page after a full page refresh (as the page gets fetched from the server). Then when I click the 'back' button on the browser, I see the JSON for the model instance that backbone fetched, instead of the view for the
show
action.
But if I attach the router to a DOM element, everything works as expected.
The only reason I've started to attach the router to a DOM element is because I saw that being done in your example app...
I've tried to describe the issue as best I can, so let me know if you need more info. I you want to try it yourself, you can get the code here : https://github.com/davidsulc/protocolonel
So if you could look into this, I'd be most interested in the hows and whys of the matter... Thanks !
@davidsulc, providing router.el
isn't specific to pushState, but applies to SwappingRouter in general.
In general, in Backbone, when you hit a route the router is going to do some logic that probably ends with "...and then put a new Backbone view on the page somewhere."
SwappingRouter does this by having the idea of a root DOM element and "swapping" different views into that root DOM element. It does that here https://github.com/thoughtbot/backbone-support/blob/master/lib/assets/javascripts/backbone-support/swapping_router.js#L12 in #swap()
, which gets called from your Protocols router https://github.com/davidsulc/protocolonel/blob/master/app/assets/javascripts/routers/protocols.js.
@davidsulc I've read through your last comment, and I think I understand what's going on. Basically, your approach to intercepting clicks only works for <a>
tags that are rendered by Backbone views, and does not intercept clicks for <a>
tags rendered by Rails (e.g. app/views/protocols/*). Any events bound by declaring an entry in a Backbone.View events
hash will only capture those events for elements that are children of the view's el
- so, only links rendered inside your Backbone views, not links rendered by Rails.
That explains why in step #1 above (go to the app's root path), everything works: because you are interacting 100% through the Backbone views.
Then, in step #2, you navigate directly to a deeper page, so all links on that page are rendered by Rails. Since none of them are intercepted and routed through pushState, it's all regular HTML link clicking....
...until you click "back", where pushState-enabled browsers fire an event called "popstate". Backbone subscribes to this, calls its internal Backbone.history.checkUrl
function, which eventually triggers Backbone.history.navigate
. At that point, your router is called, Backbone stuff gets rendered, and we're full steam ahead as we were in step #1.
To fix this, I'd advise you not bind to navigateLink inside your Backbone views, but globally, as @cpytel commented earlier https://github.com/thoughtbot/backbone-js-on-rails/issues/55#issuecomment-4104985
Does this make sense? & help? Thanks!
@JeanMertz I haven't actually written a transparently upgrading pushState app like you're aiming for, but I'll throw my hat in the ring.
One benefit of allowing your server to respond to the same URL structure as pushState URLs is so you can use pretty URLs without breaking user bookmarks. For those users, a server response that just boots up the Backbone app and invokes the correct route fragment is perfectly serviceable. You also have the opportunity here to deliver more bootstrap data in the initial page request, if you are interested in optimizing this request.
In this case, I don't see anything wrong with a catch-all route. A minor concern, but worth mentioning: If all of your Backbone URLs don't share a common prefix (namespace), you might consider adding one so that you only need one catch-all route, and so you're less likely to accidentally match too much.
A second reason you might be doing this is to support search engine indexing. In that case, you might want to serve up a fully rendered HTML page that's easy to index. Maybe that's conditional based on user agent - you can deliver simple indexable content to the search engines, and users get the same "boot up the Backbone app and invoke the route fragment" page as discussed above.
Or, maybe you always build a full HTML page and then progressively enhance it with Backbone.