I have a router defined somewhat similar to the following (greatly simplified for demo purposes here):
var MyRouter = Backbone.Router.extend({
routes: {
'': 'search',
'directions': 'directions',
'map': 'map'
},
search: function () {
// do something
},
directions: function () {
// do something
},
map: function () {
// do something
},
initialize: function () {
this.listenTo(myModel, 'change', this.updateNavigation);
Backbone.history.start({ pushState:true });
},
updateNavigation: function () {
var selected = myModel.get('selected');
this.navigate(selected);
}
});
The history entries are all getting created properly by the updateNavigation call, and when I hit the back-button to go back through the history I've generated, the routes fire for each of the entries, that is until I get to the initial entry. At that point, even though the url has updated with that history entry, the route that should interpret the url at that point does not fire. Any idea what might be going on here? Am I making some bad assumptions about how history works?
EDIT:
It seems I get inconsistent results - it's not always just the initial entry that doesn't execute, it's sometimes anything after the first time I've went back one through history. That is, I click the back-button once, the url changes, the routes fire properly. I hit it again, the url changes, the routes don't fire. It smacks of me doing something wrong, but I haven't a clue.
Don't ask me why, but my browser back button doesn't seem to function properly when I don't trigger: true on #navigate.
My very brief assumption is that Backbone.history.start({ pushState:true }); used in wrong place.
As far as I know, backbone history start should be after the router instance is created. Like,
var router = new MyRouter();
Backbone.history.start({ pushState:true });
I've discovered the problem. I was using a querystring and updating the querystring based on actions within the application. Each time I modified the querystring, I added another history entry, but the actual route parts of the history entry didn't always change. Backbone won't do do anything different based on the same route but with different querystring, so I had to abandon using the querystring and just make restful urls instead. Once I did that, the history worked fine.
Related
In our app, we actually have two Backbone SPA applications. The first one is for login, registration and other features for unauthenticated users. The URL for this would be something like http://www.example.com/registration#signin. Once you login, you are redirected to our main Backbone app at http://www.example.com/ui#home.
In my main UI app, I am using Backbone.history without pushState. The App file looks something like:
define(function (require) {
var App = new Marionette.Application();
App.addInitializer(function (options) {
...
});
...
App.on('initialize:after', function () {
$(function(){
if (Backbone.history) {
Backbone.history.start({ root: '/ui' });
}
});
$.log("**WebApp**: Marionette app started.");
});
return App;
});
Of course, everything works flawlessly in any browser except IE 9 (and maybe 10, I need to check). In IE 9, all the routing works fine. Clicking links such as http://www.example.com/ui#anotherpage works. However, when the user clicks the Back button in their browser, they are not sent back to the last route fired. Instead, they are sent to http://www.example.com/registration#signin, which is the last page served by Node, our web server. As I click through links, I can see that history.length and Backbone.history.history.length are not updating.
All routes are fired from links/URL's. I'm not using router.navigate() within the code. Here are examples of our Router:
define(function (require) {
var Backbone = require('backbone'),
Marionette = require('marionette');
return Backbone.Marionette.AppRouter.extend({
appRoutes: {
"": "showHome",
"home": "showHome",
"foo": "showFoo"
}
});
});
And Controller:
define(function (require) {
var Backbone = require('backbone'),
Marionette = require('marionette');
return Backbone.Marionette.Controller.extend({
showHome: function () {
require(['webapp','modules/home'], function (WebApp) {
WebApp.module("Home").start();
WebApp.module("Home").controller.showModule();
});
},
showFoo: function () {
require(['webapp', 'modules/foo'], function (WebApp) {
WebApp.module("Foo").start();
WebApp.module("Foo").controller.showModule();
});
}
});
});
UPDATE:
On further research, it turns out the problem is that older versions of IE don't record hash changes in their history. See - Change location.hash and then press Back button - IE behaves differently from other browsers. But I'm still not sure what the workaround for this would be. I'm guessing it would somehow involve manually handling hash change events with a plugin such as jQuery Hashchange and doing... something? Manually setting IE's history? Or crafting a custom history object and using it when we detect a Back button in IE?
I was having the same problem in one of our apps for IE.
Starting backbone history like below works.
Backbone.history.start({
pushState: true,
hashChange: false
});
Update: As mentioned By T Nguyen,
When you set pushState to true, hash URL's no longer trigger routes. Unless you add server-side support for all your Backbone routes, you need to add an event handler on the client side which captures appropriate links and calls .navigate() on the route
When a model is changed, I update the route (it has a url that contains the application's current state).
When a url is visited (or back is pressed) I update the model from the route.
This creates circular logic problems for me that I can't get my head around. Things are being changed twice for no reason.
Is it normal to base everything on the route, and use that to update the model?
Is it normal to have two models?
What is normal?
Any help or advice would be appreciated. Thanks
I wouldn't advise using Router the way you do. In general, the route action should not change model state. In general, HTTP GET operations should not have side-effects.
Routers should be used for navigation between different pages of a single-page application. Model changes should be triggered directly from the view code that handles user input. Let's say you have a model User, and view UserView, the view could work something like this:
var UserView = Backbone.View.extend({
events: {
"click #save", "save"
},
initialize: function(options) {
this.model = options.model;
},
render: function() {
//your render code here
},
save: function() {
var fields = {
name: this.$("#name").val();
email: this.$("#email").val();
};
this.model.save(fields , {
//after save go back to users page, or whatever
success: function() { window.location.hash = "/users"; },
error: this.displayError
});
}
});
Backbone isn't really an MVC framework, so the Router shouldn't be treated as a pure controller. And even if you did, changing state in a route action would be equivalent to changing state in a MVC controller GET endpoint - bad, bad idea.
If you want to adhere to a pure MVC pattern, you should implement your own controller layer, or look at another layer besides Backbone.
I'm working with trigger and backbone, and am trying to programmatically navigate to a url. This is all happening using the file:// protocol, as everything in running inside trigger io only.
This manual navigate though doesn't trigger the function associated with the route.
My router looks like this
var BARouter = Backbone.Router.extend({
routes: {
"users/sign_in": "userSignin",
"users/sign_up": "userSignup",
"": "catchAll"
},
userSignin: function(){
},
userSignup: function(){
forge.logging.info("in user signup----");
},
catchAll: function(){
}
});
var app_router = new BARouter();
BA.router = app_router;
Backbone.history.start({pushState: true});
and I'm manually navigating
BA.router.navigate(navigate_to("users/sign_up"), {trigger:true});
The navigate_to method just returns the full url in the form "file://users/sign_up".
But nothing is logged to the console, and the execution flows normally. Am I missing something here ?
Using pushState with file urls probably doesn't make sense, I'm also not sure why you need the navigate_to function.
Try setting pushState to false and navigate using the string of the route, i.e.:
BA.router.navigate("users/sign_up", {trigger:true});
Ok, I think this is something simple, however I am being to stupid to see it. Here is my code in backbone using the backbone boilerplate method
require([
"app",
// Libs
"jquery",
"backbone",
// Modules
"modules/example"
],
function(app, $, Backbone, Example) {
// Defining the application router, you can attach sub routers here.
var Router = Backbone.Router.extend({
routes: {
"": "index",
"item" : 'item'
},
index: function()
{
console.info('Index Function');
var tutorial = new Example.Views.Tutorial();
// Attach the tutorial to the DOM
tutorial.$el.appendTo("#main");
// Render the tutorial.
tutorial.render();
},
item: function()
{
console.info('Item View');
}
});
// Treat the jQuery ready function as the entry point to the application.
// Inside this function, kick-off all initialization, everything up to this
// point should be definitions.
$(function() {
// Define your master router on the application namespace and trigger all
// navigation from this instance.
app.router = new Router();
// Trigger the initial route and enable HTML5 History API support
Backbone.history.start({ pushState: true, root: '/reel' });
});
// All navigation that is relative should be passed through the navigate
// method, to be processed by the router. If the link has a data-bypass
// attribute, bypass the delegation completely.
$(document).on("click", "a:not([data-bypass])", function(evt) {
// Get the anchor href and protcol
var href = $(this).attr("href");
var protocol = this.protocol + "//";
// Ensure the protocol is not part of URL, meaning its relative.
if (href && href.slice(0, protocol.length) !== protocol &&
href.indexOf("javascript:") !== 0) {
// Stop the default event to ensure the link will not cause a page
// refresh.
evt.preventDefault();
// `Backbone.history.navigate` is sufficient for all Routers and will
// trigger the correct events. The Router's internal `navigate` method
// calls this anyways.
Backbone.history.navigate(href, true);
}
});
});
I am running this of a MAMP server and when i type Localhost:8888/reel , I get the example index page that comes with boilerplate. However when I type Localhost:8888/reel/item or Localhost:8888/reel/#item I either get, page can not be found or directed back to my index page.
My question is what am i doing wrong. Do I need to use htaccess? This doesnt seem right. Is there a way using backbone to sort this. Sorry if this is really simple, just cant get my head around it.
the problem may lie with the pushState flag.
With that on the request goes all the way to the server and it sees the full url and responds to it with whatever it would do ...
does it work if you have a
$(function (){
setTimeout(navMe, 2000);
});
function navMe() {
backbone.navigate("item");
}
that way 2 seconds after load it will navigate to item view and you know that its because of the request going to the server and not to backbone.
How can I get notified before a Backbone router call the specific routing function?
I'd like to have a generic "reset" function before rendering every page.
Is there any event I can bind?
Update: the solutions I found are based on extending the router or the history in order to trigger the event.
It looks like the 1.1.x release of Backbone has what you want with the Router.execute method:
MyRouter = Backbone.Router.extend({
routes: {},
// fired before every route.
execute: function(callback, args) {
// ...
}
});
if execute function is present it will be called before every route change but you must pass the arguments in callback to properly execute other matching routes.
MyRouter = Backbone.Router.extend({
routes: {},
// fired before every route.
execute: function(callback, args, name) {
//your logic
if (callback) callback.apply(this, args); //this must be called to pass to next route
},
});
This plugin does what you want. It works with 0.5.3. I'm not certain if it works with 0.9.1 yet or not.
https://github.com/angelo0000/backbone_filters
I' am using this in the constructor and it's working fine
this.bind( "all", this.ACL );
Here ACL is a function