Currently dojo uses on method to connect event to handler.
btn = new Button();
btn.on('click', function () {console.log('do something');});
this will call the attached function when the button gets clicked.
however, according to the documents, removing existing handlers should be done in the following way
handler = btn.on('click', function () {console.log('do something');});
handler.remove();
this is not the way I want to remove event handler.
I do not store the handler reference anywhere. But I want to add a new 'click' event by doing
btn.on('click', function () {console.log('do something different');});
so that it replaces the existing 'click' event handler and add a new one.
Is there any way to achieve what I want?
Thanks!
That's not possible, the framework tells you to do it in the way by creating a reference to the event handler. This is similar to how other frameworks like jQuery work.
jQuery has of course a mechanism to remove all event handlers by using the off() function, but that's not available in Dojo either. Like Chris Hayes suggested in the comments, you can implement such a feature by yourself, either by wrapping it inside another module, or by using aspects on the dojo/on module.
For example, you can wrap it inside a new module:
// Saving the event handlers
var on2 = function(dom, event, callback) {
on2.handlers = [];
if (on2.handlers[event] === undefined) {
on2.handlers[event] = [];
}
var handler = on(dom, event, callback);
on2.handlers[event].push({
node: dom,
handler: handler
});
return handler;
};
// Off functionality
lang.mixin(on2, on, {
off: function(dom, event) {
if (this.handlers[event] !== undefined) {
array.forEach(this.handlers[event], function(handler) {
if (handler.node === dom) {
handler.handler.remove();
}
});
}
}
});
And then you can use it:
on2(dom.byId("test"), "click", function() {
console.log("test 1 2 3"); // Old event handler
});
on2.off(dom.byId("test"), "click"); // Remove old event handlers
on2(dom.byId("test"), "click", function() {
console.log("test 4 5 6"); // New event handler
});
This should work fine, as you can see in this fiddle: http://jsfiddle.net/X7H3F/
btn = new Button();
btn.attr('id','myButton');
query("#myButton").on('click', function () {console.log('do something');});
Do the same thing when you want to replace your handler. Like,
query("#myButton").on('click', function () {console.log('do something different');});
Hope that helps :)
Related
The Twitter Bootstrap modal dialog has a set of events that can be used with callbacks.
Here is an example in jQuery:
$(modalID).on('hidden.bs.modal', function (e) {
console.log("hidden.bs.modal");
});
However, when I transcribe this method to JS the event 'hidden.bs.modal' does not work. Using 'click' does work:
document.querySelector(modalID).addEventListener('hidden.bs.modal', function(e) {
console.log("hidden.bs.modal");
}, false);
Are these Bootstrap events only useable with jQuery? Why?
Thanks,
Doug
The reasoning behind this is because Twitter Bootstrap uses that.$element.trigger('hidden.bs.modal')(line 997) to trigger it's events. In other words it uses .trigger.
Now jQuery keeps track of each element's event handlers (all .on or .bind or .click etc) using ._data. This is because there isn't any other way to get the event handlers that are bound (using .addEventListener) to an element. So the trigger method actually just get's the set event listener(s)/handler(s) from ._data(element, 'events') & ._data(element, 'handle') as an array and runs each of them.
handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" );
if ( handle ) {
handle.apply( cur, data );
}
(line 4548)
This means that no matter what context is, if an event is bound via .addEventListener it will not run using .trigger. Here's an example. On load only jquery will be logged (triggered by .trigger). If you click the a element though, both will run.
$('a')[0].addEventListener('click', function(){console.log('addlistener');}, false);
$('a').on('click', function(){
console.log('jquery');
});
$('a').trigger('click');
DEMO
Alternatively, you can trigger an event on an element in javascript using fireEvent(ie) & dispatchEvent(non-ie). I don't necessarily understand or know the reasoning of jQuery's .trigger not doing this, but they may or may not have one. After a little more research I've found that they don't do this because some older browsers only supported 1 event handler per event.
In general we haven't tried to implement functionality that only works on some browsers (and some events) but not all, since someone will immediately file a bug that it doesn't work right.
Although I do not recommend it, you can get around this with a minimal amount of changes to bootstraps code. You would just have to make sure that the function below is attached first (or you will have listeners firing twice).
$(modalID).on('hidden.bs.modal', function (e, triggered) {
var event; // The custom event that will be created
if(!triggered){
return false;
}
e.preventDefault();
e.stopImmediatePropagation();
if (document.createEvent) {
event = document.createEvent("HTMLEvents");
event.initEvent("hidden.bs.modal", true, true);
} else {
event = document.createEventObject();
event.eventType = "hidden.bs.modal";
}
event.eventName = "hidden.bs.modal";
if (document.createEvent) {
this.dispatchEvent(event);
} else {
this.fireEvent("on" + event.eventType, event);
}
});
Finally change the Twitter Bootstrap line from above to:
that.$element.trigger('hidden.bs.modal', true)
This is so you know its being triggered and not the event that you're firing yourself after. Please keep in mind I have not tried this code with the modal. Although it does work fine on the click demo below, it may or may not work as expected with the modal.
DEMO
Native Javascript Solution. Here is my way of doing it without JQuery.
//my code ----------------------------------------
export const ModalHiddenEventListener = (el, fn, owner) => {
const opts = {
attributeFilter: ['style']
},
mo = new MutationObserver(mutations => {
for (let mutation of mutations) {
if (mutation.type === 'attributes'
&& mutation.attributeName ==='style'
&& mutation.target.getAttribute('style') === 'display: none;') {
mo.disconnect();
fn({
owner: owner,
element: mutation.target
});
}
}
});
mo.observe(el, opts);
};
//your code with Bootstrap modal id='modal'-------
const el = document.getElementById('modal'),
onHide = e => {
console.log(`hidden.bs.modal`);
};
ModalHiddenEventListener(el, onHide, this);
Compatibility:
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#Browser_compatibility
I am trying to use .one() to bind to an event with the name similar to something.else.thing, I am not able to change the event name since it comes from an external library.
The problem is because of the periods, jQuery creates namespaces, else and thing for the event something instead of creating an event named something.else.thing.
Is there any way around this?
Thanks
Edit:
Some example code:
$(document).on('appfeel.cordova.admob.onAdLoaded', function() {
console.log('Does nothing');
});
document.addEventListener('appfeel.cordova.admob.onAdLoaded', function() {
console.log('Works');
});
I don't think you can disable jQuery event namespacing so if you want to use one on an event with dots in it you can just do this in pure JS:
Fiddle: http://jsfiddle.net/AtheistP3ace/8z6ewwnv/1/
HTML:
<div id="test"></div>
<button id="mybutton">Run event again</button>
JS:
var test = document.getElementById('test');
var button = document.getElementById('mybutton');
var event = new Event('something.else.blah');
function onWeirdEvent () {
test.removeEventListener('something.else.blah', onWeirdEvent);
alert('did it');
}
test.addEventListener('something.else.blah', onWeirdEvent, false);
test.dispatchEvent(event);
button.addEventListener('click', function (e) {
test.dispatchEvent(event);
}, false);
Its essentially the same thing. If you really want everything to seem jQuery-ish create a custom plugin:
Fiddle: http://jsfiddle.net/AtheistP3ace/8z6ewwnv/2/
$.fn.customOne = function (eventString, fn) {
var self = this[0];
var origFn = fn;
fn = function (event) {
self.removeEventListener(eventString, fn);
return origFn.apply(self);
};
self.addEventListener(eventString, fn, false);
};
$.fn.customTrigger = function (eventString) {
var event = new Event(eventString);
var self = this[0];
self.dispatchEvent(event);
}
$('#test').customOne('something.else.blah', function () {
alert('did it');
});
$('#test').customTrigger('something.else.blah');
$('#test').customTrigger('something.else.blah');
Here is how I decided to go about solving this issue. I went about it this way because this way allows me to continue to use jQuery and all the functionality it provides while keeping my code consistent and only requires a few lines of extra code to go about.
$(document).one('somethingElseThing', function() {
console.log('Event!');
});
document.addEventListener('something.else.thing', function() {
$(document).trigger('somethingElseThing');
});
What I am doing is using straight JavaScript to create an event listener for the event with a period in the name and then I have it trigger a custom event that doesn't a have a period so that I can continue to use jQuery. This I believe is an easy straightforward approach.
The Twitter Bootstrap modal dialog has a set of events that can be used with callbacks.
Here is an example in jQuery:
$(modalID).on('hidden.bs.modal', function (e) {
console.log("hidden.bs.modal");
});
However, when I transcribe this method to JS the event 'hidden.bs.modal' does not work. Using 'click' does work:
document.querySelector(modalID).addEventListener('hidden.bs.modal', function(e) {
console.log("hidden.bs.modal");
}, false);
Are these Bootstrap events only useable with jQuery? Why?
Thanks,
Doug
The reasoning behind this is because Twitter Bootstrap uses that.$element.trigger('hidden.bs.modal')(line 997) to trigger it's events. In other words it uses .trigger.
Now jQuery keeps track of each element's event handlers (all .on or .bind or .click etc) using ._data. This is because there isn't any other way to get the event handlers that are bound (using .addEventListener) to an element. So the trigger method actually just get's the set event listener(s)/handler(s) from ._data(element, 'events') & ._data(element, 'handle') as an array and runs each of them.
handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" );
if ( handle ) {
handle.apply( cur, data );
}
(line 4548)
This means that no matter what context is, if an event is bound via .addEventListener it will not run using .trigger. Here's an example. On load only jquery will be logged (triggered by .trigger). If you click the a element though, both will run.
$('a')[0].addEventListener('click', function(){console.log('addlistener');}, false);
$('a').on('click', function(){
console.log('jquery');
});
$('a').trigger('click');
DEMO
Alternatively, you can trigger an event on an element in javascript using fireEvent(ie) & dispatchEvent(non-ie). I don't necessarily understand or know the reasoning of jQuery's .trigger not doing this, but they may or may not have one. After a little more research I've found that they don't do this because some older browsers only supported 1 event handler per event.
In general we haven't tried to implement functionality that only works on some browsers (and some events) but not all, since someone will immediately file a bug that it doesn't work right.
Although I do not recommend it, you can get around this with a minimal amount of changes to bootstraps code. You would just have to make sure that the function below is attached first (or you will have listeners firing twice).
$(modalID).on('hidden.bs.modal', function (e, triggered) {
var event; // The custom event that will be created
if(!triggered){
return false;
}
e.preventDefault();
e.stopImmediatePropagation();
if (document.createEvent) {
event = document.createEvent("HTMLEvents");
event.initEvent("hidden.bs.modal", true, true);
} else {
event = document.createEventObject();
event.eventType = "hidden.bs.modal";
}
event.eventName = "hidden.bs.modal";
if (document.createEvent) {
this.dispatchEvent(event);
} else {
this.fireEvent("on" + event.eventType, event);
}
});
Finally change the Twitter Bootstrap line from above to:
that.$element.trigger('hidden.bs.modal', true)
This is so you know its being triggered and not the event that you're firing yourself after. Please keep in mind I have not tried this code with the modal. Although it does work fine on the click demo below, it may or may not work as expected with the modal.
DEMO
Native Javascript Solution. Here is my way of doing it without JQuery.
//my code ----------------------------------------
export const ModalHiddenEventListener = (el, fn, owner) => {
const opts = {
attributeFilter: ['style']
},
mo = new MutationObserver(mutations => {
for (let mutation of mutations) {
if (mutation.type === 'attributes'
&& mutation.attributeName ==='style'
&& mutation.target.getAttribute('style') === 'display: none;') {
mo.disconnect();
fn({
owner: owner,
element: mutation.target
});
}
}
});
mo.observe(el, opts);
};
//your code with Bootstrap modal id='modal'-------
const el = document.getElementById('modal'),
onHide = e => {
console.log(`hidden.bs.modal`);
};
ModalHiddenEventListener(el, onHide, this);
Compatibility:
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#Browser_compatibility
I have a bunch of elements that get three different classes: neutral, markedV and markedX. When a user clicks one of these elements, the classes toggle once: neutral -> markedV -> markedX -> neutral. Every click will switch the class and execute a function.
$(document).ready(function(){
$(".neutral").click(function markV(event) {
alert("Good!");
$(this).addClass("markedV").removeClass("neutral");
$(this).unbind("click");
$(this).click(markX(event));
});
$(".markedV").click(function markX(event) {
alert("Bad!");
$(this).addClass("markedX").removeClass("markedV");
$(this).unbind("click");
$(this).click(neutral(event));
});
$(".markedX").click(function neutral(event) {
alert("Ok!");
$(this).addClass("neutral").removeClass("markedX");
$(this).unbind("click");
$(this).click(markV(event));
});
});
But obviously this doesn't work. I think I have three obstacles:
How to properly bind the changing element to the already defined function, sometimes before it's actually defined?
How to make sure to pass the event to the newly bound function [I guess it's NOT accomplished by sending 'event' to the function like in markX(event)]
The whole thing looks repetitive, the only thing that's changing is the alert action (Though each function will act differently, not necessarily alert). Is there a more elegant solution to this?
There's no need to constantly bind and unbind the event handler.
You should have one handler for all these options:
$(document).ready(function() {
var classes = ['neutral', 'markedV', 'markedX'],
methods = {
neutral: function (e) { alert('Good!') },
markedV: function (e) { alert('Bad!') },
markedX: function (e) { alert('Ok!') },
};
$( '.' + classes.join(',.') ).click(function (e) {
var $this = $(this);
$.each(classes, function (i, v) {
if ( $this.hasClass(v) ) {
methods[v].call(this, e);
$this.removeClass(v).addClass( classes[i + 1] || classes[0] );
return false;
}
});
});
});
Here's the fiddle: http://jsfiddle.net/m3CyX/
For such cases you need to attach the event to a higher parent and Delegate the event .
Remember that events are attached to the Elements and not to the classes.
Try this approach
$(document).ready(function () {
$(document).on('click', function (e) {
var $target = e.target;
if ($target.hasClass('markedV')) {
alert("Good!");
$target.addClass("markedV").removeClass("neutral");
} else if ($target.hasClass('markedV')) {
alert("Bad!");
$target.addClass("markedX").removeClass("markedV");
} else if ($target.hasClass('markedX')) {
alert("Ok!");
$target.addClass("neutral").removeClass("markedX");
}
});
});
OR as #Bergi Suggested
$(document).ready(function () {
$(document).on('click', 'markedV',function (e) {
alert("Good!");
$(this).addClass("markedV").removeClass("neutral");
});
$(document).on('click', 'markedX',function (e) {
alert("Bad!");
$(this).addClass("markedX").removeClass("markedV");
});
$(document).on('click', 'neutral',function (e) {
alert("Ok!");
$(this).addClass("neutral").removeClass("markedX");
});
});
Here document can be replaced with any static parent container..
How to properly bind the changing element to the already defined function, sometimes before it's actually defined?
You don't bind elements to functions, you bind handler functions to events on elements. You can't use a function before it is defined (yet you might use a function above the location in the code where it was declared - called "hoisting").
How to make sure to pass the event to the newly bound function [I guess it's NOT accomplished by sending 'event' to the function like in markX(event)]
That is what happens implicitly when the handler is called. You only need to pass the function - do not call it! Yet your problem is that you cannot access the named function expressions from outside.
The whole thing looks repetitive, the only thing that's changing is the alert action (Though each function will act differently, not necessarily alert). Is there a more elegant solution to this?
Yes. Use only one handler, and decide dynamically what to do in the current state. Do not steadily bind and unbind handlers. Or use event delegation.
I have an anchor tag on my page, I want an event attached to it, which will fire when the display of this element change.
How can I write this event, and catch whenever the display of this element changes?
This is my way of doing on onShow, as a jQuery plugin. It may or may not perform exactly what you are doing, however.
(function($){
$.fn.extend({
onShow: function(callback, unbind){
return this.each(function(){
var _this = this;
var bindopt = (unbind==undefined)?true:unbind;
if($.isFunction(callback)){
if($(_this).is(':hidden')){
var checkVis = function(){
if($(_this).is(':visible')){
callback.call(_this);
if(bindopt){
$('body').unbind('click keyup keydown', checkVis);
}
}
}
$('body').bind('click keyup keydown', checkVis);
}
else{
callback.call(_this);
}
}
});
}
});
})(jQuery);
You can call this inside the $(document).ready() function and use a callback to fire when the element is shown, as so.
$(document).ready(function(){
$('#myelement').onShow(function(){
alert('this element is now shown');
});
});
It works by binding a click, keyup, and keydown event to the body to check if the element is shown, because these events are most likely to cause an element to be shown and are very frequently performed by the user. This may not be extremely elegant but gets the job done. Also, once the element is shown, these events are unbinded from the body as to not keep firing and slowing down performance.
You can't get an onshow event directly in JavaScript. Do remember that the following methods are non-standard.
IN IE you can use
onpropertychange event
Fires after the property of an element
changes
and for Mozilla
you can use
watch
Watches for a property to be assigned
a value and runs a function when that
occurs.
You could also override jQuery's default show method:
var orgShow = $.fn.show;
$.fn.show = function()
{
$(this).trigger( 'myOnShowEvent' );
orgShow.apply( this, arguments );
return this;
}
Now just bind your code to the event:
$('#foo').bind( "myOnShowEvent", function()
{
console.log( "SHOWN!" )
});
The code from this link worked for me: http://viralpatel.net/blogs/jquery-trigger-custom-event-show-hide-element/
(function ($) {
$.each(['show', 'hide'], function (i, ev) {
var el = $.fn[ev];
$.fn[ev] = function () {
this.trigger(ev);
return el.apply(this, arguments);
};
});
})(jQuery);
$('#foo').on('show', function() {
console.log('#foo is now visible');
});
$('#foo').on('hide', function() {
console.log('#foo is hidden');
});
However the callback function gets called first and then the element is shown/hidden. So if you have some operation related to the same selector and it needs to be done after being shown or hidden, the temporary fix is to add a timeout for few milliseconds.