Handling view options via Backbone routes - backbone.js

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;
}
});

Related

Dynamic parameters with ui-router

I'm developing a search app with angular using ui-router.
Requirements:
The search form has too many fields and all have to be optional.
Users should share with another users the URL that display the page with the result list. (So I need to use querystring)
So I could have urls like
path/to/url/list?p=123&v=876
path/to/url/list?c=yes&a=true&p=123
path/to/url/list?z=yes&c=yes&a=true&p=123
path/to/url/list?z=yes&v=876&a=true&p=123
And endless combinations. I know that I can use $location.search() to get all params in json format. That is great! but the question is How can I define the url state with ui-router? Define explicitly all params in the url is not an option. I have read many post but I didn't find a concrete answers.
If you're getting parameters from $location you don't need to define them in state explicitly.
I think, the best way is to use 'resolve' property of $stateProvider configuration:
$stateProvider.state('mystate', {
// Some code here
resolve: {
queryData: ['$location', ($location) => {
$location.absUrl(); // Contains your full URI
return true;
}]
}
});
It's kind of initialization. After that, ui-router will cut URI, but you will store needed data. This case also works fine, when user passing URI directly in browser address input.
Also you can try to set $urlRouterProvider with $urlMatcher for this purposes, but it will be more difficult.

Multiple optional route parameters and pretty urls with Angular ui router

I am trying to use multiple optional parameters to make urls looks like
/:region/:direction/:subdirection/page/
where all params instead of the last one are optional.
I've tried to use
params:{region: {value: 'default', squash:true}, ...}
for each optional parameter, but when some of them are missed in url, the router doesn't work.
So, the only solution I've found in order to save pretty urls in this situation is to declare multiple routes:
.state('page', {
url: '/:region/page'
}
.state('page-direction', {
url: '/:region/:direction/page'
}
.state('page-subdirection', {
url: '/:region/:direction/:subdirection/page'
}
Additionally:
1. there will be no subdirection without direction,
2. region will be in every link, but it is variable
Are there any optional solutions?
Thnx!
The default parameters are for internal routing, example
$state.go('page-subdirection');
The above will redirect to the state and the $stateParams.region will be the default value you defined for that state.
While $state.go('page-subdirection', {region: 'newValue'); will make $stateParams.region to be newValue in that specific case.
The same apply for constructing the URL from the view:
<a ui-sref="page-subdirection">
<a ui-sref="page-subdirection({region: 'newValue'})">
It may also depend on what your final goal is. From what you're describing, it seems that you may actually be trying to pass in filters into a single state, not necessarily trying to route to 3 different states.
If in the case of filtering a single state, I'd recommend using query parameters as described in the docs (https://github.com/angular-ui/ui-router/wiki/URL-Routing). The advantage is that query parameters are not required or used from a routing point of view, they're simply parameters that your controller will utilize to filter data or tweak the view.
Snippet from above doc link:
Query Parameters
You can also specify parameters as query parameters, following a '?':
url: "/contacts?myParam"
// will match to url of "/contacts?myParam=value"
If you need to have more than one, separate them with an '&':
url: "/contacts?myParam1&myParam2"
// will match to url of "/contacts?myParam1=value1&myParam2=wowcool"

Difference between AngularJS UI Router params object specified in state and views

I have just started working on AngularJS and specifically Angular UI Router project. While working on a project I observed that some of team members have specified params option object in view option object of state. when this is the case it doesn't accept optional parameters when passed through ui-sref/state.go.
However I moved this params option object to state instead of view and optional parameters feature started working. I am using AngularJS 1.3.x and AngularJS UI Router version 0.2.13. Here is the sample code to explain more clearly what I want to say :
$stateProvider.state('contacts', {
url:"/user/{userId}/contact"
views: {
'view1': {
....//other options
params :{userId:0,contactId:null}
}
}
...//other options including `controller` and `resolve` options.
});
In above sample code(I have given minimal required information) params object is specified on view1 object instead of on contacts state. Also contactId is the optional non-URL parameter which is passed on one of the use case and not passed in another one. However when I check $stateParams object in the controller specified on state it just shows up userId and not contactId even if I pass it.
I fixed this issue when I moved params option object from view1 object to contacts state object as shown below:
$stateProvider.state('contacts', {
url:"/user/{userId}/contact"
views: {
'view1': {
....//other options
//i have removed the `params` object from here..
}
}
...//other options including `controller` and `resolve` options.
params :{userId:0,contactId:null} //and have put it here.
});
Now I have following questions :
1) What difference does it make by changing where I specify params object. What are the significance if any?
2)Is specifying params object on view altogether wrong configuration? If yes then why UI router doesn't complain and works with parameters specified in URL?
If no then why it doesn't work with optional non-URL parameters?
3) Any specific use cases one would prefer specifying params option object on view object than on state object?
Also another side effect I found when I moved this params option object to state from view object is I am no longer able to bookmark this url or even refresh url in browser. When I do this it redirects me to our home page. Maybe this could be how we are handling this redirection in our project. But just curious to have any pointers why this could be happening?(including how generally this redirection is handled using ui-router) Of course I am going dig deep into our project code to see why this side-effect is happening.
However I would at least like to have answers to my 3 questions(and subquestions) I have asked here.
Neither the guide nor the API documentation say that a named view can have parameters. So I think adding params in the view was just a mistake, and it had no effect whatsoever.
So,
1) What difference does it make by changing where I specify params object. What are the significance if any?
In the views, ui-router doesn't care about it. In the state, it does what is documented in the API documentation.
2) Is specifying params object on view altogether wrong configuration? If yes then why UI router doesn't complain and works with parameters specified in URL? If no then why it doesn't work with optional non-URL parameters?
Yes, it seems to be wrong configuration.
3) Any specific use cases one would prefer specifying params option object on view object than on state object?
No

AngularJS routing for dynamic urls, how?

I'm trying to understand how can i configure my angularJS routing given the following case:
We have a search page where we display the search results based on tags provided (1..n tags). we would like that a user to be able to parse enter a url as the following and our system to do the search and show the respective results.
The url format should be:
http://mywebsite.com/search/<term1>/<term2>/<termN>...so it could be different number of terms.
I was looking into the route provide and couldn't figure out a way to do it dynamically.
i saw that i could put in the routeprovid:
.when('/search/:searchParams',... but that handles only when i have one term...is there anyway to configure it to take as many terms as is given?
Does this help you at all? Seems to support dynamic routing and you could probably cut apart the :name parameter to do what you wish, perhaps.
http://gregorypratt.github.io/AngularDynamicRouting/
Ken
You could try base64ing your searchParams:
.when('/search/:searchParams', {controller:'SearchCtrl'})
function SearchCtrl($routeParams, $location){
//Assuming your params are an array like ['param1', 'param2', 'param3']
//You could easily adapt this to base64 a JSON object
function encodeParams(params){
return window.btoa(params.join(';'));
}
function decodeParams(string){
return window.atob(string).split(';');
}
var searchParams = decodeParams($routeParams.searchParams);
scope.search = function(params){
$location.path('/search/' + encodeParams(params));
}
}
My solution may be looks not so glad, but it's works at least:
You may organize your routs in way
yoursite.com/term1Name/**:param1**/term2Name/**:param2**/term3Name/**:param3**
To make it's clear, you may do your routes seems like REST routes. For example I'm want to go to a list of a services:
www.yoursite.com/servises/
Go to the one of the services:
www.yoursite.com/servise/:id
And if I'm want to see some of the service details, I'll do:
www.yoursite.com/servise/:id/details
and so
www.yoursite.com/servise/:id/detail/:id

multiple matching routes

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',
}

Resources