Javascript newbie here.
Is there a "best practice" for placement of "if" statements in event delegation?
Context
I'm setting up event listeners using vanilla Javascript (I know jQuery etc. would simplify things, but let's stick to vanilla JS): there's an event listener on the parent element that invokes a function when a child is clicked. In our example, that function to-be-invoked lives elsewhere in the code.
Let's say I only want to take action when element with id=child-element is clicked. To do this, I use an "if" statement.
There are two obvious places I can put the if statement:
Within the event listener
Within the function
Question
Is (1) or (2) preferred? If so, why? ("Better memory management", "code is easier to read", etc.)
Example 1
var foo = {
bindEvent: function () {
document.getElementById('clickableElement').addEventListener('click', function (e) {
const clickTarget = e.target.id
if (clickTarget === 'child-element') {
foo.takeAnAction.bind(foo);
foo.takeAnAction();
};
});
},
takeAnAction: function () {
console.log('Click');
},
};
Example 2
var foo = {
bindEvent: function () {
document.getElementById("clickableElement").addEventListener("click",
foo.takeAnAction.bind(foo));
},
takeAnAction: function(e) {
if (e.target.id === "child-element") {
console.log('click');
};
},
};
Thanks!
I would go with option 1. The reason is that you can easily generalise it to handle any event delegation, so it's reusable. Sample:
var foo = {
bindEvent: function (selector, callback) { //accept a selector to filter with
document.getElementById('clickableElement').addEventListener('click', function (e) {
const clickTarget = e.target; //take the element
// check if the original target matches the selector
if (target.matches(selector)) {
takeAnAction.call(foo);
};
});
},
takeAnAction: function () {
console.log('Click');
},
};
foo.bindEvent("#child-element", foo.takeAction);
Now you can produce any amount of delegated event bindings. Adding another delegated binding is as simple as:
foo.bindEvent("#parent-element", foo.takeAction);
foo.bindEvent(".table-of-content", foo.takeAction);
With option 2, you will not need to change the implementation or produce new functions for each case:
/*... */
takeAnAction: function(event) {
if (event.target.id === "child-element") {
console.log('click');
};
},
takeAnActionForParent: function(event) {
if (event.target.id === "parent-element") {
console.log('click');
};
},
takeAnActionOnTableOfContentItems: function(event) {
if (event.target.classList.contains("table-of-content") {
console.log('click');
};
},
If you need to execute the same logic in each case, there is really no need to add a new function for every single case. So, for maintainability point of view, adding the logic in the event listener that would call another function is simpler to manage than producing different functions to be called.
Related
I'm using the following listener to listen for swipe and touch events on mobile. It has the following signature:
$.fn.onSwipe = function(handlers) { // adding a jQuery prototype.
my_element.addEventListener('touchmove', function(event) {
handleSwipe(event, handlers.left, handlers.right, handlers.up, handlers.down);
});
}
I like this because it allows me to:
$("foo").onSwipe({
left: (event) => { ... }, // I can define this right here.
right: (event) => { ... },
up: (event) => { ... },
down: (event) => { ... },
})
For left, right, and so on, I can define functions in the scope of assigning the listener while also being able see the event in the listener.
I've tried doing event.preventDefault in my direction handlers, but this still prevents scroll (which I'd like to enable by removing the event listener).
Problem:
I can't remove the event since it's anonymous.
I don't know how I would create a named function while being able to pass it in the same way such that the addEventListner will pass the event and direction handlers (like handlers.left()) to my handleSwipe event.
Note: I am not interested in using other third-party libraries.
Since you're already using jQuery, one option is to attach the listeners with .on instead, allowing you to remove them all with .off, without having to save a reference to them:
$.fn.onSwipe = function(handlers) { // adding a jQuery prototype.
$(this).on('touchmove', function(event) {
handleSwipe(event, handlers.left, handlers.right, handlers.up, handlers.down);
});
};
$.fn.offSwipe = function() {
$(this).off('touchmove');
};
If you might have other touchmove listeners attached to the same element, then you'll need to save a reference to the created function when called. Without using jQuery (except for the $.fn part):
const handlersByElement = new Map();
$.fn.onSwipe = function(handlers) { // adding a jQuery prototype.
const handler = function(event) {
handleSwipe(event, handlers.left, handlers.right, handlers.up, handlers.down);
};
for (const elm of this) {
handlersByElement.set(elm, handler);
elm.addEventListener('touchmove', handler);
}
};
$.fn.offSwipe = function() {
for (const elm of this) {
handlersByElement.set(elm, handler);
elm.removeEventListener('touchmove', handlersByElement.get(elm));
}
};
You can also use event namespaces with jQuery to simplify adding and removing of events without having to save a reference to them and without removing all events of that type, thanks #VLAZ:
$.fn.onSwipe = function(handlers) { // adding a jQuery prototype.
$(this).on('touchmove.myswiper', function(event) {
handleSwipe(event, handlers.left, handlers.right, handlers.up, handlers.down);
});
};
$.fn.offSwipe = function() {
$(this).off('touchmove.myswiper');
};
I want the events click and touchstart to trigger a function.
Of course this is simple with JQuery. $('#id').on('click touchstart', function{...});
But then once that event is triggered, I want that same handler to do something else when the events are triggered,
and then later, I want to go back to the original handling function.
It seems like there must be a cleaner way to do this than using $('#id').off('click touchstart'); and then re-applying the handler.
How should I be doing this?
You can create a counter variable in some construct in your javascript code that allows you to decide how you want to handle your event.
$(function() {
var trackClicks = (function() {
var clicks = true;
var getClicks = function() {
return clicks;
};
var eventClick = function() {
clicks = !clicks;
};
return {
getClicks: getClicks,
eventClicks: eventClicks
}
})();
$('#id').on('click touchstart', function {
if (trackClicks.getClicks()) {
handler1();
} else {
handler2();
}
trackClicks.eventClick();
});
function handler1() { //firsthandler};
function handler2() { //secondhandler};
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
The way I would do this is by creating a couple of functions for the handler function to call based on certain flags. Sudo code would be something like this:
function beginning_action() {
...
}
function middle() {
...
}
var beginning_state = true;
$('#id').on('click touchstart', function{
if(beginning_state) {
beginning_action();
} else {
middle();
}
});
Then all you need to do is change the variable beginning_state to change which function is called. Of course you would give them better names that describe what they do and not when they do it.
Additionally, if you want the handler to call more than two functions you can change the beginning_state variable from a boolean to an int and check it's value to determine which function to call.
Good luck!
I have a bunch of handlers that call up a specific jQuery plugin. I would like to refactor the code and create an object whose properties and methods can be passed to a wrapper which would then call up the plugin.
Problem: I have difficulties emulating the following statement:
$("li", opts.tgt).live("click", function () { GetContact(this); });
Does someone have some suggestions on how to proceed? TIA.
function InitAutoCompleteTest() { // Init custom autocomplete search
var opts = {
tgt: "#lstSug", crit: "#selCrit", prfxID: "sg_", urlSrv: gSvcUrl + "SrchForContact",
fnTest: function (str) { alert(str) },
fnGetData: function (el) { GetContact(el) }
}
$("input", "#divSrchContact").bind({
"keypress": function (e) { // Block CR (keypress fires before keyup.)
if (e.keyCode == 13) { e.preventDefault(); };
},
"keyup": function (e) { // Add suggestion list matching search pattern.
opts.el = this; $(this).msautocomplete(opts); e.preventDefault();
},
"dblclick": function (e) { // Clear search pattern.
$(this).val("");
}
});
opts.fnTest("Test"); // Works. Substituting the object method as shown works.
// Emulation attempts of below statement with object method fail:
// $("li", opts.tgt).live("click", function () { GetContact(this); });
$("li", opts.tgt).live({ "click": opts.fnGetData(this) }); // Hangs.
$("li", opts.tgt).live({ "click": opts.fnGetData }); // Calls up GetContact(el) but el.id in GetContact(el) is undefined
}
function GetContact(el) {
// Fired by clicking on #lstSug li. Extract from selected li and call web srv.
if (!el) { return };
var contID = el.id, info = $(el).text();
...
return false;
}
Edit
Thanks for the feedback. I finally used the variant proposed by Thiefmaster. I just wonder why the method must be embedded within an anonymous fn, since "opts.fnTest("Test");" works straight out of the box, so to speak.
$("li", opts.tgt).live({ "click": function () { opts.fnGetData(this); } });
Simply wrap them in an anonymous function:
function() {
opts.fnGetData();
}
Another option that requires a modern JS engine would be using .bind():
opts.fnGetData.bind(opts)
Full examples:
$("li", opts.tgt).live({ "click": opts.fnGetData.bind(opts) });
$("li", opts.tgt).live({ "click": function() { opts.fnGetData(); }});
Inside the callback you then use this to access the object.
If you want to pass the element as an argument, you can do it like this:
$("li", opts.tgt).live({ "click": function() { opts.fnGetData(this); }});
From documentation
.live( events, data, handler(eventObject) )
eventsA string containing a JavaScript event type, such as "click" or "keydown." As of jQuery 1.4 the string can contain multiple, space-separated event types or custom event names.
data A map of data that will be passed to the event handler.
handler(eventObject) A function to execute at the time the event is triggered.
Example:
$('#id').live('click', {"myValue":"someValue"}, function(evt){
console.log(evt.data["myValue"]); // someValue
});
JQuery live
This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Removing an anonymous event listener
I have the following cross browser function to add an event listener:
_SU3.addEventListener = function(elem, eventName, fn) {
if(elem.addEventListener ) {
elem.addEventListener(eventName, fn, false);
} else if (elem.attachEvent) {
elem.attachEvent('on'+eventName, fn);
}
};
I'm adding the listener like this:
_SU3.addEventListener(_show, "click", function(event) {
_SU3.getChildren(_show, uri, element);
});
Which is all fine. However I want to remove the listener after it has been called once. I.e. something like:
_SU3.getChildren = function(_show, url, element) {
... blah...
_SU3.removeEventListener(_show, 'click', ANON_FUNCTION);
};
But of course the listener function is anonymous so there's no function name to reference.
How can I remove the listener?
You need to keep a reference to the function:
var foo = function(event) { _SU3.getChildren(_show, uri, element); };
_SU3.addEventListener(_show, "click", foo);
...
_SU3.getChildren = function(_show, url, element) {
... blah...
_SU3.removeEventListener(_show, 'click', foo);
};
Make sure that the variable foo is in the scope of where you remove the event listener.
You can't. If you want to remove it, you have to store a reference to it. How else would you be able to distinguish it from the others?
Instead of removing the event listener, arrange for it to keep track of whether it's been called:
addOneShotListener = function(elem, eventName, fn) {
var triggered = false, handler = function(ev) {
if (triggered) return;
fn(ev);
triggered = true;
};
if(elem.addEventListener ) {
elem.addEventListener(eventName, handler, false);
} else if (elem.attachEvent) {
elem.attachEvent('on'+eventName, handler);
}
};
That variation on your original function just wraps the original handler (the "fn" passed in) with a function that only calls the handler the first time it is invoked. After that, it sets a flag and won't ever call the original handler function again.
If you only have one click event assigned at any one time to an element, why not set the onclick property? You can remove it anytime with element.onclick='';
I am trying to basically disable the click event on a <div> temporarily.
I have tried the following (preview):
$('hello').observe('click', function (e) {
e.stop();
});
$('hello').observe('click', function (e) {
alert('click not stopped!');
});
However, when #hello is clicked, the alert box still appears. I do not want the second attached handler to be called, and I do not want to change the second handler.
I will also accept a solution such as:
$('hello').observe('click', function (e) {
alert('click not stopped!');
});
$('hello').disableEvent('click');
// Now, handler won't be called
$('hello').observe('click', function (e) {
alert('click not stopped (2)!');
});
// New handler won't be called, either
$('hello').enableEvent('click');
// Now, handler will be called
I am using the Prototype.js framework. This doesn't seem to be a browser-specific issue.
When you assign handlers to events; you are basically just storing a set of functions to be executed when an event fires.
When an event fires, the handlers you've added are executed in the order they we're added. So if you we're to add three handlers to a div's click-event:
$("div").observe("click", function ()
{
alert("one");
});
$("div").observe("click", function ()
{
alert("two");
});
$("div").observe("click", function ()
{
alert("three");
});
.. you would get three alerts ("one", "two" and "three") when the click event of the div element fires. Those three alerts will still get shown, if you put in:
$("div").observe("click", function (e)
{
e.stop();
})
.. because you are only canceling the event for one particular handler. Not all associated handlers.
So what you will need to do is use a reference variable, which keeps track of wether the click event is allowed to fire:
var cancelClickEvent = true;
$("div").observe("click", function ()
{
// if cancelClickEvent is true, return the function early, to
// stop the code underneath from getting executed
if (cancelClickEvent) return;
// your code goes here
});
You will then need to implement the above if-clause in all your handlers.
Can't you just set the object's disabled property to true?
As I said in comments to roosteronacid's answer, I wrote an extension to Event.observe. Works in most browsers, but not IE.
// XXX HACK XXX
(function () {
var handlerCache = $A([ ]);
function findHandler(either) {
var pair = handlerCache.find(function (pair) {
return $A(pair).member(either);
});
return pair && pair[0];
}
function addHandler(handler) {
function newHandler(e) {
if (!e.halted) {
handler.apply(this, arguments);
}
}
handlerCache.push([ handler, newHandler ]);
return newHandler;
}
Event.observe = Event.observe.extended(function ($super, element, eventName, handler) {
handler = findHandler(handler) || addHandler(handler);
$super(element, eventName, handler);
});
Event.stopObserving = Event.stopObserving.extended(function ($super, element, eventName, handler) {
handler = findHandler(handler) || handler;
$super(element, eventName, handler);
});
Element.addMethods({
observe: Event.observe
});
Event.prototype.halt = function () {
this.halted = true;
};
}());
Note: Function.prototype.extended is a custom function which puts the original Event.observe in as $super.