This is a simple question, but I am new to routing and haven't been able to find an answer to this.
I have a Marionette Router (if I intimidate you, its really the same thing as a Backbone Router. Very simple).
sys.routes = {
"app/:id": "onAppRoute",
};
sys.Router = new Marionette.AppRouter({
controller: {
onAppRoute: function(route) {
console.log('Called app route!');
},
},
appRoutes: sys.routes,
});
Backbone.history.start({pushState: true})
This works - if you hit the back button my browser, the url will change within my Single Page Application and will call the onAppRoute function.
However, let's say I open a new browser window and paste in my page url to a certain 'app':
http://localhost/app/myApplication
This doesn't call the onAppRoute function. It doesn't even seem like it should, though, but I really don't know.
I want it to.
I don't know if I am doing it wrong, or if I should just manually fire it by grabbing my page url on page load, parsing it, then 'navigating' to that route. Seems hacky.
Contrary to your intuition, backbone's default behaviour is to trigger matching routes on page load! cf. http://backbonejs.org/#Router - look for the option silent: true. You'd have to specify that for the router to IGNORE your matching routes on page load, i.e. not trigger the corresponding callbacks.
So your problem lies somewhere else: your routes do NOT match the url you have stated as an example. Clearly, you require an :id parameter, trailing http://localhost/app/myApplication. Therefore, http://localhost/app/myApplication/213 would cause your callback to be triggered on page load, given you didn't pass silent: true as an option to backbone.history.start().
If you want to match the 'root' url, i.e. no params, you would define the following route:
routes: {
'/': someFunction
}
The :id part is a parameter, which will be extracted by Backbone.Router and sent as an argument to onAppRoute.
But in your URL you don't have any parameters /localhost/app/myApplication
Related
I have a Durandal application, and I use router.mapUnknownRoutes to display a user-friendly error page if the URL does not correspond to a known route. This works fine -- if I go to, say /foo, and that doesn't match a route, then the module specified by mapUnknownRoutes is correctly displayed.
However I cannot find any way to display that same error page when I have a parameterised route, and the parameter does not match anything on the backend.
For example, say I have a route like people/:slug where the corresponding module's activate method looks like this:
this.activate = function (slug) {
dataService.load(slug).then(function () {
// ... set observables used by view ...
});
};
If I go to, say /people/foo, then the result depends on whether dataService.load('foo') returns data or an error:
If foo exists on the backend then no problem - the observables are set and the composition continues.
If foo doesn't exist, then the error is thrown (because there is no catch). This results in an unhandled error which causes the navigation to be cancelled and the router to stop working.
I know that I can return false from canActivate and the navigation will be cancelled in a cleaner way without borking the router. However this isn't what I want; I want an invalid URL to tell the user that something went wrong.
I know that I can return { redirect: 'not-found' } or something similar from canActivate. However this is terrible because it breaks the back button -- after the redirect happens, if the user presses back they go back to /people/foo which causes another error and therefore another redirect back to not-found.
I've tried a few different approaches, mostly involving adding a catch call to the promise definition:
this.activate = function (slug) {
dataService.load(slug).then(function () {
// ... set observables used by view ...
}).catch(function (err) {
// ... do something to indicate the error ...
});
};
Can the activate (or canActivate) notify the router that the route is in fact invalid, just as though it never matched in the first place?
Can the activate (or canActivate) issue a rewrite (as opposed to a redirect) so that the router will display a different module without changing the URL?
Can I directly compose some other module in place of the current module (and cancel the current module's composition)?
I've also tried an empty catch block, which swallows the error (and I can add a toast here to notify the user, which is better than nothing). However this causes a lot of binding errors because the observables expected by the view are never set. Potentially I can wrap the whole view in an if binding to prevent the errors, but this results in a blank page rather than an error message; or I have to put the error message into every single view that might fail to retrieve its data. Either way this is view pollution and not DRY because I should write the "not found" error message only once.
I just want an invalid URL (specifically a URL that matches a route but contains an invalid parameter value) to display a page that says "page not found". Surely this is something that other people want as well? Is there any way to achieve this?
I think you should be able to use the following from the activate or canActivate method.
router.navigate('not-found', {replace: true});
It turns out that Nathan's answer, while not quite right, has put me on the right track. What I have done seems a bit hacky but it does work.
There are two options that can be passed to router.navigate() - replace and trigger. Passing replace (which defaults to false) toggles between the history plugin using pushState and replaceState (or simulating the same using hash change events). Passing trigger (which defaults to true) toggles between actually loading the view (and changing the URL) vs only changing the URL in the address bar. This looks like what I want, only the wrong way around - I want to load a different view without changing the URL.
(There is some information about this in the docs, but it is not very thorough: http://durandaljs.com/documentation/Using-The-Router.html)
My solution is to navigate to the not-found module and activate it, then navigate back to the original URL without triggering activation.
So in my module that does the database lookup, in its activate, if the record is not found I call:
router.navigate('not-found?realUrl=' + document.location.pathname + document.location.hash, { replace: true, trigger: true });
(I realise the trigger: true is redundant but it makes it explicit).
Then in the not-found module, it has an activate that looks like:
if (params.realUrl) {
router.navigate(params.realUrl, { replace: true, trigger: false });
}
What the user sees is, it redirects to not-found?realUrl=people/joe and then immediately the URL changes back to people/joe while the not-found module is still displayed. Because these are both replace style navigations, if the user navigates back, they go to the previous entry, which is the page they came from before clicking the broken link (i.e. what the back button is supposed to do).
Like I said, this seems hacky and I don't like the URL flicker, but it seems like the best I can do, and most people won't notice the address bar.
Working repo that demonstrates this solution
If I navigate to a view by clicking on a link such as 127.0.0.1/#/project/1, the correct view gets displayed. However, if I call this url directly in the browser (or hit refresh), the view won't be displayed. What could be the reason for this behaviour?
The way I set up the Router is as follows:
var AppRouter = Backbone.Router.extend({
routes: { },
initialize:function () { }
});
var app = new AppRouter();
and then in every module (I'm using require.js), a route and handler will be added
app.route("project/:id", "showProject");
Could it be that the routes aren't registered yet and thus the callbacks won't be called?
Make sure that you are calling Backbone.history.start() after all of your routers are loaded/instantiated and routes defined: http://backbonejs.org/#History-start
Alternatively, you could stop the history with Backbone.history.stop(), and start it again. Then the added route(s) will be picked up.
BTW, you can test if the history is currently started with the boolean Backbone.History.started (note the capital 'H' is necessary).
I think I'm missing some basics about Backbone's routing functions.
I'm building an app and it looks something like so:
file: app.js
App = {}
App.nav = new Backbone.Router;
require('app/controller');
file: controller.js
App.nav.route('home', 'home', function () {
console.log("Home Activated");
});
App.navigate('home');
At this point the browser changes the URL in the address bar to /home but nothing happens and I don't get the Home Activated console message.
I've tried using my own routing class (i.e. Backbone.Router.extend({})) but I don't really see a point in it as I still need to initialize it, and I want to use a central history/navigation in my app that all modules/controllers add routing to it rather than creating a router for every controller.
What am I doing wrong?
http://documentcloud.github.com/backbone/#Router-navigate
From the documentation:
If you wish to also call the route function, set the trigger option to true.
But as OlliM wrote, you need to activate the history first!
So your answer should be:
Backbone.history.start();
App.nav.navigate('home', {trigger: true});
edit:
forgot to put "nav"
For the routing to work, you need to call Backbone.history.start() after setting up your routes (basically after you've done everything else). See: http://documentcloud.github.com/backbone/#History-start
I just want to point this out as it saved me a world of hurt and heartache.
If you are routing to a custom page such as
Backbone.router.navigate('/some/page'); // does not work
And it appears to not be working. Add a trailing '/'
Backbone.router.navigate('/some/page/'); // works
This cost me a few hours of troubleshooting...
I want to have a kind of RESTful URL structure like below:
/accounts
/accounts/account/123
I've set up my routes as such:
MyRouter = Backbone.Router.extend({
routes : {
'/accounts': 'accounts',
'/accounts/account/:account': 'account',
},
accounts: function() {
console.log('accounts CALLED');
},
account: function() {
console.log('account CALLED');
},
});
The problem, is when I go to /accounts/account/123 , both accounts() and account() get called (as the URL matches both routes). I tried a route such as /accounts$, but it doesn't look like it's supported in the routes hash.
Is there a way to accomplish this? Would a manual router.route(route, name, callback) work instead (although I really prefer not to do that).
I got this cleared up by another SO question. I didn't realise that I had to use the router.navigate function strictly.
Using the router programmatically (not via browser bar), those duplicate calls go away. I'm also seeing the expected functions called only once... when using router.navigate.
I still have to find out how to capture back & forward buttons to call those functions. Thanks for the feedback so far.
Try /accounts$ instead. $ is the regex for the end of the string.
Using Backbone.js, is it possible to make the router navigate to the page where it came from? I'd like to use that for the case where I change my URL when a popup appears, and I want go change it back, when I hide the popup. I don't want to simply go back, because I want to keep the background page in precisely the same position as I left it before I showed the popup
You can solve this problem by extending Backbone.Router and storing all the routes during navigation.
class MyRouter extends Backbone.Router
constructor: (options) ->
#on "all", #storeRoute
#history = []
super options
storeRoute: ->
#history.push Backbone.history.fragment
previous: ->
if #history.length > 1
#navigate #history[#history.length-2], true
Then, when you have to dismiss your modal, simply call the MyRouter.previous() method that it will redirect you back to the last "hash".
I think mateusmaso's answer is mostly right but requires some tweaks to guarentee that you always get the right URL you are looking for.
First you need to override the route method to have a beforeRoute method fire:
route: (route, name, callback) =>
Backbone.Router.prototype.route.call(this, route, name, =>
#trigger('beforeRoute')
callback.apply(this, arguments)
)
Then you bind the event and initialize the history instance variable:
initialize: (options) ->
#history = []
#on "beforeRoute", #storeRoute
Next create helper methods for storing and retrieving the fragment:
storeRoute: =>
#history.push Backbone.history.fragment
previousFragment: =>
#history[#history.length-2]
Finally, you need one final helper method that can be used to change the URL without reloading and store the resulting fragment. You need to use this when closing the pop up or you won't have the expected fragment in your history if the user gets the pop up again without navigating anywhere else. This is because calling navigate without "trigger: true" won't trigger the event handler to store the fragment.
changeAndStoreFragment: (fragment) =>
#navigate(fragment)
#storeRoute()
This answer may not address the question, but it's preety much the same issue. I couldn't navigate silently to a previous route, or a custom route because of the trailing slash. In order for route functions to NOT be triggered, use {trigger:false}, or don't use trigger at all since false is the default behaviour, and make sure your route begins with #something instead of '#/something' (notice the slash), or change the regEx inside Backbone.js, the router part.
You can trigger a route in the onCloseEvent of your popup or overlay with:
router.navigate('/lasturl/');
This will set the url. If you pass true as the second param, you also will execute the routes action. Otherwise the page will be left unchanged.
http://documentcloud.github.com/backbone/#Router-navigate