Reverse a route in Backbone js - backbone.js

Similar to Django's {{ url }}, is there a method or way to reverse a particular route by passing it a name and variables.
// example Router
var router = Backbone.Router.extend({
routes: {
'!/user/:user_id': 'editUserAction',
'!/': 'homeAction'
},
editUserAction(user_id) {
// edit user view
},
homeAction() {
// home view
}
});
Some method like
router.reverse('editUserAction', '5');
Would return the hash: !/user/5
router.reverse('homeAction');
Would return the hash: !/

A discussion about reverse routing. https://github.com/documentcloud/backbone/issues/951
a simple hack
var personRoutes = {
list: "/persons",
show: "/persons/:id",
edit: "/persons/:id/edit"
}
var getRoute = function(obj, route, routeDefinitions){
return routeDefinitions[route].replace(":id", obj.id);
}
var person = new Model({id: 1});
var route = getRoute(person, "edit", personRoutes); // => "/persons/1/edit"

Unfortunately no, there isn't anything like this built in to backbone. I've wanted something similar and there has been discussion of this on the list once or twice - maybe even a pull request (don't remember for sure at the moment). But it has not yet been done.
The best that I've come up with is to write my own methods that produce the correct route string:
function userEditPath(userId){
return "/user/" + userId + "/edit";
}

Related

external redirection in AngularJS erasing navigation stack

We have set up different applications something like micro frontends running in different versions of Angular, one of them which is in AngularJS is required to redirect to a url which is in another project. Everything works until you try to go back using the browsers back button, and it takes you to the previous domain the user has navigated to. It can't go back to the immediately previous URL. It seems the navigation has been replaced but I can't see anything on the project that uses location.replace.
for example:
I am navigating
www.anyurl.com
then i go to my projects dns like
myproject.com/employer/dashboard
after I can click on to desired button which will take me to
myproject.com/employer/job-flow#location
Up to there everything is fine, but If I click on the browsers back button instead of taking me to:
myproject.com/employer/dashboard
It will go to the previous domain:
www.anyurl.com
I hope this is a clear explanation
from here: /employer/search to /employer/job-flow#location
The Process:
Declare a new item in the menu:
..., {
title: 'Post a job',
mainPath: 'job-flow',
fallbackUrl: 'employer/job-flow',
image: '/assets/images/new-job.svg',
notification: false,
subitems: [],
enabled: true
}
In the HTML there is a:
ng-href="{{ item.fallbackUrl }}"
Then there is an angular module which stores an array with all the routes:
var employerWebRoutes = [
...
'employer/job-flow'
];
Afterwards the array is iterated (I think it works like an interceptor for the given routes):
employerWebRoutes.forEach( function ( route ) {
$stateRegistryProvider.register( {
url: '/' + route + '{path:.*}',
name: route + 'ExternalState',
component: 'merlExternalRoutesComponent',
resolve: {
reloadFromServer: reloadFromServerResolver
},
data: {
authAccess: 'private',
isExternalRoute: true
}
} );
} );
And lastly the reloadFromServerResolver method
var reloadFromServerResolver = [
'$transition$',
'$q',
function (
$transition$,
/** angular.IQService */ $q
) {
var /** #type module:angular.ui.IStateService */ stateService = $transition$.router.stateService;
var /** #type string */ toUrl = stateService.href( $transition$.to().name, $transition$.params() );
setTimeout( function () {
window.location.assign( toUrl );
}, 10 );
return $q.reject();
}
];
I am guessing this is how is done, I am new to this project and can't figure very well how to describe this problem, if anyone can point in the right direction I would be highly grateful. Thanks

backbone fetch on a nested route

I have a Sitesand a Positionscollection. Each time the user selects a new site, the id is sent to the refreshPositions method which is in charge of doing the fetch call.
The route to get the positions look like this '.../sites/1/positions'
view.js
refreshPositions: function(siteId) {
this._positions.fetch({
success: this.onPositionsFetchSuccess.bind(this),
error: this.onPositionsFetchError.bind(this)
});
},
So refreshPositions is called whenever I need to update the positionson the page and the siteId parameter has the id, I just don't know to tell fetch to route to something like .../sites/n/positions where n would be the siteId .
Sorry if I missed relevant informations for my question, I'm pretty new to backbone.
I see, so you are calling fetch from your Positions Collection. The out-of-the-box functionality there is to fetch the whole collection (every Position object) if you have a RESTfull api set up. If you want more specific behaviour from your collection, you can probably write it into the Collection object definition.
var PositionCollection = Backbone.Collection.extend({
initialize: function(models, options) {
this.siteId = (options && options.siteId) || 0;
},
url: function() {
if (!this.siteId) {
return '/positions'; // or whatever
}
return '/sites/' + this.siteId + '/positions';
},
// etc...
});
Then, assuming that _positions refers to an instance of PositionCollection you can do:
refreshPositions: function(siteId) {
this._positions.siteId = siteId; // or wrap in a setter if you prefer
this._positions.fetch({
success: this.onPositionsFetchSuccess.bind(this),
error: this.onPositionsFetchError.bind(this)
});
},

Is it good practice to manage view instantiation in a router?

So this is my first Backbone project and I'm wondering if I'm doing things in the best way. My app basically has two states, one of them displays a search box and the other displays a search box with a table under it. My router has routes for searching and for the initial landing page with just the search view. When the user types in the query the router navigates to the search route and the table view is added to the page. This is my router:
app.Router = Backbone.Router.extend({
routes: {
'': 'index',
'search/coords=:address&age=:age&rad=:rad': 'search'
},
search: function(address, age, rad){
app.statusView || (app.statusView = new app.StatusView());
app.searchView || (app.searchView = new app.SearchView());
app.trigger('status:loading');
app.Practices.fetch({
reset: false,
success: function() {
app.searchView.setElement($('#search-box')).render();
var searchQuery = new app.SearchQueryModel({age: age, coords: address.split(","), radius: rad});
if (!app.tableView){
app.tableView = new app.TableView({model: searchQuery});
} else {
app.tableView.model = searchQuery;
app.tableView.refresh();
};
}
});
app.trigger('status:clear');
},
index: function() {
app.statusView = new app.StatusView();
app.searchView = new app.SearchView();
app.footerView = new app.FooterView();
app.searchView.setElement($('#search-box')).render();
}
});
As you can see my views are instantiated in the index route and then the same views are used when you search, unless the user is going directly to the search page in which case the views are instantiated there. I'd be surprised if this wasn't very sub-optimal because it seems clumsy to be checking if the view already exists in the search route. Is there a better way of doing things?
Lets say its not bad, but there is one better approach.
As for now you router is in charge of hook-up URL with app astatus and also for view and model control. The second may be detached from Router, so you will need Controller abstraction, but Backbone does not provide Controller "from the box".
But this is not the problem, you can use plugin or take a look at Controller realization in Marionette.js
The main idea here is to split responsibilities between app part correctly:
1) Router - keeps routes and hook up URL with controller action
2) Controller - manage views and models (create, delete, fetch and so on)
3) View - listen to model and DOM events and render data
4) Model - provide actual data and work with data.
First of all welcome to Backbone. It is a lovely framework which can allow you to make things as beautiful or ugly as you'ld like. Your question is about where view instantiation should be, in terms of good practices. Of course it seems sort of wrong to do it there as it violates the Law of Demeter by handling both url routing and view instantiation.
But the views have to be run from somewhere right? If not the router then where?
So I have two responses:
If your app is simple and you just want to play with backbone then you're probably going to be fine. A lot of people let single page app frameworks complicate otherwise simple apps. I'm not trying to be lazy, but where you have it now is the natural beginner's choice in Backbone. If this is your case then stop here.
If you want to use the full power of backbone to custom make a framework then read on.
So my setup is designed to be able to start a new project using some boilerplate functions and create only a few classes which are specific to the new app. Route handling and all of that kind of thing seems low-level enough to me that it should be just part of some configuration that I don't want to look at often. The upshot is that my router looks like this:
define([
'autorouter'
], function(AutoRouter){
var AppRouter = AutoRouter.extend({
autoRoutes: {
":page" : "routeDirect",
":page/:object" : "routeDirect",
":page/:object/:action" : "routeDirect",
"": "routeDirect"
}
});
return AppRouter;
});
Then for each new project I have a file where I keep the non-default routes, for instance:
define(function(require){
return {
"schedule" : require('screens/schedule')
, "logout" : require('screens/logout')
, "login" : require('screens/login')
, "create" : require('screens/create')
, "upload" : require('screens/upload')
, "select" : require('screens/selection')
, "inventory" : require('screens/inventory')
, "describe" : require('screens/description')
}
});
I put each screen into it's own file (using requirejs for the multi-file dependency management). The extra variables get passed through to the screen.
Each screen is the brain for a particular user experience and is responsible for loading views and maybe handling some events while that screen is active.
If that seems like an interesting setup then here is how I did it:
For the router itself I use a boilerplate class which I borrowed from Derick Bailey with some slight modifications:
define([
'jquery', 'underscore', 'backbone'],
function($, _, Backbone) {
var AutoRouter = Backbone.Router.extend({
constructor: function(options){
Backbone.Router.prototype.constructor.call(this, options);
var that = this;
that.app = options.app;
if (this.autoRoutes){
that.processAutoRoutes(options.app, that.autoRoutes);
}
},
processAutoRoutes: function(app, autoRoutes){
var method, methodName;
var route, routesLength;
var routes = [];
var router = this;
for(route in autoRoutes){
routes.unshift([route, autoRoutes[route]]);
}
routesLength = routes.length;
for (var i = 0; i < routesLength; i++){
route = routes[i][0];
methodName = routes[i][1];
method = app[methodName];
router.route(route, methodName, method);
}
}
});
return AutoRouter;
});
I never have to look at it, but I do need to pass it an app instance. For example:
this.appRouter = new AppRouter({app : this});
Finally my route direction function:
define(function(require){
var pathParser = function(path){
return Array.prototype.slice.call(path);
}
var pathApply = function(path, routes, context){
var pathArray = pathParser(path);
var primary = pathArray[0];
if (routes.hasOwnProperty(primary)){
routes[primary].apply(context, pathArray.slice(1));
} else {
routes["default"].apply(context, pathArray.slice(1));
}
}
return function(path){
//NOTE PLEASE that this references AutoRouter
//Which has an app property
var oApp = this.app;
var pathRoutes = _.extend(require('urls'), {
"default" : require('screens/default')
});
pathApply(arguments, pathRoutes, oApp);
};
});
So, did I make things better? Well if you're doing something very simple with just a screen or two, then you certainly don't want to build this sort of setup from scratch. But if you're like me, and you want to be able to quickly produce new projects then having some boilerplate like the two classes above allows for one JSON object to tell the app which routes I should send to which screens. Then I can have all of the logic in the appropriate places, allowing separation of concerns. Which is why I think Backbone is so pleasant.
My understanding of your problem is that you are triggering a route each time you are hitting search.
If this is how you are doing it, then use view events hash (used to capture and handle events that happen in a view) for search.Don't use routes. Define an events hash in the view and have a callback to handle the search.
var myAppEventBus = _.extend({},Backbone.Events);
var myAppController = {
function : search(options) {
// create an instance of the collection and do a fetch call passing the
// search parameters to it.
var searchResultsCollection = new SearchResultsCollection();
// pass search criteria, the success and error callbacks to the fetch
// method.
var that = this;
searchResultsCollection.fetch(
{
data:that.options,
success : function() {
// Pass the fetched collection object in the trigger call so that
// it can be
// received at the event handler call back
var options = {
"searchResultsCollection" : that.searchResultsCollection;
};
myAppEventBus.trigger("search_event_triggered",options);
},
error : function() {
// do the error handling here.
}
}
);
}
};
// Application Router.
var MyAppRouter = Backbone.Router.extend({
routes : {
'search/coords=:address&age=:age&rad=:rad': 'search'
},
search : function(searchParams) {
// Fetch the query parameters and pass it to the view.
var routeSearchExists = false;
var searchOptions = {};
var options = {};
if(searchParams) {
routeSearchExists = true;
// If search params exist split and set them accordingly in
// the searchOptions object.
options.searchOptions = searchOptions;
}
// Create and render the search view. Pass the searchOptions
var searchView = new SearchView(options);
searchView.render();
// Create and render an instance of the search results view.
var searchResultsView = new SearchResultsView();
searchResultsView.render();
// If there are search parameters from the route, then do a search.
if(routeSearchExists) {
searchView.search();
}
}
});
// The main view that contains the search component and a container(eg: div)
// for the search results.
var SearchView = Backbone.View.extend({
el : "#root_container",
searchOptions : null,
initialize : function(options) {
// Intialize data required for rendering the view here.
// When the user searches for data thru routes, it comes down in the
// options hash which can then be passed on to the controller.
if(options.searchOptions) {
this.searchOptions = options.searchOptions;
}
},
events : {
"search #search_lnk":"initSearch"
},
initSearch : function(event) {
event.preventDefault();
var searchOptions = {};
// Fetch the search fields from the form and build the search options.
myAppController.search(searchOptions);
},
search : function() {
if(this.searchOptions) {
myAppController.search(searchOptions);
}
}
});
// The view to display the search results.
var SearchResultsView = Backbone.View.extend({
searchResultsCollection : null;
initialize : function(options) {
// Handling the triggered search event.
myAppEventBus.on("search_event_triggered",this.render,this);
},
render : function(options) {
//search results collection is passed as a property in options object.
if(options.searchResultsCollection)
//Render your view.
else
// Do it the default way of rendering.
}
});
SearchView is the root view that contains the search component and a container like div to hold the search results.
SearchResultsView displays the result of a search.
When search option is clicked, the event callback (initSearch) gets the entered search data.
The search method on myAppController object is invoked and the search query is passed.
An instance of the search collection is created and fetch is invoked passing it the search query and also the success and error callback.
On success, a custom backbone event is triggered along with the fetched collection.
The callback(render method in SearchResultsView) for this event is invoked.
The callback renders the results of the search.
When loading in the router an instance for both the views can be created(the results view will be empty) and attached to the dom.
If you wish to search by multiple query strings at the url then I would suggest you to use the following route.
search?*queryString.
In the route callback make a call to a utility function the splits the querystring and returns you a search object and pass on the search string to the view.

