I've got a backbone.js application that defines two controllers, and the controllers both define route patterns which match the location.hash. I'm having trouble getting both of them to fire - e.g.
ManagerController = Backbone.Controller.extend({
routes: {
":name": "doStuff"
},
doStuff : function(name) {
console.log("doStuff called...");
}
});
Component1Controller = Backbone.Controller.extend({
routes: {
"xyz123": "doMoreStuff"
},
doMoreStuff : function() {
console.log("doMoreStuff called...");
}
});
so if the url is "http://mysite.com/#xyz123", then I am seeing 'doStuff()' called, or if I comment out that route, then 'doMoreStuff()' is called. But not both.
I'm using this architecture because my page is highly component oriented, and each component defines its own Controller. A 'component manager' also defines a Controller which does some house keeping on all routes.
Should I be able to configure two controllers that both respond to the same route? Cheers,
Colin
Short answer: No, you can't do that. One Controller per page.
Long answer: When you instantiate a new Controller, it adds its routes to the History singleton. The History singleton is monitoring the hash component of the URL, and when the hash changes, it scans the routes for the first expression that matches its needs. It then fires the function associated with that route (that function has been bound to the controller in which it was declared). It will only fire once, and if there is a conflict the order in which it fires is formally indeterminate. (In practice it's probably deterministic.)
Philosophical answer: The controller is a "view" object which affects the presentation of the whole page based on the hash component of the URL. Its purpose is to provide bookmark-capable URLs that the user can reach in the future, so that when he goes to a URL he can start from a pre-selected view among many. From your description, it sounds like you're manipulating this publicly exposed, manually addressable item to manipulate different parts of your viewport, while leaving others alone. That's not how it works.
One of the nice things about Backbone is that if you pass it a route that's already a regular expression, it will use it as-is. So if you're trying to use the controller to create a bookmarkable description of the layout (component 1 in the upper right hand corner in display mode "A", component 2 in the upper left corner in display mode "B", etc) I can suggest a number of alternatives-- allocate each one a namespace in the hash part of the URL, and create routes that ignore the rest, i.e.
routes: {
new RegExp('^([^\/]*)/.*$'): 'doComponent1stuff',
new RegExp('^[^\/]*/([^\/]*)\/.*$': 'doComponent2stuff',
}
See how the first uses only items after the first slash, the second after the second slash, etc. You can encode your magic entirely how you want.
I suggest, though, that if you're going to be doing something with the look and feel of the components, and you want that to be reasonably persistent, that you look into the views getting and setting their cookies from some local store; if they're small enough, cookies will be enough.
I have a very similar issue. At present, backbone stops after the first matching route. I have a dirty workaround where I am overriding the loadUrl method of Backbone History. Here I am iterating through all of the registered routes and triggering callback for all of the matching routes .
_.extend(Backbone.History.prototype, {
loadUrl : function() {
var fragment = this.fragment = this.getFragment();
var matched = false;
_.each(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
handler.callback(fragment);
matched = true;
}
});
return matched;
}
})
Philosophically, I am fine with having single controller per page. However, in a component based view framework, it will be nice to have multiple views per route rendering different parts of a view state.
Comments are welcome.
I've used namespacing to deal with a similar problem. Each module comes with it's own module controller, but is restricted to handle routes that start with /moduleName/ this way modules can be developed independently.
I haven't fully tested this yet, if you take a look at the Backbone.js source, you can see this at line 1449:
// Attempt to load the current URL fragment. If a route succeeds with a
// match, returns `true`. If no defined routes matches the fragment,
// returns `false`.
loadUrl: function(fragment) {
fragment = this.fragment = this.getFragment(fragment);
return _.any(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
handler.callback(fragment);
return true;
}
});
}
The any method will stop as soon as it matches a handler route (with the "return true"), just comment the return and the short-circuit will never happend, and all the handlers will be tested. Tested this with a marionette app with two modules, each one having it's own router and controller, listening same routes anb both fired up.
I think this is the simplest way of resolving it
routes: {
'': 'userGrid',
'users': 'userGrid',
}
Related
Consider a certain route let's say myapp\profile
which has two modes (buyer/seller)
What i would like to achieve is:
keep the same route url for both modes
Alternate the view with different HTML files (lets say buyer.html, seller.html), of course each view has it's view model.
Sharing some logic between the two modes.
I would like to have a controller/logic to each mode
What i already considered:
Thought about using ui-router's sub states, but I dont want to change the url.
Thought about creating this 'profile' route and while navigating to it, figure the mode (buyer/seller), and then $state.go to a new state (but again, i would like to keep same route name at the end so it's not ok)
Ideally thought i could navigate to my shared controller and then render the correct view and controller, but this idea kinda messed up me.
Could you share what is a clean way of doing this?
most use cases
Normally, in order to dynamically select a template for a state, you can use a function:
state = {
.....
templateUrl: function($stateParams){
if ($stateParams.isThis === true)
return 'this.html'
else
return 'that.html'
}
}
but...
Unfortunately you can't pass other injectables to the templateUrl function. UI.Router only passes $stateParams. You don't want to alter the URL in anyway so you can't use this.
when you need to inject more than $stateParams
But you can leverage templateProvider instead. With this feature, you can pass a service to your templateProvider function to determine if your user is a buyer or seller. You'll also want to use UI.Router's $templateFactory service to easily pull and cache your template.
state = {
....
templateProvider: function($templateFactory, profileService){
var url = profileService.isBuyer ? 'buyer.html' : 'seller.html';
return $templateFactory.fromUrl(url);
}
}
Here it is working in your plunkr - http://plnkr.co/edit/0gLBJlQrNPUNtkqWNrZm?p=preview
Docs:
https://github.com/angular-ui/ui-router/wiki#templates
http://angular-ui.github.io/ui-router/site/#/api/ui.router.util.$templateFactory
I'm working on an AngularJS app that has a catch all route (eg, .when('/:slug', {...)) which is necessary to support legacy url formats from a previous (non-angular) version of the app. The controller that responds to the catch all tries pulling a related object, and, if not found, redirects to a 404 page using the $location.path method. This works at getting the user to the 404 page, but when the user hits back in their browser it takes them back to the page that forced them to the 404 page in the first place and they end up being unable to escape the cycle.
My question is if there is 1) a better pattern for handling this situation, or 2) if there is a way to reroute the user that doesn't force a history push state in the browser?
You can change the url without adding to the history state, found here under "Replace Method". This is effectively the same as calling HTML5's history.replaceState().
$location.path('/someNewPath').replace();
I haven't found that it's possible to change the view without changing the url. The only method to change the view, that I've found, is to change the location path.
The normal operation of the route system is for the $route service to watch for the $locationChangeSuccess event and then begin loading a route. When it's done loading the template, performing the resolve steps and instantiating the controller it then in turn broadcasts a $routeChangeSuccess event. That $routeChangeSuccess is monitored by the ng-view directive, and that's how it knows to swap out the templates and scopes once the new route is ready.
With all of the above said, it may work to have application code emulate the behavior of the $route service by updating the current route and emitting the route change event to get the view to update:
var errorRoute = $route.routes[null]; // assuming the "otherwise" route is the 404
// a route instance is an object that inherits from the route and adds
// new properties representing the routeParams and locals.
var errorRouteInstance = angular.inherit(
errorRoute,
{
params: {},
pathParams: {},
}
);
// The $route service depends on this being set so it can recover the route
// for a given route instance.
errorRouteInstance.$$route = errorRoute;
var last = $route.current;
$route.current = errorRouteInstance;
// the ng-view code doesn't actually care about the parameters here,
// since it goes straight to $route.current, but we should include
// them anyway since other code might be listening for this event
// and depending on these params to behave as documented.
$rootScope.broadcast('$routeChangeSuccess', errorRoute, last);
The above assumes that your "otherwise" route doesn't have any "resolve" steps. It also assumes that it doesn't expect any $routeParams, which is of course true for the "otherwise" route but might not be true if you use a different route.
It's unclear what of the above is depending on implementation details vs. interface. The $routeChangeSuccess event is certainly documented, but the $$route property of the route instance seems to be an implementation detail of the route system given its double-dollar-sign name. The detail that the "otherwise" route is kept in the route table with the key null is possibly also an implementation detail. So with all of this said, this behavior may not remain functional in future versions of AngularJS.
For more information you could refer to the ng-view code that handles this event, which is ultimately what the above code is trying to please, along with the event emitting code that I used as the basis for the above example. As you could infer from these links, the information in this post is derived from the latest master branch of AngularJS, which at the time of writing is labelled as 1.2.0-snapshot.
From my understanding, the differences is the callback functions to events on an AppRouter should exist in the Controller, instead of the same Router object. Also there is a one-to-one relationship between such AppRouter & Controllers, all my code from Router now moves to Controller, I don't see too much point of that? So why use them? I must be missing something?
The way I see it is to separate concerns:
the controller actually does the work (assembling the data, instanciating the view, displaying them in regions, etc.), and can update the URL to reflect the application's state (e.g. displayed content)
the router simply triggers the controller action based on the URL that has been entered in the address bar
So basically, if you're on your app's starting page, it should work fine without needing any routers: your actions (e.g. clicking on a menu entry) simply fire the various controller actions.
Then, you add on a router saying "if this URL is called, execute this controller action". And within your controller you update the displayed URL with navigate("my_url_goes_here"). Notice you do NOT pass trigger: true.
For more info, check out Derick's blog post http://lostechies.com/derickbailey/2011/08/28/dont-execute-a-backbone-js-route-handler-from-your-code/ (paragraph "The “AHA!” Moment Regarding Router.Navigate’s Second Argument")
I've also covered the topic in more length in the free preview of my book on Marionette. See pages 32-46 here: http://samples.leanpub.com/marionette-gentle-introduction-sample.pdf
I made some override for the router. And currently use it in this way (like Chaplin):
https://gist.github.com/vermilion1/5525972
appRoutes : {
// route : controller#method
'search' : 'search#search'
'*any' : 'common#notFound'
},
initialize : function () {
this.common = new Common();
this.search = new Search();
}
I've been developing large Backbone Marionette applications for about a year now. One thing that has always been challenging is passing around options for view states in routes. Examples of options for view states would be active tabs, temporarily selected items, or sorting options on the page that need to be linkable.
Before updating to Backbone 0.9.9+ the best way that I found to deal with these cases was to add query parameters to the end of my routes. My router would look something like this:
"/questions/:id/" : "showQuestions"
"/questions/:id/?*params" : "showQuestionsWithFilters"
Which would match something like:
"/questions/1/?search=help&sort=name"
The real advantage to this that I found is that the router will match different routes based on the presence of url parameters. Clearing all url parameters and then triggering navigation will actually cause a route change.
After Backbone 0.9.2 routers no longer recognize url parameters. In the above example, the "showQuestions" method would get fired regardless of the presence of a url parameter. The general consensus in this GH issue (https://github.com/documentcloud/backbone/issues/891) and the opinion of the Backbone contributors seems to be that url parameters should NOT be used on the client side at all and instead all information that needs to be passed on to a view should be stored in the main url path (https://github.com/documentcloud/backbone/issues/2440).
A router using this method might look something like:
"/questions/:id/(search/:term)(sort/:type/)"
The problem with this method is that every optional parameter needs to be explicitly added to the router and that all parameters must be ordered accordingly else they will not match. Because there is no delineator between the route and its options and the order is determined by the router, it seems unnecessarily difficult to add or edit options on the fly.
At this point I'm stuck between keeping my current url structure and trying to figure out a way to make it work or migrating over to the latter approach. Before I go too far in either direction I'm wondering if there are other opinions on best practices for similar use cases.
What would you recommend?
There's another piece of a route called a splat. From http://backbonejs.org/#Router:
Routes can contain parameter parts, :param, which match a single URL
component between slashes; and splat parts *splat, which can match any
number of URL components.
In my app, I'm using one required param, and then any number of optional "filters":
var BrowseRouter = Marionette.AppRouter.extend({
appRoutes: {
'browse/:page(/*filters)': 'browse'
}
});
My URL is then formatted with a series of key/value pairs separated by slashes: #/browse/3/type:image/sort:date/count:24.
In my controller, I'm passing the function two arguments: page and filters. page is a simple value ("3"). filters is optional, and is a longer string that contains everything after the page value ("type:image/sort:date/count:2").
I have an "explode" underscore mixin to take that string and convert it to an object.
_.mixin({
/*
* Take a formatted string (from the URL) and convert it into an object of
* key/val pairs. If the val looks like an array, make it so.
* _.explode("count:105/sort:date/type:image,video")
* => { count: 105, sort: date, type: ['image','video']}
*/
explode: function(str) {
var result = {};
if(!str){
return result;
}
_.each(str.split('/'), function(element, index, list){
if(element){
var param = element.split(':');
var key = param[0];
var val = param[1];
if (val.indexOf(",") !== -1) {
val = val.split(',');
}
result[key] = val;
}
});
return result;
}
});
Trying to create a page that allows users to add edit and view a parent child combined.
UI has 3 columns
Parent : List of Parents Children : Child
I want to configure the controllers(s) so that users can come back to right where they were but see no need to have it so both Parent and child can be editable.
// Getting closer using backbone marionette but still having some small issues
MyRouter = Backbone.Marionette.AppRouter.extend({
appRoutes: {
'': 'AddClient',
'View/:clientid': 'ViewClient',
'Edit/:clientid': 'EditClient',
'View/:clientid/Add': 'PolicyAdd',
'View/:clientid/View/:policyid': 'PolicyView',
'View/:clientid/Edit/:policyid': 'PolicyEdit'
}
});
someController = {
AddClient: function () {
var someView = new ClientAdd();
MyApp.clientPane.show(someView);
},
ViewClient: function (clientid) {
var someView = new ClientView();
MyApp.clientPane.show(someView);
},
EditClient: function (clientid) {
var someView = new ClientEdit();
MyApp.clientPane.show(someView);
},
PolicyAdd: function (clientid) {
this.ViewClient(clientid);
var someView = new PolicyAdd();
MyApp.policyPane.show(someView);
},
PolicyView: function (clientid, policyid) {
this.ViewClient(clientid);
var someView = new PolicyView();
MyApp.policyPane.show(someView);
},
PolicyEdit: function (clientid, policyid) {
this.ViewClient(clientid);
var someView = new PolicyEdit();
MyApp.policyPane.show(someView);
}
};
Having the "this.ViewClient" feels hacky and also doesn't work.
Multi-part answer, here...
"this.ViewClient is not a function"
this is a bug in Marionette. the controller method is called in the context of the router instead of the controller, so the call to this.ViewClient is trying to find it on the router.
oops.
bug logged. will fix asap. https://github.com/derickbailey/backbone.marionette/issues/38
--
UPDATE: this bug is now fixed in v0.5.1 of Backbone.Marionette https://github.com/derickbailey/backbone.marionette
--
to work around this issue for now, you can do this:
PolicyEdit: {
someController.ViewClient();
// ...
}
If that doesn't work, you may need to use Underscore.js' bind or bindAll methods to ensure the correct binding on your controller functions.
These workaround won't be necessary once i get the bug fixed... hopefully later today / tonight.
is basically calling other routes the best way to manipulate multiple regions?
The direct answer to this question is no.
But, you're not calling a route in this case. You're calling a method on your controller. That's perfectly fine - and actually, I would encourage this. It's a proper use of your object, and is one of the things that I think should be done instead of calling another route / router handler.
Routers And Controllers
A router is a feature, not an architectural requirement. Your app should work without a router, and a router should only add the ability to use bookmarks and the browser's forward/backward button.
With that philosophy in mind (which I know is controversial), using a controller like you have and calling multiple methods on your controller in order to get your application to the correct state, is one of the right approaches to take.
Look at it this way: if you removed the router from your app, you would be forced to call methods on your controller directly. To prevent duplication of code, you'll want to create many small methods on your controller that can do one thing very well, and then compose larger methods out of those smaller methods.
Hope that helps. :)