Translating the `on` pattern to Backbone's `event` hash - backbone.js

Inside my Backbone views, in the initialize function I do stuff like:
initialize: function () {
$(this.el).on('click', '.button', function () {
$(this).fadeTo(0.5);
}
}
This seems to go against Backbone's convention of using events. Rewriting with the events hash:
events: { 'click .button': 'fadeButton' },
fadeButton: function () {
$(this).fadeTo(0.5);
}
The problem is inside fadeButton's scope the value of this is not the same as when using .on(). What is the correct way of doing this using the events hash?

Like paul said, Backbone automatically sets the context for event callbacks to the view itself. So this in the callback will be the view instance.
So you can get the effect you intend by using the view's scoped selector function...
events: {
'click .button': 'fadeButton'
},
fadeButton: function () {
this.$('.button').fadeTo(0.5);
}
... but if you've got multiple elements with class "button" in your view, that'll fade all of them, in which case you can always use the event object that jQuery gives you to get the event target:
fadeButton: function (event) {
$(event.target).fadeTo(0.5);
}

You defined the events hash correctly.
And for every event handler defined, Backbone automatically sets the context to the view. So this within fadeButton is the view, and you will want to access the view's element.
The code below shows how you need to update the fadeButton function.
fadeButton: function () {
$(this.el).fadeTo(0.5);
}

Related

_.after: a confuse with context

I have extend this function inside Backbone.view
toggle: function () {
var sempurna = _.after(array_obj.length, this.render);
_.each(array_obj, function (v,k) {
v.perormSomething();
delete array_obj[key];
sempurna();
}, this);
}
And so I thought that I could render the view straight away the loop is completed. But somehow the this keyword is refering to window instead of the view. How do I point to the intended this to view.
The this is set to window because your are calling sempurna() without the dot notation (so without any explicit receiving object).
To fix this you need to _.bind (or use the browser native bind if available) your sempurna to this:
toggle: function () {
var sempurna = _.bind(_.after(array_obj.length, this.render), this);
_.each(array_obj, function (v,k) {
v.perormSomething();
delete array_obj[key];
sempurna();
}, this);
}
Demo JSFiddle.

events not firing after re-render in backbone.js

I am facing a problem while trying to click submit after re-render.
This is my view:
ShareHolderInfoView = Backbone.View.extend( {
template : 'shareholderinfo',
initialize: function() {
this.model = new ShareHolderInfoModel();
},
render : function() {
$.get("shareholderinfo.html", function(template) {
var html = $(template);
that.$el.html(html);
});
//context.loadViews.call(this);
return this;
},
events:{
"change input":"inputChanged",
"change select":"selectionChanged",
"click input[type=submit]":"showModel"
},
inputChanged:function(event){
var field = $(event.currentTarget);
var data ={};
data[field.attr('id')] = field.val();
this.model.set(data);
},
showModel:function(){
console.log(this.model.attributes);
alert(JSON.stringify(this.model.toJSON()));
}
});
This is my Router
var shareholderInfo, accountOwnerInfo;
App.Router = Backbone.Router.extend({
routes:{
'share':'share',
'joint':'joint'
},
share:function(){
$("#subSection").empty();
if(!shareholderInfo){
shareholderInfo = new ShareHolderInfoView();
$("#subSection").append(shareholderInfo.render().el);
} else{
$("#subSection").append(shareholderInfo.$el);
}
},
joint:function(random){
$("#subSection").empty();
if(!accountOwnerInfo){
accountOwnerInfo = new AccountOwnerInfoView();
$("#subSection").append(accountOwnerInfo.render().el);
} else{
$("#subSection").append(accountOwnerInfo.$el);
}
}
});
This is my HTML a div with id='subSection'.
if I check in console, I can able to see the events bound to that view.
Object {change input: "inputChanged", change select: "selectionChanged", click input[type=submit]: "showModel"}
But its not calling that showModel function afer i click submit. Please help.
Your fundamental problem is that you're improperly reusing views.
From the fine manual:
.empty()
Description: Remove all child nodes of the set of matched elements from the DOM.
[...]
To avoid memory leaks, jQuery removes other constructs such as data and event handlers from the child elements before removing the elements themselves.
So when you say:
$("#subSection").empty();
you're not just clearing out the contents of #subSection, you're also removing all event handlers attached to anything inside #subSection. In particular, you'll remove any event handlers bound to accountOwnerInfo.el or shareholderInfo.el (depending on which one is already inside #subSection).
Reusing views is usually more trouble than it is worth, your views should be lightweight enough that you can destroy and recreate them as needed. The proper way to destroy a view is to call remove on it. You could rewrite your router to look more like this:
App.Router = Backbone.Router.extend({
routes: {
'share':'share',
'joint':'joint'
},
share: function() {
this._setView(ShareHolderInfoView);
},
joint: function(random){
this._setView(AccountOwnerInfoView);
},
_setView: function(view) {
if(this.currentView)
this.currentView.remove();
this.currentView = new view();
$('#subSection').append(this.currentView.render().el);
}
});
If your views need any extra cleanup then you can override remove on them to clean up the extras and then chain to Backbone.View.prototype.remove.call(this) to call the default remove.
If for some reason you need to keep your views around, you could call delegateEvents on them:
delegateEvents delegateEvents([events])
Uses jQuery's on function to provide declarative callbacks for DOM events within a view. If an events hash is not passed directly, uses this.events as the source.
and you'd say things like:
$("#subSection").append(shareholderInfo.$el);
shareholderInfo.delegateEvents();
instead of just:
$("#subSection").append(shareholderInfo.$el);
I'd strongly recommend that you treat your views and cheap ephemeral objects: destroy them to remove them from the page, create new ones when they need to go on the page.

ItemView event not working when triggered in onClose handler

I have a Backbone Marionette CollectonView that is listening for an event in its child views as follows:
this.on(this, 'itemview:timeline:storyRemoved', this._storyRemoved);
The itemview triggers the event during its onClose:
onClose: function () {
this.trigger('timeline:storyRemoved', { model: this.model });
}
But the _storyRemoved function is never called.
If I move the trigger to onShow then it works fine:
onShow: function () {
this.trigger('timeline:storyRemoved', { model: this.model });
}
I'm guessing it's something to do with the fact that the view has been closed and is therefore not in the collection view any longer?
Any way of getting this to work?
You're right that the view is not listening because it has already been closed when you trigger the event.
Depending on what you're doing, shouldn't the model itself trigger this event? That way, you could add something like this in your collection view:
collectionEvents: {
'remove': '_storyRemoved'
}
As you can see here, Removing a model will trigger a remove event with a reference to the model that was removed.
I like gbsice's point here, and I also wanted to add a more general answer: you can use onBeforeClose rather than onClose if you need something executed prior to the close event.
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.collectionview.md#onbeforeclose-callback
So in your case, it would be
onBeforeClose: function () {
this.trigger('timeline:storyRemoved', { model: this.model });
}

Prevent Backbone event from triggering more than once, how to check if event is registered already?

In my router object, I created an event object to share among my views
I pass the event object to my views
I register events to this shared object like this
var productCatalogView = Backbone.View.extend({
initialize: function (options) {
//bind alert event to sharedEvents
options.sharedEvents.bind("alert", this.alert,this);
},
alert: function () {
alert('alerted');
}
});
//The following view triggers the alert event
var testView = Backbone.View.extend({
initialize: function (options) {
this.sharedEvents = options.sharedEvents;
},
events: {
'click #test': 'triggerAlert'
},
triggerAlert: function (e) {
this.sharedEvents.trigger("alert", null);
}
});
THE PROBLEM:
The problem I experience is that the first time I click on the button which triggers the alert event (second view), the alert event gets called once (good), this causes the first view to be re-rendered by triggering the route passing search parameters, therefore creating the first view and binding the sharedEvents again, hence when I trigger the alert event a second time, it gets triggered twice (bad), the next time I repeat the same process, it gets triggered 3 times, and so on and so forth. I guess it has to do with the event binding in the first view, it occurs more than once, i.e each time the view is initialized (if I am correct)
please how can I make the binding of the event occur once.
Here is my router which shows how I initilze the views:
var Router = Backbone.Router.extend({
sharedEvents:_.extend({},Backbone.Events),
catalog: function (id) {
//....unecessary code left out
var productView = new ProductView({sharedEvents:this.sharedEvents});
this.renderView(productView);
this.renderView(new testView({sharedEvents: this.sharedEvents }));
}
renderView: function (view) {
if (null != this.currentView) {
this.currentView.undelegateEvents();
// this.currentView.remove();
}
this.currentView = view;
this.currentView.render();
}
});
I have tried this solution but problem persists, thanks
Try using Backbone.Events' listenTo method instead of the bind method. Then, in your renderView(), call this.currentView.remove instead of this.currentView.undelegateEvents.
Rationale:
I believe in your renderView() method, you are using undelegateEvents() thinking it releases all event listeners created by your view. It only releases events bound on to your view's $el element. However, using remove() on the view releases events bound to the $el as well as events created using this.listenTo() (and this.listenOnce()).
Now once you render another view, the old currentView will be properly released and you'll only get one alert.

Backbone trigger two methods in one event

I am using Backbone and I have a view with events defined:
....
events: {
'click .search-button': 'setModelTerm',
'change .source-select': 'setModelSourceId',
'change .source-select': 'activateSource'
},
....
I would like to trigger two methods when the event change .source-select fires. The problem is that the last entry in the event object overrides the preceding entry.
How can I trigger two methods in one event?
(I am trying to prevent writing another method that calls those two methods)
You can pass a wrapper function in your hash of events to call your two methods.
From http://backbonejs.org/#View-delegateEvents
Events are written in the format {"event selector": "callback"}. The
callback may be either the name of a method on the view, or a direct
function body.
Try
events: {
'click .search-button': 'setModelTerm',
'change .source-select': function(e) {
this.setModelSourceId(e);
this.activateSource(e);
}
},
The only thing that is keeping you from adding the same event/selector pair is that events is a hash - jQuery can handle multiple bindings to the same element/event pair. Good news though, jQuery events allow you to namespace events by adding a .myNamespace suffix. Practically speaking, this produces the same results but you can generate many different keys.
var MyView = Backbone.View.extend({
events: {
'click.a .foo': 'doSomething',
'click.b .foo': 'doSomethingElse'
'click.c .foo': 'doAnotherThing', // you can choose any namespace as they are pretty much transparent.
},
doSomething: function() {
// ...
},
doSomethingElse: function() {
// ...
},
doAnotherThing: function() {
// ...
},
});
The events hash in your view is just a convenience "DSL" of sorts. Just bind your 2nd event manually inside initialize.
events: {
'click .search-button': 'setModelTerm'
},
initialize: function () {
_.bindAll(this);
this.on('click .search-button', this.doAnotherThing);
}

Resources