Backbone.history.navigate with IE <= 9

Having an issue with using Backbone history / push state - but only with browsers that don't support it (old IE)
The issue is this. When I visit /en_gb/dashboard for the first time - everything works, in all browsers. However, in IE<=9, it's appending #dashboard to the address bar, forming /en_gb/dashboard#dashboard. Now, when I hit refresh, my router is not triggering.
Not all my site is under Backbone control - so the router is working off:
routes: {
'dashboard': 'showDashboard'
}
My bootstrap looks like this:
if (Backbone.history) {
var pushStateSupported = _.isFunction(history.pushState);
var urlRoot = '/en_gb/';
var enableSilent = !pushStateSupported;
Backbone.history.start({
pushState: pushStateSupported,
root: urlRoot,
silent: enableSilent
});
if (!pushStateSupported) {
Backbone.history.navigate(window.location.pathname.substring(urlRoot.length), { trigger: true });
}
}
Adding debug, I can see Backbone.history.navigate() always being called but it seems the trigger: true is not being picked up when that hash is present.
Hmm - I seem to have fixed it - while not an elegant solution, this does solve it for me:
if (!pushStateSupported) {
var route = window.location.pathname.substring(urlRoot.length);
Backbone.history.navigate('/#' + route, { trigger: true });
}
It's not elegant in that the URL in the address bar appears as /en_gb/dashboard##dashboard - but it is now getting through the Backbone.navigate() method. Previously it was failing on
if (this.fragment === fragment) return;

