Fundamental Backbone issue with registering event handlers - javascript

I really don't understand how Backbone is supposed to handle common scenarios where you want to register event handlers on HTML before the HTML is inserted into the DOM.
Most Backbone views look like so:
var PortalView = Backbone.View.extend({
events: {
'click #logout-li-id': 'onClickLogout' //1 (doesn't work)
},
initialize: function (opts) {
var self = this;
$('#logout-li-id').on('click', function (event) { //2 (doesn't work because '#logout-li-id' is not in the DOM yet
event.preventDefault();
alert(event);
});
},
render: function () {
var self = this;
var template = allTemplates['templates/portalTemplate.ejs'];
var ret = EJS.render(template, {});
$('#main-div-id').html(ret);
$('#logout-li-id').on('click', function (event) { //3 works!
event.preventDefault();
alert(event);
});
return this;
},
onClickLogout: function(event){
alert(event);
}
});
As you can see above, I have labeled the 3 similar calls to register an event handler on the DOM element '#logout-li-id' - the problem is that the only successful call to register the callback on the DOM element is in the render function (call #3), which occurs after the el for this Backbone view is inserted in the DOM. So, the standard way of declaring a key in the events object for the Backbone view doesn't work! So what is the right way to do this with Backbone?

Backbone expects that events a view will handle via the events hash are children of the view's el (that is, the root DOM element of the view). It looks like you are probably not attaching this view to the parent of the DOM element you're listening to - is $('#main-div-id') a child of the view's el?
The standard approach here would be something like:
var PortalView = Backbone.View.extend({
// Associate this view with the element it will manage
el: '#main-div-id',
events: {
'click #logout-li-id': 'onClickLogout'
},
render: function () {
var template = allTemplates['templates/portalTemplate.ejs'];
var ret = EJS.render(template, {});
// Render to this view's element
this.$el.html(ret);
}
});
More detail: The events hash works by adding delegated event handlers to the view's DOM node. Essentially, every click within the view's DOM tree will call a handler that checks whether the event target matches a given selector. If so, it passes the event to the view method you specify. But this only works if the element you click is in the view's DOM tree - otherwise, the view's DOM will never get the event and the event handler will never be called.

Related

How to cover Marionette View's event using test cases of Jasmine?

I am working on Test cases for Marionette's View. By using events attribute, I have written a callback function on click of an HTML element. The functionality is working but I am struggling in writing test cases. I am not able to cover that click event using Jasmine test cases.
I have used the Marionette Region to render the view.
I have tried using spies but those are not working.
Please find code below for more details:
var TestView = Backbone.Marionette.CompositeView.extend({
tagName: 'div',
className: 'test-menu',
childView: testMenuView,
childViewOptions: function() {
return {
'componentId': this.cid
};
},
template: _.template(testTemplate),
initialize: function(options) {
this.collection = this.options.testData;
},
onShow: function(collectionView) {
collectionView.$el.show();
},
attachHtml: function(collectionView,itemView) {
collectionView.$("#testMenu").append(itemView.el);
},
events: {
'click #testBtn': function (event) {
alert('testBtn Clicked');
}
}
});
I like to use JQuery where I can when I write tests due to it being less verbose, and for most events triggering the handlers with JQuery will also work fine.
Given that you've got everything else set up for running a Jasmine suite I'd do
it('reacts to click events on its button', function() {
var view = new TestView();
view.render();
view.$('#testBtn').click(); // or view.$('#testBtn').trigger('click')
//(verify that the view did what was expected)
});
If testing the alert is the actual problem then use a spy for that, e.g. spyOn(window, 'alert') and expect(window.alert).ToHaveBeenCalledWith('testBtn Clicked')
JQuery won't always be able to trigger event handlers that are bound with addEventListener. Click is not one of those events, but there are situations where I have to trigger events using for examples
var event = document.createEvent('Event');
event.initEvent('keydown');
event.keyCode = 40;
event.altKey = true;
view.el.querySelector('input').dispatchEvent(event);
But most of the time just using JQuery's .trigger or corresponding shortcut-function directly on the element you want is fine.

Impossible Backbone Zombies

I've been trying to debug my Backbone multi-page app for most of the day now to get rid of 'zombies', but unfortunately to no avail. Before today, I didn't even realize I have a zombie problem. What am I doing wrong?
This is my RegionManager:
var regionManager = (function() {
var currView = null;
var rm = {};
var closeView = function(view) {
if (view && view.close) {
view.close();
}
};
var openView = function(view) {
view.render();
if (view.onShow) {
view.onShow();
}
};
rm.show = function(view) {
closeView(currView);
currView = view;
openView(currView);
};
return rm;
})();
This is my View cleaning up function:
Backbone.View.prototype.close = function() {
if (this.onClose) {
this.onClose();
}
if (this.views) {
_.invoke(this.views, 'close');
}
// Unbind any view's events.
this.off();
// Unbind any model and collection events that the view is bound to.
if (this.model) {
this.model.off(null, null, this);
}
if (this.collection) {
this.collection.off(null, null, this);
}
// Clean up the HTML.
this.$el.empty();
};
I tried appending the View els directly to the body and using this.remove(); in the View clean-up function (instead of using a common el: $('#content') to which I am appending elements, then cleaning up by this.$el.empty()), but that didn't work either.
It might have something to do with my "global Events":
Backbone.Events.on('letterMouseDown', this.letterMouseDown, this);
But I take care of them with the onClose function:
onClose: function() {
Backbone.Events.off('letterMouseDown');
}
One problem I see is that your close function never removes the event delegator from the view's el. A view's events are handled by using the delegator form of jQuery's on to attach a single event handler to the view's el. Your close does:
this.$el.empty();
but that only removes the content and any event handlers attached to that content, it does nothing at all to the handlers attached directly to this.el. Consider this minimal example:
var V = Backbone.View.extend({
events: {
'click': 'clicked'
},
clicked: function() {
console.log('still here');
}
});
var v = new V({ el: '#el' });
v.close();
After that, clicking on #el will throw a 'still here' in the console even though you think that the view has been fully cleaned up. Demo: http://jsfiddle.net/ambiguous/aqdq7pwm/
Adding an undelegateEvents call to your close should take care of this problem.
General advice:
Don't use the old-school on and off functions for events, use listenTo and stopListening instead. listenTo keeps track of the events on the listener so it is easier to remove them all later.
Simplify your close to just this:
Backbone.View.prototype.close = function() {
if(this.onClose)
this.onClose();
if(this.views)
_.invoke(this.views, 'close');
this.remove();
};
Don't bind views to existing els. Let the view create (and own) its own el and let the caller place that el into a container with the usual:
var v = new View();
container.append(v.render().el);
pattern. If you must attach to an existing el then the view should override remove with a slightly modified version of the standard implementation:
remove: function() {
this.$el.empty(); // Instead of removing the element.
this.undelegateEvents(); // Manually detach the event delegator.
this.stopListening();
return this;
}
I'm pretty sure I found the root for my problem.
mu is too short was right, with the close() method I wasn't removing the events bound directly to my el (which I tried to do by this.off() - this.$el.off()/this.undelegateEvents() is the correct way). But for me, it only fixed the problem that events got called multiple times unnecessarily.
The reason I was plagued by 'zombie views' or unintended behavior was that I wasn't freeing up the memory in the View..
this.remove() only gets rid of the el and it's elements/events, but not the View's internal variables. To elaborate - in my View I have an array declared like so this.array: [] and I didn't have it freed in the onClose function.
All I had to do was empty it in the onClose function or initially declare the array as this.array: null so on recurrent View renderings it would at least free the previous array (it still should be freed on the onClose method though, because the array/object is still going to sit in the memory until browsing away from the page).
It was excruciating to debug, because it's a crossword game (at least my code is hard to read there) and sometimes the words didn't match up, but I didn't know where the problem was coming from.
Lessons learned.

Can I trigger a backbone event for .ready()?

I want to trigger a backbone event when the view has rendered. Ideally, I'd write something like this:
var DetailView = Backbone.View.extend({
id: 'detailpage',
events: {
'ready document': '_bringSlideDown',
'click .close-slideDown': '_closeSlideDown'
},
and I would be able to create and call a _bringSlideDown function once the view has finished loading. This doesn't work - is there a better way to call this event?
Specifically, I want the event to only run once, which is why I'm not nesting it in the render function. I want the view to be able to render multiple times, but want to use Backbone's .off() method to handle this event once before unbinding it. Thanks!
I have three points that I want to mention.
First, by best practice I execute Backbone code until the DOM is
ready, so if you follow this rule, your backbone view will be
executed after the DOM was ready(so there is no way to listen for the ready event).
If you want to listen for DOM additions with jQuery, you should use
plugins like: https://github.com/brandonaaron/livequery
Regarding your use case, instead of listen to the DOM, check in the render
method if is the first time that you are rendering the view using
a flag.
I would do something like this:
var YourView = Backbone.View.extend({
_rendered : false,
events : {
'click .close-slideDown': '_closeSlideDown'
},
render: function(){
//some render stuff
if( !this._rendered ){
this._rendered = true;
this._bringSlideDown();
}
}
});
Basically

Backbone.js remove event not triggering

I have the following view, where I'm trying to bind the click event to a delete button, but it doesn't seem to be triggering anything. Not getting any errors in console, it just doesn't seem to be binding the "click" event.
The span.delete element is deeply nested within a bunch of other elements, not sure if that matters, I've tried it as a direct child element as well, but still no go.
var ListRow = Backbone.View.extend(
{
events:
{
'click span.delete': 'remove'
},
initialize: function()
{
_.bindAll(this, 'render', 'unrender', 'remove');
this.model.bind('remove', this.unrender);
},
render: function()
{
this.el = _.template($('#tpl-sTableList_' + key + 'Row').html());
return this;
},
unrender: function()
{
$(this.el).fadeOut();
},
remove: function()
{
this.model.destroy();
}
});
There is no default remove event on a model, there is only a remove event coming from the collection, so if you want to remove a view when the model gets removed from the collection, it's probably better to put a
this.collection.bind('remove', this.onRemove, this);
in your ListView (as I assume you're using a ListView and a ListItemView based on your example) and then your onRemove method passes the model as an argument so you can find the view that's associated with it.
Found the problem, it was because I was only setting the el object, but not rendering it, so instead of:
this.el =
should be
$(this.el).html();
Otherwise all works as expected.

Backbone.JS Custom Event from Child View

I have two views, for simplicity sake a parent/child. The child is using trigger to throw an event. I am not seeing it in the handler of the parent. Is the below valid?
var parent = Backbone.View.extend({
events: { "childevent": "run" },
run: function(e) {
console.log(e);
},
render: function() { /* render the child here */ }
});
var child = Backbone.View.extend({
someAction: function() { this.trigger('childevent'); }
});
Figured it out! $(this.el).trigger('childevent'); works.
Shouldn't it be events: { "childevent": "run" } instead? There is no way to access the actual anonymous function in this place in the code.
Backbone store a jQuery reference to the view's node in this.$el property so you can spare some performance using it rather than re-compute the reference by $(this.el).
// use "this.$el.trigger()" to refer to jQuery's object
this.$el.trigger('childevent');
Obviously late, but for anyone else who comes across this:
the events property on a view is for auto-binding Html DOM events from components within the views el or $el elements, and the syntax involves both a UI event and a selector as the key in pair:
events: { "click #someButton", "clickHandler" }
To listen to events from other Models or Views, you use this.listenTo( target, ..... )

Categories

Resources