So I have been adding my events thusly:
element.addEvent('click', function() {
alert('foobar');
});
However, when attempting to remove said event, this syntactically identical code (with "add" switched to "remove") does not work.
element.removeEvent('click', function() {
alert('foobar');
});
I assume this is because the two functions defined are not referenced the same, so the event is not technically removed. Alright, so I redefine the event addition and removal:
element.addEvent('click', alert('foobar'));
element.removeEvent('click', alert('foobar'));
Which works great, except now when the page loads, the click event is fired even before it's clicked!
The function is removed, though, which is great......
update: when you do .addEvent('type', function(){ }) and .removeEvent('type', function(){ }), even though the functions may have the same 'signatures', they are two separte anonymous functions, assigned on the fly. function 1 is !== to function 2 - hence there is no match when MooTools tries to remove it.
to be able to remove an exact handler, o:
function handler(){ ... }
el.addEvent('click', handler);
// .. later
el.removeEvent('click', handler);
Internally, events are actually a map of keys to functions in element storage. have a look at this fiddle i did a while back for another SO question - http://www.jsfiddle.net/mVJDr/
it will check to see how many events are stacked up for a particular event type on any given element (or all events).
similarly, removeEvent looks for a match in the events storage - have a look on http://jsfiddle.net/dimitar/wLuY3/1/. hence, using named functions like Nikolaus suggested allows you to remove them easily as it provides a match.
also, you can remove events via element.removeEvents("click") for all click events.
your page now alerts because you pass on alert as the function as well as execute it with the params 'foobar'. METHOD followed by () in javascript means RUN THE METHOD PRECEDING IT IMMEDIATELY, NOT LATER. when you bind functions to events, you pass the reference (the method name) only.
to avoid using an anonymous function and to pass argument,s you can do something like:
document.id('foobar').addEvent('click', alert.bind(this, 'foo'));
as bind raps it for you, but removing this will be even more complicated.
as for event delegation, it's:
parentEl.addEvents({
"click:relay(a.linkout)": function(e, el) {
},
"mouseover:relay(li.menu)": function(e, el) {
}
});
more on that here http://mootools.net/docs/more/Element/Element.Delegation#Element:removeEvent
keep in mind it's not great / very stable. works fine for click stuff, mouseenter is not to be used delegated, just mouseover - which means IE can fire mouseout when it should not. the way i understand it, it's coming improved in mootools 2.0
edit updating to show an example of bound and unbound method within a class pattern in mootools
http://www.jsfiddle.net/wmhgw/
var foo = new Class({
message: "hi",
toElement: function() {
return this.element = new Element("a", {
href: "http://www.google.com",
text: "google",
events: {
"click": this.bar.bind(this), // bind it
"mouseenter": this.bar // unbound -> this.element becomes this
}
});
},
bar: function(event) {
event.stop();
// hi when bound to class instance (this.message will exist)
// 'undefined' otherwise.
console.log(this.message || "undefined");
}
});
document.id(new foo()).inject(document.body);
the mouseenter here will be unbound where this will refer to the default scope (i.e the element that triggered the event - the a href). when bound, you can get the element via event.target instead - the event object is always passed on to the function as a parameter.
btw, this is a slightly less familiar use of class and element relation but it serves my purposes here to illustrate binding in the context of classes.
assig the function to a variable and use the same reference to add and remove the event.
if you use an anonymous function you will get to different references
var test = function(){ alert('test: ' + this.id); }
$('element').addEvent('click', test);
...
$('element').removeEvent('click', test);
addEvent : Attaches an event listener to a DOM element.
Example -
$('myElement').addEvent('click', function(){
alert('clicked!');
});
removeEvent : Works as Element.addEvent, but instead removes the specified event listener.
Example -
var destroy = function(){ alert('Boom: ' + this.id); } // this refers to the Element.
$('myElement').addEvent('click', destroy);
//later...
$('myElement').removeEvent('click', destroy);
This means when you add an event with a eventhandler not an anonymous function if you than remove the event than it will be removed.
Related
I am experimenting with the new way of handling page events in jqM and have run into a curious issue. When handling the pagecontainerbeforechange event
$(document).on('pagecontainerbeforechange',function(e,u){test(e,u,'changing');})
function test(e,u,msg){console.log($(u.toPage));}
Attempting to put a jQuery object wrapper around u.toPage - as done above - produces strange behavior.
Check out this fiddle to see what I mean
Click on the Second Page button and then view the console. Nothing will happen (the second page is not shown) and you will see a message along the lines of *Uncaught error:syntax error, unrecognized expression http://jsfiddle.net/egn7g5xb/1/show/#second
Now comment out Line 7 and run the fiddle again. No such issue this time and the second page gets shown.
Perhaps someone here might be able to explain what is going on here?
On initial run, jQuery Mobile creates a fake page before navigating to first page in DOM. At that stage, pagecontainerbeforechange fires twice and returns .toPage as an object.
Later on, upon navigating to other pages, it fires twice again; however, it returns a string first time (URL/hash) and second time it returns an object which is the page itself.
Therefore, when using that event, you have to determine whether .toPage is an object or a string.
$(document).on("pagecontainerbeforechange", function (e, data) {
if (typeof data.toPage == "string") {
/* parse url */
}
if (typeof data.toPage == "object") {
/* manipulate page navigating to */
}
});
Note that pagecontainerbeforetransition is similar to beforechange, however, it fires once and returns .toPage as an object.
First, create your pagecontainer events within $(document).on("pagecreate", "#first", function(){ .. }).
Then the selector for these events should be $(":mobile-pagecontainer") or $("body") NOT $(document).
function test(e,u,msg)
{
console.log(msg);
var IsJQ = u.toPage instanceof $;
console.log(IsJQ);
if (IsJQ){
console.log(u.toPage.data());
} else {
console.log(u.toPage);
}
console.log('---');
}
$(document).on("pagecreate", "#first", function(){
$(":mobile-pagecontainer").on('pagecontainerbeforechange', function (e, u) {
test(e,u,'changing');
});
$(":mobile-pagecontainer").on('pagecontainerchange',function(e,u){
test(e,u,'changed');
});
});
Updated FIDDLE
RoomsDGView = Backbone.View.extend({
collection: roomcollection,
initialize: function(){
var template = _.template( $("#search_template").html(), {} );
this.$el.html( template );
this.collection.bind('add', this.modeladded);
this.collection.bind('remove', this.modelremoved);
this.collection.bind('change', this.collectionchanged);
console.log(this);
this.render();
},
render: function(){
// Compile the template using underscore
console.log("running the render function...");
//renderrender();
/*$("#roomsList").jqGrid('clearGridData');
roomcollection.each(function (room,i){
var temp = jQuery.parseJSON(JSON.stringify(room));
$("#roomsList").jqGrid('addRowData', i,{idrooms: temp["idrooms"], roomname: temp["roomname"],
occupants: temp["occupants"]});
});*/
},
events: {
"click input[type=button]": "doSearch"
},
doSearch: function(){
// Button clicked
console.log(this);
},
modeladded: function() {
console.log("room added...");
$("#roomsList").jqGrid('clearGridData');
//My intent is to call the views render function
//her. I tried using this.render() and also
//this.collection.bind('add', this.modeladded(this));
//modeladded: function(view) {
// view.render();
console.log(this);
},
modelremoved: function() {
console.log("room removed...");
$("#roomsList").jqGrid('clearGridData');
},
collectionchanged: function() {
console.log("room changed...");
$("#roomsList").jqGrid('clearGridData');
}
});
I have tried many different ways to call the views render: method from inside the code for modeladded:. Use of this.render inside model added shows that the this object at that point has no render function. I also tried passing the view in something like:
this.collection.bind('add', this.modeladded(this));
modeladded: function(view) {
view.render();
which also leads to a console error that no render() can be found. Does anyone know how to call the views render: from inside modeladded?
For the time being I moved the render function out of the views render: and into a JavaScript function declared renderGlobal() declared in global scope and I know it does work that way but I don't think that is really the backbone.js way.
This is the error that is coming out of the console:
Uncaught TypeError: Object [object Object] has no method 'render'
Thank you for posting....
You're binding your event handler using bind (AKA on):
this.collection.bind('add', this.modeladded);
But, as usual with JavaScript, the value of this inside a function depends on how the function is called, not how it is defined (ignoring bound functions of course). You're not specifying a specific this for your function anywhere so you're not getting any particular this when it is called. If you give bind the third context argument:
this.collection.bind('add', this.modeladded, this);
// ------------------------------------------^^^^
then Backbone will call modeladded with the specific this and you'll find that this inside modeladded will be your view.
You could also use _.bind, Function.prototype.bind, or $.proxy to produce a bound version of your callback:
this.collection.bind('add', _(this.modeladded).bind(this));
this.collection.bind('add', this.modeladded.bind(this));
this.collection.bind('add', $.proxy(this.modeladded, this));
All of those produce new functions so you won't be able to unbind them without stashing the bound functions somewhere. You'll usually avoid using these when you have the option to specify the context (AKA this) explicitly.
There's also listenTo:
listenTo object.listenTo(other, event, callback)
Tell an object to listen to a particular event on an other object. The advantage of using this form, instead of other.on(event, callback, object), is that listenTo allows the object to keep track of the events, and they can be removed all at once later on. The callback will always be called with object as context.
So you can (and should) say this:
this.listenTo(this.collection, 'add', this.modeladded);
That will take care of giving you the desired this and makes it easier to clean up your event handlers when you're done with them. Similarly for the other event handlers you're using.
I am currently extending Marionette's base Marionette.View type with the method I named quickClick. I'm doing this to
config/marionette/view.js
(function() {
define([
'backbone.marionette'
],
function(Marionette){
return _.extend(Backbone.Marionette.View.prototype, {
quickClick: function(e) {
$(e.target).get(0).click();
}
});
});
}).call(this);
This allows me to call this method from any view I create without having to redefine it per view. Great!
Here's a trimmed down view with the events object still in place:
(function() {
define([
'backbone.marionette',
'app',
'templates'
],
function(Marionette, App, templates){
// Define our Sub Module under App
var List = App.module("SomeApp");
List.Lessons = Backbone.Marionette.ItemView.extend({
events: {
'tap .section-container p.title': 'quickClick'
}
});
// Return the module
return List;
});
}).call(this);
In case your wondering, tap is an event I'm using from Hammer.js because this is a mobile app. So, in order to circumvent the 300ms click event delay on iOS and Android, I'm manually triggering a click event when the tap event fires on certain elements.
Now, all of this is working just fine, and I felt it was necessary to describe this in detail, so that an answer could be given with context.
My problem is having to define the events object. I don't mind at all for elements as specific as the one above, .section-container p.title. But, I would like to register a tap event for all <a> tags within every view. It doesn't make sense to keep defining this event in each view I create
events: {
'tap .section-container p.title': 'quickClick',
// I don't want to add this to every single view manually
'tap a': 'quickClick'
}
Instead, of adding this to every view, I thought I would just add an events object to the config/marionette/view.js file where I added a method to the Marionette.View prototype.
Here's what I did
(function() {
define([
'backbone.marionette'
],
function(Marionette){
return _.extend(Backbone.Marionette.View.prototype, {
events: {
'tap a': 'quickClick'
},
quickClick: function(e) {
$(e.target).get(0).click();
}
});
});
}).call(this);
Of course, that doesn't work. The events object is overridden each time I need to add events that only apply to that view. Btw, tap a does work when my view does not have its' own events object.
So, my question is: How do I add default events to Marionette's Marionette.View base type?
"Of course, that doesn't work. The events object is overridden each time I need to add events that only apply to that view."
Yes, that seems to be the problem. Here is the part of Marionette that does the event delegation:
// internal method to delegate DOM events and triggers
_delegateDOMEvents: function(events){
events = events || this.events;
if (_.isFunction(events)){ events = events.call(this); }
var combinedEvents = {};
var triggers = this.configureTriggers();
_.extend(combinedEvents, events, triggers);
Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
},
One possible solution could be overwriting this (private!) part of Marionette - but it could probably change in new versions of Marionette and you'd always have to make sure that things still work. So this is bad.
But you could do something like this in your subviews.:
events: _.extend(this.prototype.events, {
'tap .section-container p.title': 'quickClick'
})
If this makes sense for only one 'global' event is another question.
Or you could define an abstract View Class, which does something like that
events: _.extend({'tap a': 'quickClick'}, this.my_fancy_events)
and also defines the quickClick method and then use this view for all you subviews. They then define their events not in 'events' but in 'my_fancy_events'.
When extending the views I occasionally find myself in situation when I need to add some extra calls in 'initialize' as well as extend 'events' property to include some new calls.
In my abstract view I have a function:
inheritInit: function(args) {
this.constructor.__super__.initialize.apply(this, args);
this.events = _.extend(this.constructor.__super__.events, this.eventsafter);
},
Then, in an extended view, I can call
initialize: function(options) {
this.inheritInit(arguments)
//..some extra declarations...
}
and also I can use 'events' property in a regular way.
I'm using the .each method with the .getJSON method to print out objects in a JSON file. This works fine, however I am unable to add a click function to an element that has been printed out. I am trying to bind a function to the div with 'click' ID.
var loadData = function () {
$.getJSON("profiles2.json", function (data) {
var html = [];
html.push("<div id='click'>Click here</div>");
$.each(data.profiles, function (firstIndex, firstLevel) {
html.push("<h2>" + firstLevel.profileGroup + "</h2>");
});
$("#data").html(html.join(''));
});
};
$(document).ready(function () {
loadData();
$("#click").click(function () {
console.log('clicked');
});
});
$.getJSON() (like other Ajax methods) is asynchronous, so it returns immediately before the results have come back. So your loadData() method also returns immediately and you then try to bind a handler to an element not yet added.
Move the .click(...) binding into the callback of $.getJSON(), after adding the element(s), and it will work.
Alternatively, use a delegated event handler:
$("#data").on("click", "#click", function() {
console.log('clicked');
});
...which actually binds the handler to the parent element that does exist at the time. When a click occurs it then tests whether it was on an element that matched the selector in the second parameter.
And as an aside, don't bind click handlers to divs unless you don't care about people who are physically unable to (or simply choose not to) use a mouse or other pointing device. Use anchor elements (styled as you see fit) so that they're "click"-accessible via the keyboard and the mouse.
$.getJSON is an asynchronous call and probably hasn't finished by the time you are trying to bind to the element that it injects into your DOM. Put your binding inside the $.getJSON call after you append the element to the page at the bottom.
There's a addPost function in my router. I don't want to re-create the postAddView every time the function is invoked:
addPost: function () {
var that = this;
if (!this.postAddView) {
this.postAddView = new PostAddView({
model: new Post()
});
this.postAddView.on('back', function () {
that.navigate('#/post/list', { trigger: true });
});
}
this.elms['page-content'].html(this.postAddView.render().el);
}
Here's the PostAddView:
PostAddView = backbone.View.extend({
events: {
'click #post-add-back': 'back'
}
, back: function (e) {
e.preventDefault();
this.trigger('back');
}
});
The first time the postAddView is rendered, the event trigger works well. However, after rendering other views to page-content and render postAddView back, the event trigger won't be trigger anymore. The following version of addPost works well, though.
addPost: function () {
var that = this, view;
view = new PostAddView({
model: new Post()
});
this.elms['page-content'].html(view.render().el);
view.on('back', function () {
delete view;
that.navigate('#/post/list', { trigger: true });
});
}
Somewhere you are calling jQuery's remove and that
In addition to the elements themselves, all bound events and jQuery data associated with the elements are removed.
so the delegate call that Backbone uses to bind events to your postAddView.el will be lost. Then, when you re-add your postAddView.el, there are is no delegate attached anymore and no events are triggered. Note that Backbone.View's standard remove method calls jQuery's remove; a few other things in jQuery, just as empty will do similar things to event handlers. So the actual function call that is killing your delegate could be hidden deep inside something else.
You could try calling delegateEvents manually:
this.elms['page-content'].html(this.postAddView.render().el);
this.postAddView.delegateEvents();
or better, just throw the view away and create a new one every time you need it. Your view objects should be pretty light weight so creating new ones should be cheap and a lot less hassle than trying to keep track of the existing views by hand.
If you really want to reuse the current DOM and View you do not need to set again and again the element as you are doing, everything that you call .html() you are destroying the DOM of the View and generating again and losing events. Also I prefer always to add the "el" in the DOM before render the View. I will have your function in this way:
addPost: function () {
if (!this.postAddView) {
this.postAddView = new PostAddView({
model: new Post()
});
this.postAddView.on('back', this.onBack);
this.elms['page-content'].html(this.postAddView.el);
}
this.postAddView.render();
},
onBack : function () {
this.navigate('#/post/list', { trigger: true });
}
I'm not fan of the use of local variables to refer to "this". If all of your Views uses _.bindAll(this) in the initialize method you could bind your events to your view and could use this(check how I transformed onBack).
With my code there is not a need to manually call this.delegateEvents()