I have a simple task - retrieve click listener function from DOM element.
I've fased two problems:
I have no idea how to obtain event listener, that was set via addEventListener function
$(element).data('events') is always empty
Talking about first problem - I think it's not critical as I'm using this function only in one place. But the second problem is a huge pain...
I've tested on pure jQuery environment:
$(element).click(function(){})
$(element).data('events') /*contains events info*/
But with Backbone:
$(element).click(function(){})
$(element).data('events') /*alway empty*/
I'm not a JS guru but it seems like there no bound data at all... Maybe it's just a typical Backbone behaviour, but still - how can I retrieve event handler?
If you are using Backbone.js you should be managing your events inside a Backbone.View object and avoid capturing the event with JQuery directly.
You should try something like this:
var myBody = $( 'body' );
var myDIV = $( '<DIV id="contentDIV"></DIV>' );
myBody.append( myDIV );
var myButton = $( '<button id="aButton">test</button>' );
myDIV.append ( myButton );
var MyView = Backbone.View.extend({
el : myDIV,
events: { 'click button#aButton' : 'doSomething' }, //here you bound the
//event to a function.
initialize: function(){
_.bindAll(this, 'render')
},
render: function(){
myDIV.append('<br>working...');
},
doSomething: function(){
alert( 'doSomething function.. after click event' );
}
});
var myView = new MyView;
myView.render();
PS: A good tutorial for understanding how it works: http://arturadib.com/hello-backbonejs/
Related
I created a view and has the ff codes:
var app = app || {};
app.singleFlowerView = Backbone.View.extend({
tagName: 'article',
className: 'flowerListItem',
// tells where to apply the views
template: _.template( $("#flowerElement").html() ),
// render
render: function(){
var flowerTemplate = this.template(this.model.toJSON());
// el contains all the prop above and pass to backbone
this.$el.html(flowerTemplate);
return this;
},
events: {
'mouseover': 'addBgColor',
'mouseout': 'removeBgColor'
},
addBgColor: function(){
this.$el.addBgColor('bgColorImage');
},
removeBgColor: function(){
this.$el.removeBgColor('bgColorImage');
}
});
When I run this to my HTML file I got the error addBgColor and removeBgColor is not a function. I have the CSS for this and all the models and views were set up.
Am I missing something here? Any idea why events doesn't work?
this.$el.addBgColor is the problem.
The events are triggering but you're calling addBgColor on the $el jQuery object, which is not a jQuery function, like the error message is telling you.
Check what's the difference between $el and el.
Tony, your events are cool and they are running they're just not doing anything.
this.addBgColor() will call your function in a view.
this.$el is referring to the html and there's no property called addBgColor assigned to $el.
You need to do something like change the class on your tag with the functions like so...
addBgColor: function(){
this.$el.className = 'bgColorImage'
},
.bgColorImage {
background-image: url('bgColorImage.jpg');
}
What is the best way to detect the moment after a Backbone View, extended from an other object or not, has been removed?
JsFiddle added :
http://jsfiddle.net/simmoniz/M5J8Q/1917/
How to make the line #32 working without altering the views...
<h2>The container</h2>
<div id="container"></div>
<script>
var SomeExtendedView = Marionette.ItemView.extend({
events: {
'click button.remove':'remove',
},
});
var JohnView = SomeExtendedView.extend({
template: _.template('<div><p>I\'m a <em>John view</em> <button class="remove">Remove me</button></p></div>'),
});
var DoeView = SomeExtendedView.extend({
template: _.template('<div><p>I\'m a <strong>Doe view</strong> <button class="remove">Remove me</button>'),
});
var SimpleView = Backbone.View.extend({
initialize: function(){
Backbone.View.prototype.initialize.apply(this, arguments);
this.$el.bind('click', _.bind(this.remove, this));
},
render: function(){
this.$el.html('<div><p>Simple view. <strong>Click on me to remove</strong></p></div>');
return this;
}
});
var container = {
el: $('#container'),
views: null,
add: function(view){
if(!this.views)this.views = [];
this.el.append(view.render().el);
view.$el.bind('remove', _.bind(this.onRemove, this));
},
onRemove : function(element){
console.log('Element ' + element + ' has been removed!');
}
}
container.add(new JohnView());
container.add(new DoeView());
container.add(new SimpleView());
</script>
View lifecycle management is one of the important things missing from the backbone core.
All non-trivial apps end up needing to solve this. You can either roll your own, or use something like marionette or Chaplin.
Basically, backbone doesn't have the concept of view destruction or dealocation. A point in time in which listeners should be unbound. This is the single greatest source of memory leaks in backbone apps.
I finally came up with a working solution. Since the element added is a Backbone view (simple or extended), it has remove function. This solution replaces the remove function with a new "remove" event that performs the same operation, but triggers a "remove" event juste before. Listeners can catch it now. It works great :
var ev = new $.Event('remove'),
orig = $.fn.remove;
view.remove = function() {
$(this).trigger(ev);
return orig.apply(this, arguments);
}
Then we can listen to the "remove" event like in my question
view.bind('remove', _.bind(this.onRemove, this));
Inside the view
events: {
"remove" : "some function",
},
I have super-View who is in charge of rendering sub-Views. When I re-render the super-View all the events in the sub-Views are lost.
This is an example:
var SubView = Backbone.View.extend({
events: {
"click": "click"
},
click: function(){
console.log( "click!" );
},
render: function(){
this.$el.html( "click me" );
return this;
}
});
var Composer = Backbone.View.extend({
initialize: function(){
this.subView = new SubView();
},
render: function(){
this.$el.html( this.subView.render().el );
}
});
var composer = new Composer({el: $('#composer')});
composer.render();
When I click in the click me div the event is triggered. If I execute composer.render() again everything looks pretty the same but the click event is not triggered any more.
Check the working jsFiddle.
When you do this:
this.$el.html( this.subView.render().el );
You're effectively saying this:
this.$el.empty();
this.$el.append( this.subView.render().el );
and empty kills the events on everything inside this.$el:
To avoid memory leaks, jQuery removes other constructs such as data and event handlers from the child elements before removing the elements themselves.
So you lose the delegate call that binds events on this.subView and the SubView#render won't rebind them.
You need to slip a this.subView.delegateEvents() call into this.$el.html() but you need it to happen after the empty(). You could do it like this:
render: function(){
console.log( "Composer.render" );
this.$el.empty();
this.subView.delegateEvents();
this.$el.append( this.subView.render().el );
return this;
}
Demo: http://jsfiddle.net/ambiguous/57maA/1/
Or like this:
render: function(){
console.log( "Composer.render" );
this.$el.html( this.subView.render().el );
this.subView.delegateEvents();
return this;
}
Demo: http://jsfiddle.net/ambiguous/4qrRa/
Or you could remove and re-create the this.subView when rendering and sidestep the problem that way (but this might cause other problems...).
There's a simpler solution here that doesn't blow away the event registrations in the first place: jQuery.detach().
http://jsfiddle.net/ypG8U/1/
this.subView.render().$el.detach().appendTo( this.$el );
This variation is probably preferable for performance reasons though:
http://jsfiddle.net/ypG8U/2/
this.subView.$el.detach();
this.subView.render().$el.appendTo( this.$el );
// or
this.$el.append( this.subView.render().el );
Obviously this is a simplification that matches the example, where the sub view is the only content of the parent. If that was really the case, you could just re-render the sub view. If there were other content you could do something like:
var children = array[];
this.$el.children().detach();
children.push( subView.render().el );
// ...
this.$el.append( children );
or
_( this.subViews ).each( function ( subView ) {
subView.$el.detach();
} );
// ...
Also, in your original code, and repeated in #mu's answer, a DOM object is passed to jQuery.html(), but that method is only documented as accepting strings of HTML:
this.$el.html( this.subView.render().el );
Documented signature for jQuery.html():
.html( htmlString )
http://api.jquery.com/html/#html2
When using $(el).empty() it removes all the child elements in the selected element AND removes ALL the events (and data) that are bound to any (child) elements inside of the selected element (el).
To keep the events bound to the child elements, but still remove the child elements, use:
$(el).children().detach(); instead of $(.el).empty();
This will allow your view to rerender successfully with the events still bound and working.
It appears as though the following code is getting inside initialize but my event doesn't appear to be firing.
What am I missing here?
var index = (function ($, window, document) {
var methods = {};
methods = {
init: function () {
},
getView: Backbone.View.extend({
el: $('.settings'),
events: {
'click .settings': 'addUl'
},
initialize: function () {
console.log('init');
},
render: function () {
},
addUl: function () {
console.log('addUI');
this.el.append("<ul> <li>hello world </li> </ul>");
}
})
};
return methods; } (jQuery, window, document));
var stuff = new index.getView();
Link to the jsbin
Remove the space in 'click .settings'
Actually remove .settings entirely.
'click .settings' is registering a click handler for a descendant of this.el that matches '.settings'.
In your example you want to register an event on this.el directly so you don't need the descendant selector.
The problem is that it is your view element ($el) that has the settings class and not a child.
click .settings tells backbone to bind a "click" event on the $el for any children that have .settings. However, because, it is $el which has the class settings the binding never match.
This is why when you remove .settings it works, because you say "any 'click' on $el"
The reason the documentation says click .blah is because it assumes that the html element(s) with the class='blah' are children of the $el element.
Hope this help.
I have some Backbone.js code that bind a click event to a button,
and I want to unbind it after clicked, the code sample as below:
var AppView = Backbone.View.extend({
el:$("#app-view"),
initialize:function(){
_.bindAll(this,"cancel");
},
events:{
"click .button":"cancel"
},
cancel:function(){
console.log("do something...");
this.$(".button").unbind("click");
}
});
var view = new AppView();
However the unbind is not working, I tried several different way and end up binding event in initialize function with jQuery but not in Backbone.events model.
Anyone know why the unbind is not working?
The reason it doesn't work is that Backbonejs doesn't bind the event on the DOM Element .button itself. It delegates the event like this:
$(this.el).delegate('.button', 'click', yourCallback);
(docs: http://api.jquery.com/delegate)
You have to undelegate the event like this:
$(this.el).undelegate('.button', 'click');
(docs: http://api.jquery.com/undelegate)
So your code should look like:
var AppView = Backbone.View.extend({
el:$("#app-view"),
initialize:function(){
_.bindAll(this,"cancel");
},
events:{
"click .button":"cancel"
},
cancel:function(){
console.log("do something...");
$(this.el).undelegate('.button', 'click');
}
});
var view = new AppView();
Another (maybe better) way to solve this is to create a state attribute like this.isCancelable now everytime the cancel function is called you check if this.isCancelable is set to true, if yes you proceed your action and set this.isCancelable to false.
Another button could reactivate the cancel button by setting this.isCancelable to true without binding/unbinding the click event.
You could solve this another way
var AppView = Backbone.View.extend({
el:$("#app-view"),
initialize:function(){
_.bindAll(this,"cancel");
},
events:{
"click .button":"do"
},
do:_.once(function(){
console.log("do something...");
})
});
var view = new AppView();
underscore.js once function ensures that the wrapped function
can only be called once.
There is an even easier way, assuming you want to undelegate all events:
this.undelegateEvents();
I like bradgonesurfing answer. However I came across a problem using the _.once approach when multiple instances of the View are created. Namely that _.once would restrict the function to be called only once for all objects of that type i.e. the restriction was at the class level rather than instance level.
I handled the problem this way:
App.Views.MyListItem = Backbone.View.extend({
events: {
'click a.delete' : 'onDelete'
},
initialize: function() {
_.bindAll(this);
this.deleteMe = _.once(this.triggerDelete);
},
// can only be called once
triggerDelete: function() {
console.log("triggerDelete");
// do stuff
},
onDelete:(function (e) {
e.preventDefault();
this.deleteMe();
})
});
Hopefully this will help someone
you can simply use object.off, the code below is work for me
initialize:function () {
_.bindAll(this, 'render', 'mouseover', 'mouseout', 'delete', 'dropout' , 'unbind_mouseover', 'bind_mouseover');
.......
},
events: {
'mouseover': 'mouseover',
'unbind_mouseover': 'unbind_mouseover',
'bind_mouseover': 'bind_mouseover',
.....
},
mouseover: function(){
$(this.el).addClass('hover');
this.$('.popout').show();
},
unbind_mouseover: function(){
console.log('unbind_mouseover');
$(this.el).off('mouseover');
},
bind_mouseover: function(){
........
},