How to use Backbone's trigger method - javascript

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

Related

How to do a route alias within a Backbone Router?

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.

setupController not being called when using {{linkTo}} or transtionTo("path", model);

Is there any reason why setupController would not get called when using {{linkTo}}? I have two instances in my app where linkTo is being used, and in the second case. It doesn't work. The only difference that I can see is that in the first case linkTo is being used in a loop, and in the second it's not. Below is relevant code for the non-working one:
App.Router.map(function() {
this.resource("search", { path: "/search/:args" });
});
App.SearchCriteria = Ember.Object.extend({ });
App.SearchRoute = Ember.Route.extend({
serialize: function(model, params) {
// .. some code that converts model to a string called args
return {'args': args}
},
model: function(params) {
// convert args, which is query string-formatted, to an object
// and then make a App.SearchCriteria object out of it.
return App.SearchCriteria.create($.deparam(params.args));
},
setupController: function(controller, model) {
controller.set("searchCriteria", model);
}
});
In the search template:
{{view Ember.Checkbox checkedBinding="searchCriteria.music"}} Music
{{#linkTo search searchCriteria}}Search{{/linkTo}}
The last thing I see in the logs is:
Transitioned into 'search'
Normally, I'd see the setupController being called at some point, but it's not happening or some reason. I even tried using the {{action}} method to call a handler and then use transtionTo, but that had the same results.
UPDATE 1: Adding more details
The only difference between the working and non-working cases is that in the working case, the {{linkTo}} is being called from the same template as that of the controller and router (i.e., the linkTo is in the search template and it's invoking the SearchRoute). In the working case, the linkTo is being called on the SearchRoute but from a different template belonging to a different router).
After some Chrome debugging of Ember code, I found out that the router isn't being called is because partitioned.entered is empty. In the working case, it is non-empty.
var aborted = false;
eachHandler(partition.entered, function(handler, context) {
if (aborted) { return; }
if (handler.enter) { handler.enter(); }
setContext(handler, context);
if (handler.setup) {
if (false === handler.setup(context)) {
aborted = true;
}
}
});
UPDATE 2: Root issue found - bug?
I think I understand the root cause of why the handler isn't being triggered, and I think it's because the partitionHandlers(oldHandlers, newHandlers) method doesn't think that the model has changed, thus doesn't fire the handler.
To be specific, this is the relevant part of the view:
{{view Ember.Checkbox checkedBinding="searchCriteria.music"}} Music
{{#linkTo search searchCriteria}}Search{{/linkTo}}
Although the user checks off the checkbox (thus changing the state of searchCriteria), Ember doesn't think that searchCriteria is any different, thus doesn't do anything.
Is this a bug?
I'm not sure what your problem is, but this may help.
setupController is called every time the route is entered. But model hook may not be called every time.
See Ember guide: http://emberjs.com/guides/routing/specifying-a-routes-model/
Note: A route with a dynamic segment will only have its model hook called when it is entered via the URL. If the route is entered through a transition (e.g. when using the link-to Handlebars helper), then a model context is already provided and the hook is not executed. Routes without dynamic segments will always execute the model hook.
Genrally speaking, if you click the link generated by link-to to enter the route, Ember will not call model hook for that route. Instead it passes the model (link-to parameter) to that route.
The philosophy here is since the client already has the model context, Ember think there is no need to get it again from server (that's model hook's job).

having issue with backbonejs router

Scenario
I am working on backbone app. What is happening right now is when user clicks edit link on page then it should show a form. I am trying to implement this using backbone routers rather than events. With events object it works perfectly fine. To use routers, I am using global events.
Problem
The problem is that when user clicks on edit link, it shows me following error in console
Uncaught TypeError: Object 10 has no method 'toJSON' views.js:57
This error is because on line 57 in views.js, I am using this.model.toJSON() whereas I am not passing model via router. I don't know how pass model through router
Here is my router. Note: All of the following codes are in separate files
App.Router = Backbone.Router.extend({
routes: {
'contacts/:id/edit': 'editContact'
},
editContact: function (id) {
console.log('yahhhhh');
vent.trigger('contact:edit', id);
}
});
In above router I am triggering an event inside editContact function. Then I am listening to above event in following initialize function.
App.Views.App = Backbone.View.extend({
initialize: function () {
vent.on('contact:edit', this.editContact, this);
},
editContact: function (contact) {
var editContactView = new App.Views.EditContact({ model: contact });
$('#editContact').html(editContactView.render().el);
}
});
Now in above after listening to event in initialize function, I am calling editContact function and I am also passing model using this keyword. Inside editContact function, I am creating an instance of EditContact, view which is following, and then rendering a form which needs to be shown.
App.Views.EditContact = Backbone.View.extend({
template: template('editContactTemplate'),
render: function () {
var html = this.template(this.model.toJSON()); //<--- this is line 57
this.$el.html(html);
return this;
}
});
After doing all of the above, the form is not shown and I am getting above mentioned error.
Question
How do I pass model to render function inside EditContact via router so that it starts working?
UPDATE
Here is my model
App.Models.Contact = Backbone.Model.extend({
urlRoot : '/contacts'
});
In your editContact method the argument contact refers to the id you pass onwards from the router. When you initialize a new view with new App.Views.EditContact({ model: contact }) the model of the view will, expectedly, be the id.
You need to map the id into a model instance. IMHO the correct place to do this is in the router:
editContact: function (id) {
var contact = new App.Models.Contact({id:id});
vent.trigger('contact:edit', contact);
}
Notice that at this point the model will only have the id property set. If you need to populate the model properties for editing, you should fetch the model from the server, and only then trigger the event:
editContact: function (id) {
var contact = new App.Models.Contact({id:id});
contact.fetch().done(function() {
vent.trigger('contact:edit', contact);
});
}
Edit based on comments: Generally speaking you shouldn't pass anything to the router. The router should be a starting point for every new request (url change). If you want to hold some state between page changes, you should store the data on the router level, and pass the models and collections "down" from the view.
In a simplified scenario this would mean initializing and storing a reference to the collection in the router. Something like:
var Router = Backbone.Router.extend({
initialize: function() {
this.contactCollection = new App.Collections.Contacts();
},
editContact: function (id) {
id = parseInt(id, 10);
if(_.isNaN(id)) {
//error...
}
//try to get a model from the collection
var contact = this.contactCollection.get(id);
//if not found, create, add and fetch
if(!contact) {
contact = new App.Models.Contact({id:id});
this.contactCollection.add(contact);
contact.fetch().done(function() {
vent.trigger('contact:edit', contact);
});
} else {
vent.trigger('contact:edit', contact);
}
}
});
Please note that this is just example code, and not necessarily how you should implement it, line by line. You should consider whether it's OK to display a potentially stale model in the view, or whether you should always fetch it from the server. In practice you might also abstract the collection state in a nice class, instead of handling it directly in the router.
Hope this answers your questions.

Order of events triggerd by Backbone.Router when using navigate

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

backbone.js - controller properties from a view

I have a controller property called authenticated which defaults to false. However, in my login view I need to be able to set it to true. Also, in my logout view I need to be able to set it to false. How can I expose this property within the view?
var Controller = Backbone.Controller.extend({
...
authenticated: false,
login: function() {
if(this.authenticated)
{
location.hash = '!/dashboard';
} else {
new LoginView();
}
},
logout: function() {
$.post('/admin/logout', {},
function(resp){
}, "json");
this.authenticated = false;
location.hash = '!/login';
}
...
});
Your controller is correctly doing the login and logout functionality. All you need to do is have your view fire backbone.js events and have the controller be registered to receive those.
Somewhere in your controller, you need something like:
var loginView = new LoginView(...); // params as needed
loginView.bind("login_view:login", this.login);
loginView.bind("login_view:logout", this.logout);
loginView.render();
Also, you need to assure that the controller is set up to handle events, so something like this is needed in your initialize function:
_.extend(this, Backbone.Events);
_.bindAll(this, "login", "logout");
Your view will need the event code, so be sure to add the _.extend(...) call into its initialize.
In your view where appropriate, you need:
this.trigger("login_view:login");
and
this.trigger("login_view:logout");
As a final note, you want the controller to do the login and logout server calls. All you need from the view is an event and potentially a populated model or data otherwise. This data would be passed as a parameter in the trigger statement(s) and would be received as an argument in the login/logout functions. I have not included this in the code, however.
You basically want the view to manage the DOM and bubble up application events to the controller. The controller can then mediate with the server and manage any necessary views.

Categories