Backbone.js: get current route

Using Backbone, is it possible for me to get the name of the current route? I know how to bind to route change events, but I'd like to be able to determine the current route at other times, in between changes.
If you have instantiated a Router in your application, the following line returns the current fragment:
Backbone.history.getFragment();
From the Backbone.js documentation:
"
[...]
History serves as a global router (per frame) to handle hashchange events or pushState, match the appropriate route, and trigger callbacks. You shouldn't ever have to create one of these yourself — you should use the reference to Backbone.history that will be created for you automatically if you make use of Routers with routes.
[...]"
If you need the name of the function bound to that fragment, you can make something like this inside the scope of your Router:
alert( this.routes[Backbone.history.getFragment()] );
Or like this from outside your router:
alert( myRouter.routes[Backbone.history.getFragment()] );
Robert's answer is interesting, but sadly it will only work if the hash is exactly as defined in the route. If you for example have a route for user(/:uid) it won't be matched if the Backbone.history.fragment is either "user" or "user/1" (both which are the two most obvious use cases for such route). In other words, it'll only find the appropriate callback name if the hash is exactly "user(/:uid)" (highly unlikely).
Since i needed this functionality i extended the Backbone.Router with a current-function that reuses some of the code the History and Router object use to match the current fragment against the defined Routes for triggering the appropriate callback.
For my use case, it takes the optional parameter route, which if set to anything truthful will return the corresponding function name defined for the route. Otherwise it'll return the current hash-fragment from Backbone.History.fragment.
You can add the code to your existing Extend where you initialize and setup the Backbone router.
var Router = new Backbone.Router.extend({
// Pretty basic stuff
routes : {
"home" : "home",
"user(:/uid)" : "user",
"test" : "completelyDifferent"
},
home : function() {
// Home route
},
user : function(uid) {
// User route
},
// Gets the current route callback function name
// or current hash fragment
current : function(route){
if(route && Backbone.History.started) {
var Router = this,
// Get current fragment from Backbone.History
fragment = Backbone.history.fragment,
// Get current object of routes and convert to array-pairs
routes = _.pairs(Router.routes);
// Loop through array pairs and return
// array on first truthful match.
var matched = _.find(routes, function(handler) {
var route = handler[0];
// Convert the route to RegExp using the
// Backbone Router's internal convert
// function (if it already isn't a RegExp)
route = _.isRegExp(route) ? route : Router._routeToRegExp(route);
// Test the regexp against the current fragment
return route.test(fragment);
});
// Returns callback name or false if
// no matches are found
return matched ? matched[1] : false;
} else {
// Just return current hash fragment in History
return Backbone.history.fragment
}
}
});
// Example uses:
// Location: /home
// console.log(Router.current()) // Outputs 'home'
// Location: /user/1
// console.log(Router.current(true)) // Outputs 'user'
// Location: /user/2
// console.log(Router.current()) // Outputs 'user/2'
// Location: /test
// console.log(Router.current(true)) // Outputs 'completelyDifferent'
I'm sure some improvements could be made, but this is a good way to get you started. Also, it's easy to create this functionality without extending the Route-object. I did this because it was the most convenient way for my set-up.
I haven't tested this fully yet, so please let me know if anything goes south.
UPDATE 04/25/2013
I did some changes to the function, so instead of returning either the hash or route callback name, i return an object with fragment, params and route so you can access all the data from the current route, much like you would from the route-event.
You can see the changes below:
current : function() {
var Router = this,
fragment = Backbone.history.fragment,
routes = _.pairs(Router.routes),
route = null, params = null, matched;
matched = _.find(routes, function(handler) {
route = _.isRegExp(handler[0]) ? handler[0] : Router._routeToRegExp(handler[0]);
return route.test(fragment);
});
if(matched) {
// NEW: Extracts the params using the internal
// function _extractParameters
params = Router._extractParameters(route, fragment);
route = matched[1];
}
return {
route : route,
fragment : fragment,
params : params
};
}
See previous code for further comments and explanations, they look mostly the same.
To get the calling route (or url) from the called route handler, you can get it by checking
Backbone.history.location.href ... the full url
Backbone.history.location.search ... query string starting from ?
I got here in the search of this answer so I guess I should leave what I have found.
If you use the root setting for the Router, you can also include it to get the 'real' fragment.
(Backbone.history.options.root || "") + "/" + Backbone.history.fragment
Here's a tad more verbose (or, depending on your taste, more readable) version of Simon's answer:
current: function () {
var fragment = Backbone.history.fragment,
routes = _.pairs(this.routes),
route,
name,
found;
found = _.find(routes, function (namedRoute) {
route = namedRoute[0];
name = namedRoute[1];
if (!_.isRegExp(route)) {
route = this._routeToRegExp(route);
}
return route.test(fragment);
}, this);
if (found) {
return {
name: name,
params: this._extractParameters(route, fragment),
fragment: fragment
};
}
}
If you look at the source for the Router, you'll see that when the router triggers the event saying that something changes, it passes the name with it as "route:name".
http://documentcloud.github.com/backbone/docs/backbone.html#section-84
You can always hook the "route" event on the router and store it to get the current route.
router = new Backbone.Router.extend({
routes: { "stuff/:id" : "stuff" },
stuff: function(id) {}
});
router.on('route', function(route) {
this.current = route;
});
Now if you navigate to /stuff/1, router.current will be "stuff"

Resources