Can't figure out what's going wrong with my Backbone router. Can anyone spot a mistake in the following block of code? The index route is working fine but the classes route isn't ever triggering (e.g. when I navigate to a URL like localhost/classes/test)
var app = app || {};
$(function() {
app.Router = Backbone.Router.extend({
routes: {
'' : 'index',
'classes/:id' : 'classes'
},
initialize: function() {
this.classList = new app.ClassCollection();
},
index: function() {
this.menuView = new app.ClassCollectionView({collection: this.classList});
},
classes: function(id) {
console.log("hello")
var _class = new app.ClassModel({id: id});
this.classView = new app.ClassPageView({model: _class});
}
});
router = new app.Router();
Backbone.history.start({pushState: true});
})
If everything looks in order, there's probably a bug somewhere else in my code.
Backbone.router is extending hashbang navigation.
so
localhost/#classes/test
should lead to your method. ALSO! pay attention that emty route should be at the end of the routes list.
It's like else if construction, if route matches "" (default # ?!) it will never match other routes
by default the route will work with the hash try localhost/#classes/test
Related
I am having a trouble with my router and controller. On my app's before:start, I have a handler that fetches collections of Leads and Vehicles.
I have a single region, with my layout view as:
var App = new Marionette.Application({});
var AppLayoutView = Marionette.LayoutView.extend({
el: 'body',
regions: {
main: '#app-container'
}
});
My controller is:
var Controller = Marionette.Object.extend({
leads: function() {
App.regions.main.show(new App.leadsTableView({collection: App.leads}));
},
vehicles: function() {
App.regions.main.show(new App.vehiclesTableView({collection: App.vehicles}));
}
});
In my start handler:
App.on('start', function() {
App.regions = new AppLayoutView();
App.router = new Marionette.AppRouter({
controller: new Controller(),
appRoutes: {
'leads': 'leads',
'vehicles': 'vehicles'
}
});
Backbone.history.start({pushState: true});
});
App.start();
How can I start with a specific route? And, when a user goes to #vehicles, how can I make the region load the new view? I'm missing something about routers.
EDIT: When I go to, #leads in my URL, my vehicles view comes up. When I click on links that go to #leads and #vehicles, they don't work.
Default route
You can define a default by adding a "splat" route (one that starts with *) to the end of your routes. I like to use *default to make the intent obvious:
appRoutes: {
'leads': 'leads',
'vehicles': 'vehicles',
'*default': 'leads'
}
Broken links
Because you are using pushstate routing, the view URL is /vehicles rather than the hash fragment #vehicles. You should no longer use hash fragment urls.
Here's a simple approach to trigger pushState routes with link clicks:
$('a[href]').on('click', function(e) {
e.preventDefault();
var href = e.target.getAttribute('href');
App.router.navigate(href, { trigger: true })
});
You may find this post about moving from hash fragment to pushState routing useful.
You'll also need to configure your server to pass requests that match your route to the main app page - for example, it needs to understand that http://localhost/app/vehicle should be handled by http://localhost/app.
I have a backboneJS app that has a router that looks
var StoreRouter = Backbone.Router.extend({
routes: {
'stores/add/' : 'add',
'stores/edit/:id': 'edit'
},
add: function(){
var addStoresView = new AddStoresView({
el: ".wrapper"
});
},
edit: function(id){
var editStoresView = new EditStoresView({
el: ".wrapper",
model: new Store({ id: id })
});
}
});
var storeRouter = new StoreRouter();
Backbone.history.start({ pushState: true, hashChange: false });
and a model that looks like:
var Store = Backbone.Model.extend({
urlRoot: "/stores/"
});
and then my view looks like:
var EditStoresView = Backbone.View.extend({
...
render: function() {
this.model.fetch({
success : function(model, response, options) {
this.$el.append ( JST['tmpl/' + "edit"] (model.toJSON()) );
}
});
}
I thought that urlRoot when fetched would call /stores/ID_HERE, but right now it doesn't call that, it just calls /stores/, but I'm not sure why and how to fix this?
In devTools, here is the url it's going for:
GET http://localhost/stores/
This might not be the answer since it depends on your real production code.
Normally the code you entered is supposed to work, and I even saw a comment saying that it works in a jsfiddle. A couple of reasons might affect the outcome:
In your code you changed the Backbone.Model.url() function. By default the url function is
url: function() {
var base =
_.result(this, 'urlRoot') ||
_.result(this.collection, 'url') ||
urlError();
if (this.isNew()) return base;
return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id);
},
This is the function to be used by Backbone to generate the URL for model.fetch();.
You added a custom idAttribute when you declared your Store Model to be like the one in your DB. For example your database has a different id than id itself, but in your code you still use new Model({ id: id }); when you really should use new Model({ customId: id });. What happens behind the scenes is that you see in the url() function it checks if the model isNew(). This function actually checks if the id is set, but if it is custom it checks for that:
isNew: function() {
return !this.has(this.idAttribute);
},
You messed up with Backbone.sync ... lots of things can be done with this I will not even start unless I want to make a paper on it. Maybe you followed a tutorial without knowing that it might affect some other code.
You called model.fetch() "a la" $.ajax style:
model.fetch({
data: objectHere,
url: yourUrlHere,
success: function () {},
error: function () {}
});
This overrides the awesomeness of the Backbone automation. (I think sync takes over from here, don't quote me on that).
Reference: Backbone annotated sourcecode
I've implemented "change page" in my one page application with Backbone.js. However, I'm not sure if my Router should contain so much business logic. Should I consider go with Marionette.js to implement such functionality and make my Router thin? Should I worry about destroying Backbone models and views attached to "previous" active page/view when I change it (in order to avoid memory leaks) or it's enough to empty html attached to those models/views.
Here is my Router:
App.Router = Backbone.Router.extend({
routes: {
'users(/:user_id)' : 'users',
'dashboard' : 'dashboard'
},
dashboard: function() {
App.ActiveView.destroy_view();
App.ActiveViewModel.destroy();
App.ActiveViewModel = new App.Models.Dashboard;
App.ActiveViewModel.fetch().then(function(){
App.ActiveView = new App.Views.Dash({model: App.ActiveViewModel });
App.ActiveView.render();
});
},
users: function(user_id) {
App.ActiveView.destroy_view();
App.ActiveViewModel.destroy();
App.ActiveViewModel = new App.Models.User;
App.ActiveViewModel.fetch().then(function() {
App.ActiveView = new App.Views.UsersView({model: App.ActiveViewModel});
App.ActiveView.render();
});
}
});
Another approach:
Create an AbstractView
Having an AbstractView declared and then extending your other application specific View's from AbstractView has many advantages. You always have a View where you can put all the common functionalities.
App.AbstractView = Backbone.View.extend({
render : function() {
App.ActiveView && App.ActiveView.destroy_view();
// Instead of destroying model this way you can destroy
// it in the way mentioned in below destroy_view method.
// Set current view as ActiveView
App.ActiveView = this;
this.renderView && this.renderView.apply(this, arguments);
},
// You can declare destroy_view() here
// so that you don't have to add it in every view.
destroy_view : function() {
// do destroying stuff here
this.model.destroy();
}
});
Your App.Views.UsersView should extend from AbstractView and have renderView in place of render because AbstractView's render will make a call to renderView. From the Router you can call render the same way App.ActiveView.render();
App.Views.UsersView = AbstractView.extend({
renderView : function() {
}
// rest of the view stuff
});
App.Views.Dash = AbstractView.extend({
renderView : function() {
}
// rest of the view stuff
});
Router code would then change to :
dashboard: function() {
App.ActiveViewModel = new App.Models.Dashboard;
App.ActiveViewModel.fetch().then(function(){
new App.Views.Dash({model: App.ActiveViewModel }).render();
});
}
I've got a router defined as such:
var MyRouter = Backbone.Router.extend({
routes: {
// catch-all for undefined routes
"*notfound" : "notFound",
},
initialize: function(options) {
this.route("my_resource/clear_filters", 'clearFilters');
this.route("my_resource/:id", 'show');
},
show: function(id){
console.log('show', id);
},
clearFilters: function() {
console.log('clearFilters');
},
notFound: function() {
console.log('notFound');
},
});
var app = {};
app.MyRouter = new MyRouter();
Backbone.history.start({silent: true});
Thus the following URLs would map as:
var opts = {trigger: true};
app.MyRouter.navigate('/foo', opts); // logged -> 'notFound'
app.MyRouter.navigate('/my_resource/123', opts); // logged -> 'show', '123'
app.MyRouter.navigate('/my_resource/clear_filters', opts); // logged -> 'clearFilters'
app.MyRouter.navigate('/my_resource/some_thing', opts); // logged -> 'show', 'some_thing'
How can I restrict the my_resource/:id route to only match on numeric parameters so that app.MyRouter.navigate('/my_resource/some_thing') is handled by notFound?
From the fine manual:
route router.route(route, name, [callback])
Manually create a route for the router, The route argument may be a routing string or regular expression. Each matching capture from the route or regular expression will be passed as an argument to the callback.
So you can always say things like:
this.route(/my_resource\/(\d+)/, 'show')
in your router's initialize if you need finer grained control over the routes than Backbone's string patterns give you.
What I Have
Trying to understand what's going on and how to control it. I have a "public" view for users that have not yet been authenticated, and a "home" view for users that are authenticated. Here's my route config:
app.start().then(function() {
//Replace 'viewmodels' in the moduleId with 'views' to locate the view.
//Look for partial views in a 'views' folder in the root.
viewLocator.useConvention();
//configure routing
router.useConvention();
router.mapRoute('home', 'viewmodels/home', 'Test App', true);
router.mapRoute('public', 'viewmodels/public', 'Test App', true);
router.mapRoute('set/:id', 'viewmodels/set', 'Set');
router.mapRoute('folder/:id', 'viewmodels/folder', 'Folder');
router.mapRoute('api', 'viewmodels/api', 'API Reference');
router.mapRoute('error', 'viewmodels/error', 'Error', false);
app.adaptToDevice();
//Show the app by setting the root view model for our application with a transition.
if (dataservice.isAuthenticated() === true) {
app.setRoot('viewmodels/shell', 'entrance');
router.navigateTo('home');
} else {
app.setRoot('viewmodels/public');
router.navigateTo('#/public');
}
router.handleInvalidRoute = function (route, params) {
logger.logError('No route found', route, 'main', true);
router.navigateTo('#/error');
};
});
The Problems
When I run the app for the first time, I'm not authenticated, and I get an error:
Uncaught TypeError: Cannot call method 'lookupRoute' of undefined
Originating from the 'router.navigateTo('#/public');' line.
Then when I try to click the login button, I get the same error from this:
define(['durandal/app', 'durandal/plugins/router', 'services/dataservice'], function (app, router, dataservice) {
var publicViewModel = function () {
self.logIn = function () {
app.setRoot('viewmodels/shell');
router.navigateTo('#/home');
};
But the content loads correctly. When I navigate to a particular page by clicking, say to /folder/2, and then change the url to /folders/2 (invalid), I get "route not found" in my log, as expected, but I run into a few other issues:
I don't get the error page, or any errors (as I think I should, per my handleInvalidRoute)
If I click on something else, the url doesn't change, and new content isn't loaded, again with no errors.
I think I'm breaking routing somehow, but I'm not sure how. How can I correct the above issues?
Screen:
I suspect calling navigateTo where you are might be too soon for some reason. To test this theory try move this code.
if (dataservice.isAuthenticated() === true) {
app.setRoot('viewmodels/shell', 'entrance');
router.navigateTo('home');
} else {
app.setRoot('viewmodels/public');
router.navigateTo('#/public');
}
into an "activate" method on your publicviewmodel, and in the main.js just leave this:
app.setRoot('viewmodels/public');
EDIT: Old suggestion
I believe on your viewmodel for the root you need a router property. So modify your public viewmodel to add one:
define(['durandal/app', 'durandal/plugins/router', 'services/dataservice'], function (app, router, dataservice) {
var publicViewModel = function () {
self.router = router;
self.logIn = function () {
app.setRoot('viewmodels/shell');
router.navigateTo('#/home');
};
(where do you define self though?)