I am using Backbone's "all" event to catch all route events in my app in order to log the page views. This works well as long as I don't use navigate to manually trigger a route.
In the following example, I forward the user from the dashboard route to the login route. Backbone fires the event AFTER the route callback is executed, leading to the following output:
showDashboard
showLogin
route:showLogin
tracking:/login
route:showDashboard
tracking:/login
Obviously this is not what I want. I know I could call showLogin instead of using navigate to trigger the login route and this is what I am doing right now, but I would like to know why the order of the events is not the same than the order of the triggered callbacks.
Here is my router (shortened):
var AppRouter = Backbone.Router.extend({
routes: {
"/login": "showLogin",
"": "showDashboard",
},
initialize: function() {
return this.on('all', this.trackPageview);
},
trackPageview: function(eventName) {
console.log(eventName);
var url = Backbone.history.getFragment();
console.log('tracking: ' + url);
},
showDashboard: function() {
console.log('showDashboard');
// check if the user is logged in etc.
this.navigate('#/login', { trigger: true });
},
showLogin: function() {
console.log('showLogin');
}
});
Backbone's Router is actually very simple, and if you read the code you'll see the following in it's constructor:
this._bindRoutes();
this.initialize.apply(this, arguments);
_bindRoutes attaches all your routes as you expect, and it does this before your initialize function gets called. So your binding will always fire after Backbone's does.
You're probably going to be better off finding another way to do this.
You could call a before type function yourself in your routes to do stuff like track pageviews/etc. Or maybe you could just override route, track your pageview and then make sure to call Backbone's implementation with something like Backbone.Router.prototype.route.call(arguments);
Related
Having a route like 'dogs': 'process', I need to rewrite it to 'animals': 'process'.
Now, I need the router to recognize both routes, but always display the url like /animals, it is sort of aliasing, but could not find any info on how to solve this without placing an url redirect in 'process' handler.
I'm assuming that the real need for aliases is different than dogs to animals, so I'll answer regardless of if the use-case here is good or not. But if you don't want to change the hash but want to trigger different behaviors in the app, using the router is probably not the route to go.
Route aliases don't really exist in Backbone, other than defining different routes using the same callback. Depending on your exact use-case, there are multiple ways to handle similar routes.
Replace the hash
To display the same hash for a generic route coming from different routes, use the replace option of the navigate function.
routes: {
'lions': 'animalsRoute',
'animals': 'animalsRoute'
},
animalsRoute: function() {
this.navigate("#/animals", { replace: true });
// or using the global history object:
// Backbone.history.navigate("#/animals", { replace: true });
}
then handle the animals route, regardless of which route was initially used to get in this callback.
Some other answers or tutorials will say to use window.location.hash but don't. Manually resetting the hash will trigger the route regardless and may cause more trouble than it'll help.
Different behaviors but showing the same route
Just use different callbacks, both using the replace trick above.
routes: {
'lions': 'lionsRoute',
'tigers': 'tigersRoute'
},
showGenericRoute: function() {
this.navigate("#/animals", { replace: true });
},
tigersRoute: function() {
this.showGenericRoute();
// handle the tigers route
},
lionsRoute: function() {
this.showGenericRoute();
// handle the lions route
}
Notice the inexistent animalsRoute. You could add the route if there's a generic behavior if no specific animal is chosen.
Use the route params
If you want to know which animal was chosen but still use the same callback and remove the chosen animal from the hash, use the route params.
routes: {
'animals/:animal': 'animalsRoute',
},
animalsRoute: function(animal) {
// removes the animal from the url.
this.navigate("#/animals", { replace: true });
// use the chosen animal
var view = new AnimalView({ type: animal });
}
Redirect to the generic route
If you want a different behavior but always show the same route, use different callbacks, then redirect. This is useful if the generic route is in another router instance.
var Router = Backbone.Router.extend({
routes: {
'animals': 'animalsRoute'
},
animalsRoute: function() {
// handle the generic behavior.
}
});
var PussyRouter = Backbone.Router.extend({
routes: {
'lions': 'lionsRoute'
// ...
},
lionsRoute: function() {
// handle lions, then redirect
this.navigate("#/animals", { trigger: true, replace: true });
}
});
Using the trigger options will call the animalsRoute in the other router and the replace option will avoid making an entry in the history, so pushing the back button won't go to lions to get back to animals and being caught in the animals route.
Is there any event fired stating the transition/rendering has completed (and the dom is visible/ready).
setupcontroller/activate are before the dom is built/rendered
didInsertElement gets fired only the first time when I've already inserted an element and I'm just switching the model out underneath it.
What I'm really looking for is the transition is complete event
I guess I can do this, but I was kind of hoping it was already built in...
Ember.Router.reopen({
didTransition:function(infos) {
this._super(infos);
console.log('transition complete');
}
});
Even cooler would be a callback to the route that the transition completed for it, I may have to write this and submit a pull request.
There are a couple of different ways you can solve this
didInsertElement
This is fired when the view is inserted on the first time, but not fired if the model is switched out under the view (because Ember likes to reuse items, since it's cheaper than rebuilding the entire DOM). Example below.
Simple
If you only need to do it once, the first time the view is inserted, use didInsertElement
App.FooView = Em.View.extend({
setupSomething: function(){
console.log('the dom is in place, manipulate');
}.on('didInsertElement')
});
Example: http://emberjs.jsbin.com/wuxemo/1/edit
Complex
If you need to schedule something after the DOM has been rendered from the route itself, you can use schedule and insert it into the afterRender queue.
App.FooRoute = Em.Route.extend({
setupController: function(controller, model){
this._super('controller', model);
Ember.run.schedule('afterRender', this, function () {
//Do it here
});
}
});
Example: http://emberjs.jsbin.com/wuxemo/2/edit
Transition promise
The transition's promise will complete before it's finished rendering items. But it gives you a hook for when it's done with fetching all of the models and controllers and hooking them up.
If you want to hook up to the transition event you can do it like so:
var self = this;
transitionTo('foo').then(function(){
Ember.run.schedule('afterRender', self, function () {
//Do it here
});
})
The afterModel hook might work for you:
App.MyRoute = Ember.Route.extend({
afterModel: function(model, transition) {
transition.then(function() {
// Done transitioning
});
}
});
I tested this using RC-7 with routes that both do and don't have dynamic segments (i.e., a route with a model and a route without a model). It seems to work either way.
See this JSBin for an RC-6 example:
Output: http://jsbin.com/OteC/1/
Source: http://jsbin.com/OteC/1/edit?html,js
setupController is the last thing that the Router calls before finalizing the transition. And if it completes without errors, as far as Ember is concerned the transition is complete. You actually see this in action by enabling LOG_TRANSITIONS_INTERNAL.
At that point, It doesn't matter if the controller has thrown an error, view has thrown an error, etc. The router has completed transitioning into the target route.
So setupController is the last place in terms of the Router that corresponds to didTransition.
When the content/model backing the controller changes on an existing View, the bindings kick in. Most of the changes that happen to the view at that point are via Metamorphing.
The closest place I can think of to hook into would be View.render which pushes changes into the RenderBuffer. But you still need to account for Metamorphing via mixins that happens here.
didTransition does exist as you hoped -- but its an action and not a hook
XXRouter
actions: {
didTransition: function() {
this.controller.set("hasTransitioned", true); // or whatever is needed?!
return true; // Bubble the didTransition event
},
}
XXController
observeTransition: function() {
alert('complete Transition');
}.observes('hasTransitioned'),
I have this Backbone App where users and admins can log in and out. Now admins do have multiple options to add features on different pages, so for that, I have an admin-menu which should only display the relevant buttons on the relevant page. I want to use the trigger-method but cant get it working properly. So lets say, admins should have the possibility to change something on the frontpage, so a specific button should be visible, only on when navigating to the frontpage. This is what I did so far:
Router.js:
routes: {
'': 'index',
'home': 'home'
}
home: function(){
App.trigger('showFrontBtn');
}
Then on my Admin-MenuView.js:
Admin-Menu.View = Backbone.View.extend({
tagName: 'div',
template: 'adminMneu',
initialize: function() {
this.render();
App.on('showFrontBtn', this.changeFront, this);
},
changeFront:function(user){
alert('works!')
if(user && user.role === 'admin'){
$('.frontBtn').show();
} else {
$('.frontBtn').hide();
}
},
The thing is, that it actually returns the alert('works'), so I assume there must be an issue with the if statement, BUT am I using the method correctly?
The function changeFront:function(user) is expecting a "user" object passed into it. But when you triggered the event, you didn't specify the parameter to pass (user).
I am guessing when you tried to access "user.role" it threw an exception because user is null and you are trying to get to null.role.
You might be confusing the third argument of the event listener with the parameter of the function. The third parameter of the app.on is actually the context not the parameter. The parameter needs to be passed by the trigger. So you can do something like
home: function(){
App.trigger('showFrontBtn', {role: "Admin"});
}
Is there any event fired stating the transition/rendering has completed (and the dom is visible/ready).
setupcontroller/activate are before the dom is built/rendered
didInsertElement gets fired only the first time when I've already inserted an element and I'm just switching the model out underneath it.
What I'm really looking for is the transition is complete event
I guess I can do this, but I was kind of hoping it was already built in...
Ember.Router.reopen({
didTransition:function(infos) {
this._super(infos);
console.log('transition complete');
}
});
Even cooler would be a callback to the route that the transition completed for it, I may have to write this and submit a pull request.
There are a couple of different ways you can solve this
didInsertElement
This is fired when the view is inserted on the first time, but not fired if the model is switched out under the view (because Ember likes to reuse items, since it's cheaper than rebuilding the entire DOM). Example below.
Simple
If you only need to do it once, the first time the view is inserted, use didInsertElement
App.FooView = Em.View.extend({
setupSomething: function(){
console.log('the dom is in place, manipulate');
}.on('didInsertElement')
});
Example: http://emberjs.jsbin.com/wuxemo/1/edit
Complex
If you need to schedule something after the DOM has been rendered from the route itself, you can use schedule and insert it into the afterRender queue.
App.FooRoute = Em.Route.extend({
setupController: function(controller, model){
this._super('controller', model);
Ember.run.schedule('afterRender', this, function () {
//Do it here
});
}
});
Example: http://emberjs.jsbin.com/wuxemo/2/edit
Transition promise
The transition's promise will complete before it's finished rendering items. But it gives you a hook for when it's done with fetching all of the models and controllers and hooking them up.
If you want to hook up to the transition event you can do it like so:
var self = this;
transitionTo('foo').then(function(){
Ember.run.schedule('afterRender', self, function () {
//Do it here
});
})
The afterModel hook might work for you:
App.MyRoute = Ember.Route.extend({
afterModel: function(model, transition) {
transition.then(function() {
// Done transitioning
});
}
});
I tested this using RC-7 with routes that both do and don't have dynamic segments (i.e., a route with a model and a route without a model). It seems to work either way.
See this JSBin for an RC-6 example:
Output: http://jsbin.com/OteC/1/
Source: http://jsbin.com/OteC/1/edit?html,js
setupController is the last thing that the Router calls before finalizing the transition. And if it completes without errors, as far as Ember is concerned the transition is complete. You actually see this in action by enabling LOG_TRANSITIONS_INTERNAL.
At that point, It doesn't matter if the controller has thrown an error, view has thrown an error, etc. The router has completed transitioning into the target route.
So setupController is the last place in terms of the Router that corresponds to didTransition.
When the content/model backing the controller changes on an existing View, the bindings kick in. Most of the changes that happen to the view at that point are via Metamorphing.
The closest place I can think of to hook into would be View.render which pushes changes into the RenderBuffer. But you still need to account for Metamorphing via mixins that happens here.
didTransition does exist as you hoped -- but its an action and not a hook
XXRouter
actions: {
didTransition: function() {
this.controller.set("hasTransitioned", true); // or whatever is needed?!
return true; // Bubble the didTransition event
},
}
XXController
observeTransition: function() {
alert('complete Transition');
}.observes('hasTransitioned'),
My view should be destroyed after the current route position is left.
So in this schematic example the login view should be destroyed after the user entered his credentials:
I tried to solve this by using Backbone.Router events:
var Router = Backbone.Router.extend({
initialize: function () {
Backbone.history.start();
},
routes: {
"sample" : "sample"
},
sample: function(){
// Build view
var demoView = $("<div/>")
.appendTo(document.body)
.text("I am lost!");
// Destroy view
this.once('route', function(){
demoView.remove();
});
},
});
Unfortunately this does not work as the route events are raised after the routes are executed:
http://jsfiddle.net/hcuX9/
Is there a solution to destroy views after leaving the route position?
Do I have to hack a new event into Backbone.js?
What I use to do is to have an App.current variable pointing to the current view being rendered.
At the top of each route (or the relevant ones in your case), I remove the current view from App.current and then assign it the new view:
someRoute: function() {
if(App.current && App.current.remove) App.current.remove();
// Asign a new current page
App.current = new SomeView();
...
}
That way I only let one view live per route, getting rid of problems like yours.
If you don't like to be checking for App.current and invoking the remove method at the top of every route, you can listen for Backbone.history route event and injecting that logic there:
Backbone.history.on('route', function() {
if(App.current && App.current.remove) App.current.remove();
});
I think you are stuck with your hack, unless you can adapt .listenTo to your needs - then you will need to fire a custom event with .trigger anywhere you have a route change, which might not be possible. Note that this functionality has been requested (and denied) before in backbone:
https://github.com/documentcloud/backbone/pull/494
See that pull request for other patches that try to do the same thing you are doing.
Here, we're using on and off to listen for route events coming in instead of once because we can't rely on a single event not being the current route. When we receive a route even that is not our current route, we can destroy the view and remove the listener:
// Destroy view
var self = this;
var onRoute = function(route, params){
if(route !== 'sample'){
demoView.remove();
self.off('route', onRoute);
}
};
this.on('route', onRoute);
I've modified your test fiddle here: http://jsfiddle.net/rgthree/hcuX9/3/
Another option, as your fiddle (not in your question) navigates directly to another view. This causes the other route's event to fire after the sample2 route. Because of this the above will remove the view. Now, it's much more complete. A hackier way you could handle it is to simply defer the once in a setTimeout so it doesn't listen until after the current route has been fired:
// Destroy view
var self = this;
setTimeout(function(){
self.once('route', function(){
demoView.remove();
});
}, 0);
You can see your fiddle with this method here: http://jsfiddle.net/rgthree/hcuX9/4/